Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 95 additions & 19 deletions bin/fm-capability-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,25 @@
# This header owns the wire format and selection contracts:
# - Log path: $FM_HOME/data/capability-outcomes.log (override: FM_CAPABILITY_LOG).
# - One append-only line per finished ship/scout teardown:
# <unix-epoch>|<task-type>|<harness>|<model>|<effort>|<outcome>
# <unix-epoch>|<task-type>|<harness>|<model>|<effort>|<outcome>[|<fix-rounds>[|<steers>]]
# Fields never contain '|' or newlines; invalid fields refuse the append.
# - Outcomes: green (normal landed teardown) or discarded (--force).
# The trailing counts are written only when derivable, each as one
# non-negative integer: fix-rounds is the number of earlier recorded
# pipeline attempts for the task's branch before its final attempt, and
# steers is the confirmed supervisor send count from state/<id>.steers.
# An absent fix-rounds count retains an empty field when steers is present;
# otherwise absent trailing counts are omitted, never guessed. Older
# six-field lines without them stay valid forever.
# - Outcomes (green means exactly what it claims):
# green validation passed on the first recorded pipeline attempt
# (fix-rounds 0)
# fixed validation passed only after earlier recorded attempts
# failed validation ran but its newest recorded attempt never
# completed
# unknown no validation result was derivable at teardown (scout
# reports, direct-PR/local-only delivery, or unavailable run
# records)
# discarded work was discarded by an approved --force teardown
# - Secondmate teardowns are not recorded (not a worker capability sample).
# - task-type is a free-form slug from meta task_type= when present, else kind
# (ship|scout). Firstmate should pass a stable slug at spawn for finer bins.
Expand All @@ -17,9 +33,10 @@
# cost-filtered profile set; this lib never invents a harness outside it and
# never bypasses third-party-model / crew-dispatch guards.
# - select=capability-recent ranks allowed profiles by green density
# (green / (green+discarded)) in the window; a sampled profile outranks an
# earlier unsampled one only when density > 0; all-zero or absent evidence
# keeps input (configured) order; no samples for a task-type keep the first.
# (first-try greens / all samples) in the window; a sampled profile
# outranks an earlier unsampled one only when density > 0; all-zero or
# absent evidence keeps input (configured) order; no samples for a
# task-type keep the first.
# - Scout tax (~10%): advisory CAPABILITY_SCOUT_TAX stderr suggestion of a
# different allowed profile; never changes the selected stdout profile.
# FM_CAPABILITY_SCOUT_TAX=0 disables; =1 forces; otherwise a roll
Expand Down Expand Up @@ -68,45 +85,102 @@ fm_capability_field_ok() {
}

# Append one outcome line. Args: task-type harness model effort outcome
# [fix-rounds] [steers]
# Each count is optional: a non-empty value must be a non-negative integer.
# An empty fix-rounds value retains its positional field when steers is present.
# Best-effort: creates data/ as needed; returns non-zero on invalid fields or
# write failure but never blocks teardown callers that ignore the status.
fm_capability_log_append() {
local task_type=$1 harness=$2 model=$3 effort=$4 outcome=$5
local log_path ts dir
local fix_rounds=${6:-} steers=${7:-}
local log_path ts dir line
case "$outcome" in
green|discarded) ;;
green|fixed|failed|unknown|discarded) ;;
*) return 1 ;;
esac
fm_capability_field_ok "$task_type" || return 1
fm_capability_field_ok "$harness" || return 1
fm_capability_field_ok "$model" || return 1
fm_capability_field_ok "$effort" || return 1
case "$fix_rounds" in
'') ;;
*[!0-9]*) return 1 ;;
esac
case "$steers" in
'') ;;
*[!0-9]*) return 1 ;;
esac
log_path=$(fm_capability_log_path)
dir=$(dirname "$log_path")
mkdir -p "$dir" || return 1
ts=$(fm_capability_now)
fm_capability_field_ok "$ts" || return 1
printf '%s|%s|%s|%s|%s|%s\n' "$ts" "$task_type" "$harness" "$model" "$effort" "$outcome" >> "$log_path"
line="$ts|$task_type|$harness|$model|$effort|$outcome"
case "$fix_rounds:$steers" in :) ;; *) line="$line|$fix_rounds" ;; esac
case "$steers" in '') ;; *) line="$line|$steers" ;; esac
printf '%s\n' "$line" >> "$log_path"
}

# Derive the teardown capability outcome from captured `no-mistakes runs` text.
# Args: branch runs-output (empty when unavailable). Rows are newest-first,
# whitespace-separated: <status> <branch> <sha> <date> <time> [url]; only
# completed/failed/cancelled rows for the branch are samples. Prints
# "<outcome>|<fix-rounds>" where fix-rounds is empty when not derivable:
# - no row for the branch -> unknown| (never guessed)
# - newest attempt completed, first -> green|0 (first try)
# - newest attempt completed, later -> fixed|<earlier-attempt-count>
# - newest attempt not completed -> failed|
fm_capability_outcome_from_runs() {
local branch=$1 runs=$2
[ -n "$branch" ] || { printf 'unknown|\n'; return 0; }
printf '%s\n' "$runs" | awk -v want="$branch" '
($1 == "completed" || $1 == "failed" || $1 == "cancelled") && $2 == want {
if (seen != 1) { first = $1; seen = 1 }
total++
}
END {
if (total == 0) { printf "unknown|\n"; exit }
if (first == "completed") {
if (total == 1) { printf "green|0\n" }
else { printf "fixed|%d\n", total - 1 }
} else {
printf "failed|\n"
}
}
'
}

# Record teardown evidence from already-loaded meta fields.
# Args: kind force_flag harness model effort [task_type]
# force_flag is "--force" or empty. No-ops for secondmate and missing harness.
# Record teardown evidence from already-loaded meta fields plus the derived
# validation outcome. Args: kind force_flag harness model effort task_type
# outcome fix_rounds steers
# force_flag is "--force" or empty: a forced discard always records discarded
# without counts, whatever was derived. outcome must be one of the wire
# outcomes whenever recording happens (the caller derives it from recorded
# validation evidence); fix_rounds/steers are numeric strings or empty.
# Unreadable or missing inputs skip the record rather than guess. No-ops for
# secondmate and missing harness, and this function never fails its caller.
fm_capability_record_teardown() {
local kind=$1 force=$2 harness=$3 model=$4 effort=$5 task_type=${6:-}
local outcome
local outcome=${7:-} fix_rounds=${8:-} steers=${9:-}
[ "$kind" = secondmate ] && return 0
[ -n "$harness" ] || return 0
if [ "$force" = "--force" ]; then
outcome=discarded
fix_rounds=
steers=
fi
case "$outcome" in
green|fixed|failed|unknown|discarded) ;;
*) return 0 ;;
esac
case "$fix_rounds" in ''|*[!0-9]*) fix_rounds= ;; esac
case "$steers" in ''|*[!0-9]*) steers= ;; esac
[ -n "$model" ] || model=default
[ -n "$effort" ] || effort=default
[ -n "$task_type" ] || task_type=$kind
[ -n "$task_type" ] || task_type=ship
if [ "$force" = "--force" ]; then
outcome=discarded
else
outcome=green
fi
fm_capability_log_append "$task_type" "$harness" "$model" "$effort" "$outcome" || true
fm_capability_log_append "$task_type" "$harness" "$model" "$effort" \
"$outcome" "$fix_rounds" "$steers" || true
}

# Print recent matching lines for a task-type (stdout), one wire line each.
Expand All @@ -127,7 +201,9 @@ fm_capability_recent_lines() {
# Summarize green density per harness|model|effort for a task-type.
# Prints lines: <harness>|<model>|<effort>|<green>|<total>|<density_percent>
# sorted by density desc, then total desc, then key asc. Density is integer
# percent (green*100/total). Args: task-type
# percent (green*100/total); green counts first-try passes only, while fixed,
# failed, unknown, and discarded samples still count toward total.
# Args: task-type
fm_capability_summarize() {
local task_type=$1
fm_capability_recent_lines "$task_type" | awk -F'|' '
Expand Down
12 changes: 12 additions & 0 deletions bin/fm-send.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
# footer appears, so an immediate peek would otherwise see the stale idle pane.
# The pause is fm-send-only; the shared submit core (used by the away-mode daemon,
# which only needs "submitted") does not pay it, and the --key path is unaffected.
#
# Every confirmed text submit to a task-selector target appends one line to
# state/<id>.steers, the per-task supervisor steer counter that teardown reads
# for the capability outcome log and removes with the rest of the volatile
# state. Best-effort only; explicit backend targets and the --key path never
# write it.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down Expand Up @@ -292,6 +298,12 @@ else
exit 1
;;
esac
# Confirmed submit: record one steer line for task-selector targets so
# teardown can report how many supervisor sends the task received. The file
# lives with the task's other volatile state and is never read elsewhere.
if [ -n "$TARGET_SELECTOR" ]; then
printf 'steer\n' >> "$STATE/$(fm_send_id_from_meta "$TARGET_META").steers" 2>/dev/null || true
fi
if [ -n "$PENDING_REPLY_CORR" ]; then
if fm_pending_reply_confirm_delivery "$STATE" "$PENDING_REPLY_CORR"; then
:
Expand Down
57 changes: 54 additions & 3 deletions bin/fm-teardown.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@
# checks before any destructive return. Teardown output notes every wait, retry, and
# removal so the operator can see what happened.
# On successful ship/scout cleanup, teardown appends one capability outcome line to
# data/capability-outcomes.log (bin/fm-capability-lib.sh owns the wire format).
# data/capability-outcomes.log. green means the task's validation passed on the
# first try: the outcome is derived from the repo-scoped no-mistakes run records
# for the task branch (gathered before destructive cleanup), never from the
# teardown mode, and anything not derivable records unknown. bin/fm-capability-lib.sh
# owns the wire format and outcome rules.
set -eu

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
Expand Down Expand Up @@ -110,6 +114,8 @@ SUB_HOME_MARKER=".fm-secondmate-home"
. "$SCRIPT_DIR/fm-public-followup-lib.sh"
# shellcheck source=bin/fm-capability-lib.sh
. "$SCRIPT_DIR/fm-capability-lib.sh"
# shellcheck source=bin/fm-timeout-lib.sh
. "$SCRIPT_DIR/fm-timeout-lib.sh"
# shellcheck source=bin/fm-worktree-lease-lib.sh
. "$SCRIPT_DIR/fm-worktree-lease-lib.sh"
# shellcheck source=bin/fm-secondmate-registry-lib.sh
Expand Down Expand Up @@ -156,6 +162,43 @@ HARNESS=$(grep '^harness=' "$META" | cut -d= -f2- || true)
MODEL=$(grep '^model=' "$META" | cut -d= -f2- || true)
EFFORT=$(grep '^effort=' "$META" | cut -d= -f2- || true)
TASK_TYPE=$(grep '^task_type=' "$META" | cut -d= -f2- || true)
# Capability evidence gathering, best-effort and read-only, BEFORE any
# destructive step: the task branch from the still-present worktree and the
# recorded pipeline history from the repo-scoped no-mistakes run table (the
# same durable record fm-crew-state.sh treats as the coarse run authority;
# bounded probe via bin/fm-timeout-lib.sh). The worktree is the correct
# working directory for that repo-scoped query and disappears below. Anything
# unavailable degrades to outcome=unknown - never guessed, never fatal
# (bin/fm-capability-lib.sh owns the outcome rules). The bound keeps very long
# run histories honest-but-bounded: only the newest 200 recorded attempts are
# considered.
CAP_OUTCOME=
CAP_FIX_ROUNDS=
CAP_STEERS=
if [ "$KIND" != secondmate ] && [ -n "$HARNESS" ]; then
if [ "$FORCE" != "--force" ]; then
CAP_OUTCOME=unknown
fi
CAP_BRANCH=
if [ -d "$WT" ]; then
CAP_BRANCH=$(git -C "$WT" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
case "$CAP_BRANCH" in
''|HEAD) CAP_BRANCH= ;;
esac
fi
if [ -n "$CAP_BRANCH" ] && [ "$FORCE" != "--force" ]; then
CAP_RUNS=$( ( cd "$WT" && fm_run_timeout 20 no-mistakes runs --limit 200 ) 2>/dev/null || true )
CAP_EVIDENCE=$(fm_capability_outcome_from_runs "$CAP_BRANCH" "$CAP_RUNS")
case "$CAP_EVIDENCE" in
*\|*)
CAP_OUTCOME=${CAP_EVIDENCE%%|*}
CAP_FIX_ROUNDS=${CAP_EVIDENCE#*|}
case "$CAP_FIX_ROUNDS" in ''|*[!0-9]*) CAP_FIX_ROUNDS= ;; esac
;;
*) CAP_OUTCOME=$CAP_EVIDENCE ;;
esac
fi
fi
PUBLIC_FOLLOWUP_HOME=$FM_HOME
PUBLIC_FOLLOWUP_STATE=$STATE
PUBLIC_FOLLOWUP_WORK_HOME=main
Expand Down Expand Up @@ -1437,9 +1480,17 @@ rm -rf "$STATE/browse/$ID"
remove_pr_poll_artifacts "$STATE" "$ID" || exit 1
fm_pending_reply_remove_task "$STATE" "$ID"
# Record capability evidence before meta disappears (ship/scout only; best-effort).
fm_capability_record_teardown "$KIND" "$FORCE" "$HARNESS" "$MODEL" "$EFFORT" "$TASK_TYPE"
# The steer counter is a volatile state file read here and removed below.
if [ -f "$STATE/$ID.steers" ]; then
CAP_STEERS=$(grep -c . "$STATE/$ID.steers" 2>/dev/null || true)
case "$CAP_STEERS" in
''|*[!0-9]*) CAP_STEERS= ;;
esac
fi
fm_capability_record_teardown "$KIND" "$FORCE" "$HARNESS" "$MODEL" "$EFFORT" \
"$TASK_TYPE" "$CAP_OUTCOME" "$CAP_FIX_ROUNDS" "$CAP_STEERS"
remove_captain_held_surfaced_markers "$STATE" "$ID" "$T"
rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.meta" "$STATE/$ID.pi-ext.ts" "$STATE/$ID.prime-ext.ts" "$STATE/$ID.grok-turnend-token"
rm -f "$STATE/$ID.status" "$STATE/$ID.turn-ended" "$STATE/$ID.meta" "$STATE/$ID.pi-ext.ts" "$STATE/$ID.prime-ext.ts" "$STATE/$ID.grok-turnend-token" "$STATE/$ID.steers"
rm -rf "$STATE/$ID.kimi-home" "$STATE/$ID.prime-agent-home"
"$FM_ROOT/bin/fm-visible-status.sh" --all >/dev/null 2>&1 || true
if [ "$KIND" != scout ] && [ "$KIND" != secondmate ] && [ "$MODE" != local-only ]; then
Expand Down
1 change: 1 addition & 0 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ tests/fm-gotmp.test.sh
tests/fm-toolchain-drift.test.sh
tests/fm-sessionstart-nudge.test.sh
tests/fm-operational-input.test.sh
tests/fm-send-steer-count.test.sh
tests/fm-project-presentation.test.sh
tests/fm-bearings-skill.test.sh
tests/fm-kimi-worker.test.sh
Expand Down
17 changes: 12 additions & 5 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -310,24 +310,31 @@ Secondmate homes inherit this file from the primary, so a secondmate's own crewm
## Capability outcome log (data/capability-outcomes.log)

Firstmate records how each harness/model/effort combination performs per task type so dispatch can consult recent evidence without replacing cost rules.
`bin/fm-capability-lib.sh` owns the wire format, the 7-day recency window, ranking, and the advisory scout-tax suggestion.
`bin/fm-teardown.sh` appends one line on successful ship or scout cleanup (not secondmate), and `bin/fm-dispatch-select.sh --task-type <slug>` surfaces the window for that slug.
`bin/fm-capability-lib.sh` owns the wire format, the outcome rules, the 7-day recency window, ranking, and the advisory scout-tax suggestion.
`bin/fm-teardown.sh` appends one line on ship or scout cleanup (not secondmate), and `bin/fm-dispatch-select.sh --task-type <slug>` surfaces the window for that slug.
The log is a captain-inspectable plain-text file under the home's private `data/`; there is no new runtime dependency.

Each append-only line is:

```text
<unix-epoch>|<task-type>|<harness>|<model>|<effort>|<outcome>
<unix-epoch>|<task-type>|<harness>|<model>|<effort>|<outcome>[|<fix-rounds>[|<steers>]]
```

`outcome` is `green` for a normal landed teardown or `discarded` for `--force`.
`outcome` is derived from the task's recorded validation result at teardown, never from the teardown mode.
`green` means validation passed on the first try: exactly one pipeline attempt is recorded for the task branch.
`fixed` means validation passed only after earlier recorded attempts, and its `fix-rounds` field counts those earlier attempts.
`failed` means validation ran but its newest recorded attempt never completed.
`unknown` means no validation result was derivable (scout reports, direct-PR or local-only delivery, or unavailable run records).
`discarded` records an approved `--force` teardown.
The trailing counts are written only when derivable: `steers` counts confirmed supervisor sends recorded per task by `bin/fm-send.sh`, and an empty `fix-rounds` slot is retained when only `steers` is known.
Older six-field lines without trailing counts stay valid, and readers treat missing counts as absent rather than guessing them.
`task-type` comes from meta `task_type=` when `fm-spawn.sh --task-type <slug>` recorded it, otherwise from `kind` (`ship` or `scout`).
Fields never contain `|` or newlines.

Cost rules in `config/crew-dispatch.json` always win: evidence only ranks or advises within the already cost-filtered `use` array and never bypasses the third-party-model guard.
With `--task-type`, dispatch-select prints `CAPABILITY_EVIDENCE:` lines on stderr for firstmate.
About 10% of those dispatches may also print one `CAPABILITY_SCOUT_TAX:` suggestion naming a different allowed profile; that suggestion is advisory and never changes the selected stdout profile.
`select: capability-recent` makes ranking choose the best recent green density inside the allowed array; a sampled profile outranks an earlier unsampled one only when density is greater than 0, and absent or all-zero evidence keeps configured input order.
`select: capability-recent` makes ranking choose the best recent green density inside the allowed array, where green strictly means first-try passes; a sampled profile outranks an earlier unsampled one only when density is greater than 0, and absent or all-zero evidence keeps configured input order.
Overrides for tests and ops live under Environment variables (`FM_CAPABILITY_*`).

## Toolchain
Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-home-seed.sh` | Transactionally provision a secondmate home and maintain `data/secondmates.md` |
| `fm-spawn.sh` | Spawn crewmates, scouts, `id=repo` batches, and secondmates on the resolved harness and runtime backend |
| `fm-dispatch-select.sh` | Resolve a matched crew-dispatch rule to one concrete profile, owning `quota-balanced` and `capability-recent` selection plus capability evidence surfacing |
| `fm-capability-lib.sh` | Append-only capability outcome log, 7-day reader, ranking, and advisory scout-tax helpers |
| `fm-capability-lib.sh` | Append-only capability outcome log (green means first-try validation pass), 7-day reader, ranking, and advisory scout-tax helpers |
| `fm-backend.sh` | Runtime-backend selection, meta helpers, selector resolution, and operation dispatch |
| `fm-backend-hometag-lib.sh` | Shared per-installation home-tag derivation for zellij tab and cmux workspace titles |
| `fm-composer-lib.sh` | Single fleet-wide owner of composer-content classification for all backends |
Expand Down
3 changes: 3 additions & 0 deletions fork-surface.conf
Original file line number Diff line number Diff line change
Expand Up @@ -329,13 +329,16 @@ status = active
why = Dispatch can consider observed outcomes for a task type alongside configured cost rules.
owns = bin/fm-capability-lib.sh
owns = tests/fm-capability.test.sh
owns = tests/fm-send-steer-count.test.sh
modifies = bin/fm-dispatch-select.sh
modifies = bin/fm-send.sh
modifies = bin/fm-spawn.sh
modifies = bin/fm-teardown.sh
modifies = bin/fm-bootstrap.sh
anchor = bin/fm-teardown.sh :: fm_capability_record_teardown
anchor = bin/fm-dispatch-select.sh :: ^emit_capability_advisories\(\)
proves = tests/fm-capability.test.sh
proves = tests/fm-send-steer-count.test.sh
assert = files+test
commits = 52cad29 bb0867c
topology = independent
Expand Down
Loading
Loading