Skip to content

feat(bin): record why every agent dispatch was necessary at the spawn chokepoint - #1830

Open
sbracewell64 wants to merge 3 commits into
kunchenguid:mainfrom
sbracewell64:fm/cfvc-08-reasoning-required
Open

feat(bin): record why every agent dispatch was necessary at the spawn chokepoint#1830
sbracewell64 wants to merge 3 commits into
kunchenguid:mainfrom
sbracewell64:fm/cfvc-08-reasoning-required

Conversation

@sbracewell64

Copy link
Copy Markdown

Intent

Implement CFVC-08 from the approved CFVC remediation plan (data/cfvc-synthesis-and-plan/report.md, increment CFVC-08; commission data/captain-rulings-2026-08-06/cfvc-remediation-commission.md). The spec's completion criteria, tests, retirement obligations, migration and rollback are binding.

GOAL: every agent dispatch records why an agent turn was necessary, in a closed vocabulary, so the question is answerable from the record. Commission section 5: 'Why was an agent turn necessary here? If it cannot answer, investigate whether the turn should have been code.'

TARGET STATE (from the spec): bin/fm-spawn.sh records reasoning_required, reason_code, capability_floor and escalation_policy into the state/.meta it already writes. RECORDS; DOES NOT ENFORCE - no dispatch is blocked for reasoning too little. bin/fm-spawn.sh was chosen because it is already the chokepoint that validates mode/yolo/model/worktree; the spec explicitly says no new component is needed. Value-creator split: CODE records and derives the derivable fields; the agent authors only what is checkable but not derivable.

SCHEMA: the closed reason_code enum is Lane B's eight reasoning codes (NL_RULE_CLASSIFICATION, CONTRACT_SCOPE_JUDGMENT, MULTIPLE_PLAUSIBLE_ROOT_CAUSES, NOVEL_DECOMPOSITION, UNFAMILIAR_CODE, SEMANTIC_REVIEW, SYNTHESIS, AMBIGUOUS_INTENT) plus TOOLING_GAP, which is explicitly NOT a reasoning code: it names a turn taken only because a deterministic reader is broken or absent, is counted separately, never as justified reasoning, and always files work.

CERTIFICATION REQUIRED BY THE SPEC: TOOLING_GAP must file a work item, or the code becomes a laundering label - the single failure mode that would make this increment worse than nothing. Implemented as: --reason-code TOOLING_GAP requires --tooling-gap-item naming a backlog item that is currently OPEN in this home's data/backlog.md, verified by code before the spawn proceeds; absent, already-closed and prefix-matching items are all refused.

COMPLETION CRITERIA: (a) every new spawn record carries all four fields; (b) an out-of-enum reason_code is refused with a stable token; (c) a TOOLING_GAP dispatch produces a filed work item, asserted by test.

DELIBERATE DEVIATION FROM CRITERION (a), documented in the commit body as the binding constraints require: --secondmate spawns are excluded and refuse all three new flags rather than carrying the fields. Rationale: a secondmate provisions a standing home rather than dispatching a task (AGENTS.md section 10 already keeps a secondmate out of the backlog for the same reason), and Lane B derived the enum entirely from task invocations, so demanding one of its codes for a provisioning action would manufacture exactly the rubber-stamp answer the enum exists to prevent. It would also break bin/fm-remote-secondmate-control.sh's provisioning path. Per the commission's rule for stale criteria, (a) is replaced by a stronger tested pair: every TASK dispatch carries all four fields, AND a secondmate spawn that passes one is refused with a stable token rather than silently defaulted. Absent fields read as unknown, never as justified, so a secondmate record can never be miscounted.

DERIVED, NOT DECLARED: reasoning_required follows from the reason code (TOOLING_GAP yields no, every reasoning code yields yes) and escalation_policy follows from kind plus the already-validated delivery contract, so neither can be declared into disagreement with the record it summarizes. capability_floor is taken verbatim from config/crew-dispatch.json and refused when the config does not define it; an omitted floor falls back to that file's default route floor; a home with no dispatch config records 'unconfigured' and refuses an explicit floor; a dispatch config that exists but cannot be read fails closed as unverifiable rather than degrading to unconfigured.

CONSEQUENTIAL CHANGE: bin/fm-promote.sh now recomputes escalation_policy, because promotion changes the delivery contract that field is derived from; leaving it would keep a scout's report-only posture on a task that can now reach a merge gate. The recorded reason_code is deliberately preserved across promotion - the same agent continues and why its turn was necessary did not change.

RETIREMENT OBLIGATION (binding): no name or mechanism may be introduced without retiring what it replaces, and both must not be left alive. Here current_state was ABSENT as a record, so nothing is replaced in code. What retires is the untracked category of agent turns taken because a reader is broken: before this, such a turn was indistinguishable in the record from justified reasoning, and TOOLING_GAP plus its refusing filing check is what ends that. No transitional second path exists - --reason-code is the only way to record a reason and it is required rather than optional. Verified by grep that no competing 'reason' concept or second meta field list exists in docs/ or .agents/.

MIGRATION AND ROLLBACK per spec: additive; absent fields on existing metas read as unknown, never as justified. Rollback is to stop writing the fields. No strict meta-key validation exists anywhere in bin/, so the added keys are safely additive.

ARCHITECTURE: bin/fm-reasoning-lib.sh is a new small library that is the single owner of the closed enum, the derivations, the stable refusal tokens, the floor vocabulary read and the open-backlog-item check. This follows the repo's one-owner rule from the firstmate-coding-guidelines skill, which was loaded before editing shared tracked material (bin/, AGENTS.md). AGENTS.md section 2's meta field list and section 7's dispatch paragraph were updated; per the knowledge-placement tree the exact flag mechanics live in the script header and --help rather than in AGENTS.md prose.

CERTIFICATION EVIDENCE (the commission requires red-capable verifiers; absence is never evidence): tests/fm-reasoning-required.test.sh has eight cases and EVERY ONE was witnessed RED against the pre-change bin/fm-spawn.sh and bin/fm-promote.sh before being accepted green. Three further targeted negative controls were run and witnessed red: loosening the open-item match (the already-closed and prefix rows go red), making reasoning_required always yes (the TOOLING_GAP row goes red), and degrading an unreadable dispatch config to unconfigured (the unverifiable-floor row goes red). One test was deliberately rewritten because its first version asserted only the ABSENCE of a refusal and was vacuously green against the reverted implementation; it now asserts the recorded value positively. The lint gate was itself shown able to reject (exit 1 on a deliberate violation) rather than trusted for being quiet.

SCOPE OF TEST EDITS: 18 existing test files carry the new required flag at their ship and scout spawn call sites. These are mechanical flag additions at call sites, not weakened assertions.

KNOWN PRE-EXISTING FAILURES, proven by stashed baseline runs on the unmodified contribution base rather than asserted: two scout-teardown decision-gate cases (tests/fm-backend.test.sh, tests/fm-backend-orca.test.sh) refuse because this environment has tasks-axi 0.2.3 against the 0.2.4 floor required by bin/fm-tasks-axi-lib.sh, and one Pi extension case (tests/fm-busy-adapter-wiring.test.sh) fails on a Node ESM loader error. All three reproduce identically with these changes reverted. I did break the fm-backend.test.sh symlinked-prefix spawn case mid-way and fixed it; that fix is what exposed the pre-existing tasks-axi failure behind it, since the file previously aborted earlier.

CONSTRAINTS THAT APPLY: this commission does NOT create new merge authority (ruling B4, autonomous landing NOT AUTHORIZED); no new scheduler, daemon, wake queue or LoopSpec runner was built; the deterministic Runtime keeps sole ownership of bounded-work identity.

What Changed

  • bin/fm-spawn.sh now requires --reason-code on ship and scout dispatches and records four fields into the state/<id>.meta it already writes: reason_code (a closed vocabulary of eight reasoning codes plus TOOLING_GAP), derived reasoning_required and escalation_policy, and capability_floor taken from config/crew-dispatch.json. Out-of-enum codes, undefined floors, unreadable dispatch configs, and newline-bearing flag values are refused with stable tokens; TOOLING_GAP additionally requires --tooling-gap-item naming a currently-open data/backlog.md item, matched literally. --secondmate spawns refuse all three new flags rather than carrying the fields, since they provision a home rather than dispatch a task.
  • New bin/fm-reasoning-lib.sh is the single owner of the enum, the derivations, the refusal tokens, the floor-vocabulary read (handling both object- and array-form default in crew-dispatch config), and the open-backlog-item check; bin/fm-promote.sh recomputes escalation_policy on promotion because promotion changes the delivery contract it is derived from, while preserving the recorded reason_code. The array-form floor read, literal gap-item matching, and meta-line injection hardening came out of the review pass on this branch.
  • tests/fm-reasoning-required.test.sh adds a 10-case suite covering the enum, all refusal paths, the TOOLING_GAP filing certification, and promotion recompute; 18 existing test files gain the new required flag at their spawn call sites, and AGENTS.md, docs/architecture.md, docs/configuration.md, and docs/examples/crew-dispatch.json document the new fields and the capability-floor dispatch axis.

Risk Assessment

✅ Low: The fix commit is tightly scoped to the six previously reported defects, each fix is verified working against the real config shapes and adversarial inputs, it reuses the repo's existing dispatch-profile normalization instead of inventing a second one, and the one behavior change (the new local-only escalation token) has no consumers anywhere in the repo since the field is record-only by design.

Testing

I validated the intent at the surface an end user actually touches: a CLI transcript that drives the real fm-spawn.sh and fm-promote.sh against a real git worktree and reads back the resulting state/<id>.meta records, showing all four fields on ship and scout dispatches, the derived escalation policy tracking kind/mode/yolo from an unchanged reason code, an omitted floor falling back to the config's default route floor, and every refusal path carrying its stable token while writing no metadata — including TOOLING_GAP refused against unfiled and already-closed backlog items but accepted against the open one with reasoning_required=no. The new 10-case suite passes and is selected by the portable-serial CI lane, and I confirmed it is red-capable rather than vacuous by mutating the derivation and the backlog-item match and watching the relevant cases fail. Ten touched or adjacent test files pass; the three that fail (two tasks-axi version-floor cases and one Node ESM .ts loader case) produce byte-identical results on the unmodified base commit, so they are environmental and pre-existing. The worktree is clean and all evidence lives in the dedicated evidence directory.

Evidence: End-to-end CLI transcript: dispatch justification recorded, refused, derived and recomputed

1. A ship dispatch records all four justification fields $ fm-spawn.sh impl-task <project> codex --mode no-mistakes --yolo off &#10;--reason-code NL_RULE_CLASSIFICATION --capability-floor F-IMPL-MED exit=0 --- state/impl-task.meta (the record on disk) --- kind=ship mode=no-mistakes yolo=off reasoning_required=yes reason_code=NL_RULE_CLASSIFICATION capability_floor=F-IMPL-MED escalation_policy=captain-approves-gates 3. The enum is closed $ ... --reason-code HARD_PROBLEM error: FM_SPAWN_REASON_CODE_UNKNOWN: 'HARD_PROBLEM' is not a reason code; the vocabulary is closed because free-text reasons cannot be counted. One of: NL_RULE_CLASSIFICATION CONTRACT_SCOPE_JUDGMENT MULTIPLE_PLAUSIBLE_ROOT_CAUSES NOVEL_DECOMPOSITION UNFAMILIAR_CODE SEMANTIC_REVIEW SYNTHESIS AMBIGUOUS_INTENT TOOLING_GAP exit=1 record written? no - nothing was dispatched 4. TOOLING_GAP must file work $ ... --reason-code TOOLING_GAP --tooling-gap-item already-repaired-reader (filed, but CLOSED) error: FM_SPAWN_TOOLING_GAP_ITEM_UNFILED: no open work item 'already-repaired-reader' in .../data/backlog.md; a TOOLING_GAP dispatch is only recordable once the reader's repair is actually filed and still open exit=1 $ ... --tooling-gap-item fleet-view-exits-nonzero exit=0 --- state/gap-ok.meta --- reasoning_required=no reason_code=TOOLING_GAP tooling_gap_item=fleet-view-exits-nonzero 8. The whole fleet record, answerable from the state directory alone TASK KIND REASONING REASON_CODE FLOOR ESCALATION gap-ok ship no TOOLING_GAP F-GEN captain-approves-gates impl-task ship yes NL_RULE_CLASSIFICATION F-IMPL-MED captain-approves-gates local-task ship yes UNFAMILIAR_CODE F-GEN captain-approves-local-merge scout-task ship yes MULTIPLE_PLAUSIBLE_ROOT_CAUSES F-GEN firstmate-routine-gates yolo-task ship yes NL_RULE_CLASSIFICATION F-GEN firstmate-routine-gates


════════════════════════════════════════════════════════════════════════
1. A ship dispatch records all four justification fields
════════════════════════════════════════════════════════════════════════

$ fm-spawn.sh impl-task <project> codex --mode no-mistakes --yolo off \
      --reason-code NL_RULE_CLASSIFICATION --capability-floor F-IMPL-MED
exit=0

--- state/impl-task.meta (the record on disk) ---
  kind=ship
  mode=no-mistakes
  yolo=off
  reasoning_required=yes
  reason_code=NL_RULE_CLASSIFICATION
  capability_floor=F-IMPL-MED
  escalation_policy=captain-approves-gates

════════════════════════════════════════════════════════════════════════
2. The derived fields follow their sources, never a caller declaration
════════════════════════════════════════════════════════════════════════

$ ... --mode no-mistakes --yolo on --reason-code NL_RULE_CLASSIFICATION   (no --capability-floor)
exit=0   # same reason code, yolo flips the policy; omitted floor falls back to the config default route

--- state/yolo-task.meta (the record on disk) ---
  kind=ship
  mode=no-mistakes
  yolo=on
  reasoning_required=yes
  reason_code=NL_RULE_CLASSIFICATION
  capability_floor=F-GEN
  escalation_policy=firstmate-routine-gates

$ ... --mode local-only --yolo off --reason-code UNFAMILIAR_CODE
exit=0   # a local-only ship never reaches a PR merge gate

--- state/local-task.meta (the record on disk) ---
  kind=ship
  mode=local-only
  yolo=off
  reasoning_required=yes
  reason_code=UNFAMILIAR_CODE
  capability_floor=F-GEN
  escalation_policy=captain-approves-local-merge

$ ... --scout --reason-code MULTIPLE_PLAUSIBLE_ROOT_CAUSES
exit=0   # a scout holds no merge authority at all

--- state/scout-task.meta (the record on disk) ---
  kind=scout
  reasoning_required=yes
  reason_code=MULTIPLE_PLAUSIBLE_ROOT_CAUSES
  capability_floor=F-GEN
  escalation_policy=report-only

════════════════════════════════════════════════════════════════════════
3. The enum is closed: an out-of-enum reason code is refused with a stable token
════════════════════════════════════════════════════════════════════════

$ ... --reason-code HARD_PROBLEM
error: FM_SPAWN_REASON_CODE_UNKNOWN: 'HARD_PROBLEM' is not a reason code; the vocabulary is closed because free-text reasons cannot be counted. One of: NL_RULE_CLASSIFICATION CONTRACT_SCOPE_JUDGMENT MULTIPLE_PLAUSIBLE_ROOT_CAUSES NOVEL_DECOMPOSITION UNFAMILIAR_CODE SEMANTIC_REVIEW SYNTHESIS AMBIGUOUS_INTENT TOOLING_GAP
exit=1
record written? no - nothing was dispatched

$ ... (no --reason-code at all)
error: FM_SPAWN_REASON_CODE_REQUIRED: every dispatch records why an agent turn was necessary; pass --reason-code <NL_RULE_CLASSIFICATION CONTRACT_SCOPE_JUDGMENT MULTIPLE_PLAUSIBLE_ROOT_CAUSES NOVEL_DECOMPOSITION UNFAMILIAR_CODE SEMANTIC_REVIEW SYNTHESIS AMBIGUOUS_INTENT TOOLING_GAP>. Use TOOLING_GAP when the turn is only needed because a deterministic reader is broken or absent - it is not a reasoning code and files repair work instead.
exit=1
record written? no - nothing was dispatched

════════════════════════════════════════════════════════════════════════
4. TOOLING_GAP must file work, and is never counted as justified reasoning
════════════════════════════════════════════════════════════════════════

$ ... --reason-code TOOLING_GAP --tooling-gap-item no-such-repair   (never filed)
error: FM_SPAWN_TOOLING_GAP_ITEM_UNFILED: no open work item 'no-such-repair' in /tmp/fm-cfvc08-demo.H5pDkm/home/data/backlog.md; a TOOLING_GAP dispatch is only recordable once the reader's repair is actually filed and still open
exit=1

$ ... --reason-code TOOLING_GAP --tooling-gap-item already-repaired-reader   (filed, but CLOSED)
error: FM_SPAWN_TOOLING_GAP_ITEM_UNFILED: no open work item 'already-repaired-reader' in /tmp/fm-cfvc08-demo.H5pDkm/home/data/backlog.md; a TOOLING_GAP dispatch is only recordable once the reader's repair is actually filed and still open
exit=1

$ grep -n 'fleet-view-exits-nonzero' data/backlog.md   # the repair that IS filed and open
  4:- [ ] fleet-view-exits-nonzero - The fleet view reader exits 1 (repo: firstmate) (kind: ship)

$ ... --reason-code TOOLING_GAP --tooling-gap-item fleet-view-exits-nonzero
exit=0

--- state/gap-ok.meta (the record on disk) ---
  kind=ship
  mode=no-mistakes
  yolo=off
  reasoning_required=no
  reason_code=TOOLING_GAP
  capability_floor=F-GEN
  escalation_policy=captain-approves-gates
  tooling_gap_item=fleet-view-exits-nonzero

════════════════════════════════════════════════════════════════════════
5. A capability floor the routing config never defines is refused
════════════════════════════════════════════════════════════════════════

$ ... --reason-code SYNTHESIS --capability-floor F-INVENTED
error: FM_SPAWN_CAPABILITY_FLOOR_UNKNOWN: 'F-INVENTED' is not a floor defined by /tmp/fm-cfvc08-demo.H5pDkm/home/config/crew-dispatch.json. Defined: F-GEN F-IMPL-MED F-UTIL
exit=1

════════════════════════════════════════════════════════════════════════
6. A secondmate provisions a home; it refuses the dispatch flags outright
════════════════════════════════════════════════════════════════════════

$ fm-spawn.sh sub-a <home> --secondmate --reason-code SYNTHESIS
error: FM_SPAWN_REASON_CODE_REFUSED: --reason-code applies to ship and scout dispatches; a --secondmate spawn provisions a standing home rather than dispatching a task
exit=1

$ fm-spawn.sh sub-b <home> --secondmate --capability-floor F-GEN
error: FM_SPAWN_REASON_CODE_REFUSED: --capability-floor applies to ship and scout dispatches; a --secondmate spawn matches no dispatch rule
exit=1

════════════════════════════════════════════════════════════════════════
7. Promotion recomputes the derived policy and keeps the recorded reason
════════════════════════════════════════════════════════════════════════

--- state/scout-task.meta BEFORE promotion ---
  kind=scout
  reason_code=MULTIPLE_PLAUSIBLE_ROOT_CAUSES
  escalation_policy=report-only

$ fm-promote.sh scout-task --mode no-mistakes --yolo on
promoted scout-task to ship mode=no-mistakes yolo=on (teardown protection restored)
next: FM_HOME=/tmp/fm-cfvc08-demo.H5pDkm/home bin/fm-send.sh fm-scout-task '<ship instructions for mode=no-mistakes: review scratch state with git status and git log; reset to a clean default-branch base; carry over only intended fix changes; create branch fm/scout-task; implement; report done>'
exit=0

--- state/scout-task.meta AFTER promotion ---
  reason_code=MULTIPLE_PLAUSIBLE_ROOT_CAUSES
  kind=ship
  mode=no-mistakes
  yolo=on
  escalation_policy=firstmate-routine-gates

════════════════════════════════════════════════════════════════════════
8. The whole fleet record, answerable from the state directory alone
════════════════════════════════════════════════════════════════════════

$ for m in state/*.meta; do ...  # every dispatch this home made

  TASK         KIND   REASONING          REASON_CODE                    FLOOR        ESCALATION
  gap-ok       ship   no                 TOOLING_GAP                    F-GEN        captain-approves-gates
  impl-task    ship   yes                NL_RULE_CLASSIFICATION         F-IMPL-MED   captain-approves-gates
  local-task   ship   yes                UNFAMILIAR_CODE                F-GEN        captain-approves-local-merge
  scout-task   ship   yes                MULTIPLE_PLAUSIBLE_ROOT_CAUSES F-GEN        firstmate-routine-gates
  yolo-task    ship   yes                NL_RULE_CLASSIFICATION         F-GEN        firstmate-routine-gates
Evidence: Reproducible demo script that generated the transcript
#!/usr/bin/env bash
# End-to-end demonstration of CFVC-08: every agent dispatch records why an agent
# turn was necessary. Drives the REAL bin/fm-spawn.sh / bin/fm-promote.sh in a
# throwaway firstmate home (fake tmux + real git worktree) exactly as firstmate
# would from the command line, and shows the resulting state/<id>.meta records.
set -u

ROOT_REPO=$1
EV=$2
. "$ROOT_REPO/tests/lib.sh"

SPAWN="$ROOT_REPO/bin/fm-spawn.sh"
PROMOTE="$ROOT_REPO/bin/fm-promote.sh"
TMP=$(fm_test_tmproot fm-cfvc08-demo)

HOME_DIR="$TMP/home"
PROJ="$TMP/project"
WT="$TMP/wt"
FAKEBIN=$(fm_fakebin "$TMP/fake")
mkdir -p "$HOME_DIR"/{data,state,config,projects}

cat > "$FAKEBIN/tmux" <<'SH'
#!/bin/sh
case "$1" in
  display-message) printf '%s\n' "$FM_FAKE_PANE_PATH" ;;
  new-window|new-session) printf 'fm-fake:1\n' ;;
  list-panes|list-windows) printf 'fm-fake:1\n' ;;
  *) : ;;
esac
exit 0
SH
chmod +x "$FAKEBIN/tmux"
fm_fake_exit0 "$FAKEBIN" treehouse
fm_git_worktree "$PROJ" "$WT" wt-cfvc08
touch "$HOME_DIR/state/.last-watcher-beat"

# This home's routing config: the ONLY source of the capability-floor vocabulary.
cat > "$HOME_DIR/config/crew-dispatch.json" <<'JSON'
{
  "rules": [
    { "when": "implementation", "use": { "harness": "codex" }, "route": "R3-MED", "floor": "F-IMPL-MED" }
  ],
  "default": { "harness": "codex", "route": "R2-GEN", "floor": "F-GEN" },
  "_floors": { "F-UTIL": {}, "F-IMPL-MED": {}, "F-GEN": {} }
}
JSON

# This home's backlog: one OPEN repair item, one already-closed one.
cat > "$HOME_DIR/data/backlog.md" <<'MD'
# Backlog

## In flight
- [ ] fleet-view-exits-nonzero - The fleet view reader exits 1 (repo: firstmate) (kind: ship)

## Done
- [x] already-repaired-reader - A reader that was already fixed (repo: firstmate) (kind: ship)
MD

brief() {
  mkdir -p "$HOME_DIR/data/$1"
  printf 'You are a crewmate.\n\n# Definition of done\nShip it.\n' > "$HOME_DIR/data/$1/brief.md"
}

# A real dispatch: fake terminal, real worktree, real meta on disk.
spawn() {
  FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" \
    FM_STATE_OVERRIDE="$HOME_DIR/state" FM_DATA_OVERRIDE="$HOME_DIR/data" \
    FM_PROJECTS_OVERRIDE="$HOME_DIR/projects" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
    FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_FAKE_PANE_PATH="$WT" PATH="$FAKEBIN:$PATH" \
    "$SPAWN" "$@" 2>&1
}

hr() { printf '\n════════════════════════════════════════════════════════════════════════\n%s\n════════════════════════════════════════════════════════════════════════\n' "$1"; }
cmd() { printf '\n$ %s\n' "$*"; }
meta() {
  printf '\n--- state/%s.meta (the record on disk) ---\n' "$1"
  grep -E '^(kind|mode|yolo|reasoning_required|reason_code|capability_floor|escalation_policy|tooling_gap_item)=' \
    "$HOME_DIR/state/$1.meta" | sed 's/^/  /'
}

hr "1. A ship dispatch records all four justification fields"
brief impl-task
cmd "fm-spawn.sh impl-task <project> codex --mode no-mistakes --yolo off \\
      --reason-code NL_RULE_CLASSIFICATION --capability-floor F-IMPL-MED"
spawn impl-task "$PROJ" codex --mode no-mistakes --yolo off \
  --reason-code NL_RULE_CLASSIFICATION --capability-floor F-IMPL-MED >/dev/null
printf 'exit=%s\n' "$?"
meta impl-task

hr "2. The derived fields follow their sources, never a caller declaration"
brief yolo-task
cmd "... --mode no-mistakes --yolo on --reason-code NL_RULE_CLASSIFICATION   (no --capability-floor)"
spawn yolo-task "$PROJ" codex --mode no-mistakes --yolo on \
  --reason-code NL_RULE_CLASSIFICATION >/dev/null
printf 'exit=%s   # same reason code, yolo flips the policy; omitted floor falls back to the config default route\n' "$?"
meta yolo-task

brief local-task
cmd "... --mode local-only --yolo off --reason-code UNFAMILIAR_CODE"
spawn local-task "$PROJ" codex --mode local-only --yolo off \
  --reason-code UNFAMILIAR_CODE >/dev/null
printf 'exit=%s   # a local-only ship never reaches a PR merge gate\n' "$?"
meta local-task

brief scout-task
cmd "... --scout --reason-code MULTIPLE_PLAUSIBLE_ROOT_CAUSES"
spawn scout-task "$PROJ" codex --scout --reason-code MULTIPLE_PLAUSIBLE_ROOT_CAUSES >/dev/null
printf 'exit=%s   # a scout holds no merge authority at all\n' "$?"
meta scout-task

hr "3. The enum is closed: an out-of-enum reason code is refused with a stable token"
brief bad-code
cmd "... --reason-code HARD_PROBLEM"
spawn bad-code "$PROJ" codex --mode no-mistakes --yolo off --reason-code HARD_PROBLEM
printf 'exit=%s\n' "$?"
printf 'record written? %s\n' "$([ -e "$HOME_DIR/state/bad-code.meta" ] && echo 'YES (bad)' || echo 'no - nothing was dispatched')"

brief no-code
cmd "... (no --reason-code at all)"
spawn no-code "$PROJ" codex --mode no-mistakes --yolo off
printf 'exit=%s\n' "$?"
printf 'record written? %s\n' "$([ -e "$HOME_DIR/state/no-code.meta" ] && echo 'YES (bad)' || echo 'no - nothing was dispatched')"

hr "4. TOOLING_GAP must file work, and is never counted as justified reasoning"
brief gap-unfiled
cmd "... --reason-code TOOLING_GAP --tooling-gap-item no-such-repair   (never filed)"
spawn gap-unfiled "$PROJ" codex --mode no-mistakes --yolo off \
  --reason-code TOOLING_GAP --tooling-gap-item no-such-repair
printf 'exit=%s\n' "$?"

brief gap-closed
cmd "... --reason-code TOOLING_GAP --tooling-gap-item already-repaired-reader   (filed, but CLOSED)"
spawn gap-closed "$PROJ" codex --mode no-mistakes --yolo off \
  --reason-code TOOLING_GAP --tooling-gap-item already-repaired-reader
printf 'exit=%s\n' "$?"

cmd "grep -n 'fleet-view-exits-nonzero' data/backlog.md   # the repair that IS filed and open"
grep -n 'fleet-view-exits-nonzero' "$HOME_DIR/data/backlog.md" | sed 's/^/  /'

brief gap-ok
cmd "... --reason-code TOOLING_GAP --tooling-gap-item fleet-view-exits-nonzero"
spawn gap-ok "$PROJ" codex --mode no-mistakes --yolo off \
  --reason-code TOOLING_GAP --tooling-gap-item fleet-view-exits-nonzero >/dev/null
printf 'exit=%s\n' "$?"
meta gap-ok

hr "5. A capability floor the routing config never defines is refused"
brief bad-floor
cmd "... --reason-code SYNTHESIS --capability-floor F-INVENTED"
spawn bad-floor "$PROJ" codex --mode no-mistakes --yolo off \
  --reason-code SYNTHESIS --capability-floor F-INVENTED
printf 'exit=%s\n' "$?"

hr "6. A secondmate provisions a home; it refuses the dispatch flags outright"
mkdir -p "$TMP/sub-home/.fm-secondmate-home"
cmd "fm-spawn.sh sub-a <home> --secondmate --reason-code SYNTHESIS"
spawn sub-a "$TMP/sub-home" --secondmate --reason-code SYNTHESIS
printf 'exit=%s\n' "$?"
cmd "fm-spawn.sh sub-b <home> --secondmate --capability-floor F-GEN"
spawn sub-b "$TMP/sub-home" --secondmate --capability-floor F-GEN
printf 'exit=%s\n' "$?"

hr "7. Promotion recomputes the derived policy and keeps the recorded reason"
printf '\n--- state/scout-task.meta BEFORE promotion ---\n'
grep -E '^(kind|reason_code|escalation_policy)=' "$HOME_DIR/state/scout-task.meta" | sed 's/^/  /'
cmd "fm-promote.sh scout-task --mode no-mistakes --yolo on"
FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" \
  "$PROMOTE" scout-task --mode no-mistakes --yolo on 2>&1
printf 'exit=%s\n' "$?"
printf '\n--- state/scout-task.meta AFTER promotion ---\n'
grep -E '^(kind|mode|yolo|reason_code|escalation_policy)=' "$HOME_DIR/state/scout-task.meta" | sed 's/^/  /'

hr "8. The whole fleet record, answerable from the state directory alone"
printf '\n$ for m in state/*.meta; do ...  # every dispatch this home made\n\n'
printf '  %-12s %-6s %-18s %-30s %-12s %s\n' TASK KIND REASONING REASON_CODE FLOOR ESCALATION
for m in "$HOME_DIR"/state/*.meta; do
  id=$(basename "$m" .meta)
  k=$(sed -n 's/^kind=//p' "$m")
  r=$(sed -n 's/^reasoning_required=//p' "$m"); r=${r:-unknown}
  c=$(sed -n 's/^reason_code=//p' "$m"); c=${c:-unknown}
  f=$(sed -n 's/^capability_floor=//p' "$m"); f=${f:-unknown}
  e=$(sed -n 's/^escalation_policy=//p' "$m"); e=${e:-unknown}
  printf '  %-12s %-6s %-18s %-30s %-12s %s\n' "$id" "$k" "$r" "$c" "$f" "$e"
done
printf '\n'
Evidence: Red-capability proof: mutating the implementation fails the new tests

### CONTROL A - fm_reasoning_required_for mutated so TOOLING_GAP yields 'yes' ok - fm-spawn: the reason code is required and its vocabulary is closed ok - fm-spawn: every code the closed enum publishes is accepted and recorded ok - fm-spawn: a TOOLING_GAP dispatch is only recordable alongside open filed work ok - fm-spawn: a recorded capability floor matches the dispatch config that defines it ok - fm-spawn: every task-dispatch record carries all four fields, derived from their sources not ok - TOOLING_GAP was recorded as reasoning the turn actually required test-file exit=1 ### CONTROL B - fm_backlog_item_open loosened to a plain substring grep ok - fm-spawn: the reason code is required and its vocabulary is closed ok - fm-spawn: every code the closed enum publishes is accepted and recorded not ok - an item that is already closed: refusal did not carry its stable token (missing: 'FM_SPAWN_TOOLING_GAP_ITEM_UNFILED') (bin/fm-reasoning-lib.sh restored via git checkout after each control; worktree clean)

### NEGATIVE CONTROL A - mutate fm_reasoning_required_for so TOOLING_GAP yields 'yes'
### (i.e. a broken reader would be counted as justified reasoning)
ok - fm-spawn: the reason code is required and its vocabulary is closed
ok - fm-spawn: every code the closed enum publishes is accepted and recorded
ok - fm-spawn: a TOOLING_GAP dispatch is only recordable alongside open filed work
ok - fm-spawn: a recorded capability floor matches the dispatch config that defines it
ok - fm-spawn: every task-dispatch record carries all four fields, derived from their sources
not ok - TOOLING_GAP was recorded as reasoning the turn actually required
test-file exit=1
Evidence: Pre-existing failures proven against the unmodified base commit

target 5649c06 : 57 ok / 3 not ok baseline 345de4e : 57 ok / 3 not ok diff of every ok/not-ok line, target vs baseline: (no differences - identical outcomes) Failing cases and their environment causes: not ok - old fm-teardown.sh (scout, report present) should succeed fm-decision-hold: compatible tasks-axi is required not ok - Orca scout teardown should succeed once report exists fm-decision-hold: compatible tasks-axi is required not ok - turn_end drive failed: node:internal/modules/esm/get_format:219 throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath); Environment: tasks-axi installed 0.2.3, floor required by bin/fm-tasks-axi-lib.sh is 0.2.4

Pre-existing failure verification: same three touched test files, target vs unmodified base.

target   5649c06 : 57 ok / 3 not ok
baseline 345de4e : 57 ok / 3 not ok

diff of every ok/not-ok line, target vs baseline:
  (no differences - identical outcomes)

Failing cases and their environment causes:
not ok - old fm-teardown.sh (scout, report present) should succeed
fm-decision-hold: compatible tasks-axi is required
not ok - Orca scout teardown should succeed once report exists
fm-decision-hold: compatible tasks-axi is required
not ok - turn_end drive failed: node:internal/modules/esm/get_format:219
  throw new ERR_UNKNOWN_FILE_EXTENSION(ext, filepath);

Environment: tasks-axi installed 0.2.3, floor required by bin/fm-tasks-axi-lib.sh is 0.2.4
Evidence: Additive migration: a pre-change meta gains the derived policy without a manufactured reason

--- state/legacy-scout.meta BEFORE (written by the old fm-spawn.sh) --- window=fm-legacy-scout kind=scout $ fm-promote.sh legacy-scout --mode direct-PR --yolo off exit=0 --- state/legacy-scout.meta AFTER --- kind=ship mode=direct-PR yolo=off escalation_policy=captain-approves-gates reason_code present? no - reads as unknown, never as justified reasoning reasoning_required present? no - reads as unknown

### MIGRATION: a pre-change meta carries none of the new fields

--- state/legacy-scout.meta BEFORE (written by the old fm-spawn.sh) ---
  window=fm-legacy-scout
  worktree=/tmp/whatever
  harness=codex
  kind=scout
  tasktmp=/tmp/whatever/tmp

$ fm-promote.sh legacy-scout --mode direct-PR --yolo off
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  ●  WATCHER DOWN - SUPERVISION IS OFF
  ●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
  ●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
  ●  This is a supervision warning only; the guarded operation WILL still run.
  ●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
  ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  promoted legacy-scout to ship mode=direct-PR yolo=off (teardown protection restored)
  next: FM_HOME=/tmp/fm-legacy-meta.7DwzD2/home bin/fm-send.sh fm-legacy-scout '<ship instructions for mode=direct-PR: review scratch state with git status and git log; reset to a clean default-branch base; carry over only intended fix changes; create branch fm/legacy-scout; implement; report done>'
exit=0

--- state/legacy-scout.meta AFTER (additive: policy added, no reason invented) ---
  window=fm-legacy-scout
  worktree=/tmp/whatever
  harness=codex
  tasktmp=/tmp/whatever/tmp
  kind=ship
  mode=direct-PR
  yolo=off
  escalation_policy=captain-approves-gates

reason_code present?          no - reads as unknown, never as justified reasoning
reasoning_required present?   no - reads as unknown
Evidence: New suite result and adjacent touched-test results

tests/fm-reasoning-required.test.sh (10/10): ok - fm-spawn: the reason code is required and its vocabulary is closed ok - fm-spawn: every code the closed enum publishes is accepted and recorded ok - fm-spawn: a TOOLING_GAP dispatch is only recordable alongside open filed work ok - fm-spawn: a recorded capability floor matches the dispatch config that defines it ok - fm-spawn: every task-dispatch record carries all four fields, derived from their sources ok - fm-spawn: TOOLING_GAP is counted separately and never as justified reasoning ok - fm-spawn: an array-form default route resolves its floor instead of refusing every dispatch ok - fm-spawn: a justification value can never forge a second line of the record ok - fm-spawn: secondmate provisioning is out of scope for the justification record ok - fm-promote: promotion recomputes the derived escalation policy adjacent touched files: FM_TEST_SUMMARY total=7 failed=0 secondmate + docs: FM_TEST_SUMMARY total=2 failed=0

FM_TEST_BEGIN 2026-08-06T18:03:58Z tests/fm-spawn-dispatch-profile.test.sh family=backend-dispatch expected_gate_skip=none
ok - no --model/--effort records defaults and types the claude launch instructions
ok - relative home overrides ignore CDPATH and become absolute before spawn launch construction
ok - FM_HOME defaults resolve relative paths and preserve absolute spellings
ok - absolute override spellings are preserved in spawn launch paths
ok - unresolvable relative spawn overrides fail with named diagnostics
ok - active crew-dispatch profile requires an explicit harness for ship spawns
ok - active crew-dispatch profile requires an explicit harness for scout spawns
ok - active crew-dispatch profile allows an explicit resolved harness
ok - active crew-dispatch profile allows the legacy positional harness form
ok - active crew-dispatch profile allows the raw launch-command escape hatch
ok - claude receives --model and --effort profile flags
ok - codex receives --model and model_reasoning_effort profile flags
ok - codex omits unsupported max effort instead of passing a bad config value
ok - grok receives --model and --reasoning-effort profile flags
ok - grok omits unsupported max reasoning effort
ok - grok omits unsupported xhigh reasoning effort
ok - opencode receives --model and omits the unsupported effort axis
ok - pi receives --model and --thinking max profile flags
ok - pi-signed shares Pi launch semantics while preserving its configured and recorded identity
ok - pi-signed refuses safely and actionably when the selected executable is unavailable
ok - pi-signed is a distinct persistent secondmate runtime with shared Pi supervision semantics
ok - batch dispatch forwards shared --harness, --model, and --effort to every pair
ok - claude forwards firstmate's CLAUDE_CONFIG_DIR so the crewmate uses the same credential store
ok - claude omits the config-dir prefix when firstmate runs with the single-store default
ok - non-claude harnesses do not receive the claude CLAUDE_CONFIG_DIR prefix
ok - active crew-dispatch profile does not block secondmate launches
# all fm-spawn-dispatch-profile tests passed
FM_TEST_END 2026-08-06T18:04:42Z tests/fm-spawn-dispatch-profile.test.sh exit=0 duration_ms=44233 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:04:42Z tests/fm-task-delivery.test.sh family=pure-contract-unit expected_gate_skip=none
ok - fm-spawn: a ship spawn requires a valid explicit mode and yolo before anything is created
ok - fm-spawn: scout and secondmate spawns refuse ship delivery flags
ok - fm-spawn: the brief's recorded mode and the spawn's explicit mode must agree
ok - fm-spawn: a rigor downgrade against the registered posture is announced, never blocked
ok - fm-spawn: a scout spawn resolves no delivery posture from the registry
ok - fm-promote: promotion requires the delivery contract and records it exactly once
ok - fm-project-mode: the conditional policy is accepted, mapped for mechanical callers, and readable raw
# all fm-task-delivery tests passed
FM_TEST_END 2026-08-06T18:04:44Z tests/fm-task-delivery.test.sh exit=0 duration_ms=1841 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:04:44Z tests/fm-trace-context-spawn.test.sh family=backend-dispatch expected_gate_skip=none
ok - enabled: one resolved carrier is recorded in meta and the identical TRACEPARENT is exported before launch
ok - disabled: neither traceparent= in meta nor a TRACEPARENT export is produced
ok - failed TRACEPARENT delivery omits metadata while the source task still launches
ok - uncleared TRACEPARENT input stops before the launch command is appended
ok - failed traceparent metadata append removes the carrier from the launched task
ok - duplicate secondmate preflight leaves trace-context unchanged
ok - relaunch reuses the recorded carrier verbatim for both the meta record and the injected export
ok - session start freezes the env override and later config or environment edits do not alter spawns
ok - two-level: env-on/file-absent keeps the nested worker enabled, rooting its own per-task trace
ok - two-level: env-off/file-present keeps the nested worker disabled even though the config file was copied into the secondmate home
ok - two unrelated routed tasks through one persistent Secondmate root distinct traces, adopt nothing from its environment, and keep per-task identity across relaunch
ok - secondmate carrier and FM_TRACE_CONTEXT snapshot always agree, both derived from one frozen decision (file-decided path)
# all fm-trace-context-spawn tests passed
FM_TEST_END 2026-08-06T18:05:19Z tests/fm-trace-context-spawn.test.sh exit=0 duration_ms=34435 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:05:19Z tests/fm-spawn-batch.test.sh family=backend-dispatch expected_gate_skip=none
ok - batch dispatch re-execs and reports every id=repo pair
ok - batch detection: single pair batches, non-pair rejected, single-task and slash-id stay single
ok - batch dispatch requires the shared ship delivery contract before any pair runs
ok - scout batch refuses ship delivery flags instead of ignoring them
ok - projects/ paths are scoped through the firstmate home for single-task spawn
FM_TEST_END 2026-08-06T18:05:20Z tests/fm-spawn-batch.test.sh exit=0 duration_ms=960 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:05:20Z tests/fm-gate-refuse.test.sh family=session-bootstrap expected_gate_skip=none
ok - fm-gate-refuse-lib: refuses when NO_MISTAKES_GATE is set
ok - fm-gate-refuse-lib: refuses when NO_MISTAKES_GATE is set empty
ok - fm-gate-refuse-lib: refuses from a gate worktree via git-common-dir (marker unset)
ok - fm-gate-refuse-lib: no-op for a normal session (neither signal, set -eu clean)
ok - fm-spawn: refuses on marker and gate-worktree backstop; a normal crew spawn is unaffected
ok - fm-send: refuses on marker and gate-worktree backstop; a normal steer is unaffected
ok - fm-teardown: refuses on marker and gate-worktree backstop; a normal teardown is unaffected
FM_TEST_END 2026-08-06T18:05:24Z tests/fm-gate-refuse.test.sh exit=0 duration_ms=3955 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:05:24Z tests/fm-tangle-guard.test.sh family=session-bootstrap expected_gate_skip=none
ok - fm_primary_tangle_branch: feature branch alarms; default/detached/non-git stay silent
ok - fm-guard: bordered tangle banner fires only for a feature branch and suppresses repair commands in read-only mode
ok - fm-bootstrap: TANGLE problem line fires only for a feature branch and suppresses repair commands in detect-only mode
ok - fm-brief: ship brief asserts worktree isolation before the branch step
ok - fm-spawn: aborts unless the resolved worktree is a genuine, isolated worktree
ok - fm-spawn: appends windows by session-colon, pins the name, and targets the window id
FM_TEST_END 2026-08-06T18:05:36Z tests/fm-tangle-guard.test.sh exit=0 duration_ms=12695 gate_skip=false
FM_TEST_BEGIN 2026-08-06T18:05:36Z tests/fm-spawn-worktree-settle.test.sh family=backend-dispatch expected_gate_skip=none
ok - a single transient stale pane_current_path read is not accepted as the worktree
ok - an already-settled pane confirms via the existing inter-poll sleep, not an extra full cycle
# all fm-spawn-worktree-settle tests passed
FM_TEST_END 2026-08-06T18:05:41Z tests/fm-spawn-worktree-settle.test.sh exit=0 duration_ms=4808 gate_skip=false
FM_TEST_SUMMARY total=7 failed=0 skipped_gate=0 duration_ms=103134
FM_TEST_SUMMARY_FAMILY family=backend-dispatch count=4 duration_ms=84436 failed=0
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=1841 failed=0
FM_TEST_SUMMARY_FAMILY family=session-bootstrap count=2 duration_ms=16650 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-spawn-dispatch-profile.test.sh duration_ms=44233
FM_TEST_SLOWEST rank=2 script=tests/fm-trace-context-spawn.test.sh duration_ms=34435
FM_TEST_SLOWEST rank=3 script=tests/fm-tangle-guard.test.sh duration_ms=12695
FM_TEST_SLOWEST rank=4 script=tests/fm-spawn-worktree-settle.test.sh duration_ms=4808
FM_TEST_SLOWEST rank=5 script=tests/fm-gate-refuse.test.sh duration_ms=3955
FM_TEST_SLOWEST rank=6 script=tests/fm-task-delivery.test.sh duration_ms=1841
FM_TEST_SLOWEST rank=7 script=tests/fm-spawn-batch.test.sh duration_ms=960
- Evidence: fm-spawn.sh --help: the new flags as an end user reads them (local file: /tmp/no-mistakes-evidence/01KZC20GY968DMHR4GE2R9XSGE/fm-spawn-help.txt) - Outcome: ⚠️ 1 warning across 1 run (10m58s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 6 issues found → auto-fixed ✅
  • 🚨 bin/fm-reasoning-lib.sh:121 - fm_capability_floor_vocabulary reads .default.floor, which is a jq error when default is the array form that docs/configuration.md:269 documents as valid and that docs/examples/crew-dispatch.json ships. jq exits 5, the || return 2 fires, and bin/fm-spawn.sh:449-452 turns that into FM_SPAWN_CAPABILITY_FLOOR_UNVERIFIABLE and exit 1 — so EVERY ship and scout spawn is hard-refused in any home whose crew-dispatch config uses an array default, even when no --capability-floor is passed. Verified: fm_capability_floor_vocabulary on docs/examples/crew-dispatch.json returns 2 (jq: 'Cannot index array with string "floor"'). fm_capability_floor_default (line 133) has the identical defect. No test covers it: every spawn-facing fixture (tests/fm-reasoning-required.test.sh:40, tests/fm-spawn-dispatch-profile.test.sh:71) uses the object form. bin/fm-bootstrap.sh:919-926 already defines the correct profiles() object-or-array normalization; reuse that shape instead of indexing .default directly.
  • 🚨 bin/fm-reasoning-lib.sh:144 - fm_backlog_item_open interpolates the caller-supplied item id into a grep -qE pattern, but the sed escape class [][\.*^$/&amp;] omits the ERE metacharacters (, ), +, ?, {, }, |. Verified: fm_backlog_item_open &lt;data&gt; &#39;|&#39; and &#39;x|&#39; both return 0 against a normal backlog.md, because the trailing alternation makes the pattern match any line. That means --reason-code TOOLING_GAP --tooling-gap-item &#39;|&#39; clears the certification the intent calls "the single failure mode that would make this increment worse than nothing", with no open work item filed at all. (( instead makes grep exit 2, which the caller reads as not-open, so behavior is also inconsistent across metacharacters.) Match the id literally instead — e.g. strip the - [ ] prefix with awk/sed and compare the first field with =, or use grep -F against a normalized line.
  • ⚠️ bin/fm-reasoning-lib.sh:119 - The floor vocabulary is read from _floors, rules[].floor and default.floor, but none of those keys exist in the crew-dispatch schema. docs/configuration.md:241-282 declares itself "the single owner of the canonical schema and its per-field semantics" (line 248) and AGENTS.md section 4 names it as the schema owner; neither it nor docs/examples/crew-dispatch.json was updated, and the keys appear only in the new test fixture (tests/fm-reasoning-required.test.sh:38-41). Consequence on any real home: the vocabulary comes back empty, capability_floor always records unconfigured (conflating "no dispatch config" with "config that defines no floors", contrary to the constant's own comment at line 74), and --capability-floor is always refused. So the intent's "capability_floor is taken verbatim from config/crew-dispatch.json and refused when the config does not define it" holds only against a fixture, and the one-owner rule the intent claims to follow is not satisfied. Either document floor/_floors in docs/configuration.md's crew-dispatch section plus the example file, or derive the floor from a source that already exists.
  • ⚠️ bin/fm-spawn.sh:443 - Neither --capability-floor nor --tooling-gap-item is checked for embedded newlines, and both checks accept multi-line values because grep treats a newline-bearing pattern argument as several patterns. Verified: printf &#39;%s\n&#39; &#34;$VOCAB&#34; | grep -qxF -- $&#39;F-GEN\nkind=secondmate&#39; succeeds, and fm_backlog_item_open accepts $'fleet-view-exits-nonzero\nkind=secondmate'. The value then reaches echo &#34;capability_floor=$CAPABILITY_FLOOR&#34; (line 2338) and echo &#34;tooling_gap_item=$TOOLING_GAP_ITEM&#34; (line 2340) verbatim, writing extra forged lines such as kind=secondmate or worktree=... into state/<id>.meta — which teardown, supervision and backend resolution all read. Reject values containing a newline (or any whitespace) alongside the existing non-empty checks at lines 328-329.
  • ℹ️ bin/fm-reasoning-lib.sh:95 - fm_escalation_policy_for is documented as &lt;kind&gt; &lt;mode&gt; &lt;yolo&gt; and both call sites pass mode (bin/fm-spawn.sh:2291, bin/fm-promote.sh:86), but the body binds only $1 and $3$2 is never read. The derivation is therefore kind+yolo only, so a local-only yolo=off ship records the same captain-approves-gates as a no-mistakes yolo=off ship even though it never reaches a merge gate. That contradicts the header comment and AGENTS.md's description of the field as derived from "kind plus the delivery contract" (mode is half of that contract). Either fold mode into the derivation or drop the parameter so the signature stops implying it matters.
  • ℹ️ AGENTS.md:95 - The meta field list points readers to "(bin/fm-reasoning-lib.sh, section 4)" for the agent-justification fields, but the paragraph that defines --reason-code and TOOLING_GAP is in section 7 (AGENTS.md:280); section 4 "Harness and runtime dispatch" never mentions them. Change the cross-reference to section 7.

🔧 Fix: fix array-form floor read, literal gap-item match, meta injection
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 warning
  • ⚠️ tests/fm-backend.test.sh - Three test files touched by this change fail in this environment for reasons unrelated to it, and neither is fixable inside the worktree (fixing them would mean upgrading a system-installed binary and the Node toolchain). tests/fm-backend.test.sh and tests/fm-backend-orca.test.sh fail their scout-teardown decision-gate cases with fm-decision-hold: compatible tasks-axi is required — this host has tasks-axi 0.2.3 against the 0.2.4 floor in bin/fm-tasks-axi-lib.sh:22 — and tests/fm-busy-adapter-wiring.test.sh fails its Pi extension case with ERR_UNKNOWN_FILE_EXTENSION for a .ts module. I verified these are pre-existing rather than caused by the change: running the same three files against the base tree at 345de4e produces byte-identical ok/not-ok lines (57 ok / 3 not ok in both runs). Remote CI, which owns broad regression, is the right place to confirm these are green on a properly provisioned runner.
  • bash bin/fm-test-run.sh tests/fm-reasoning-required.test.sh — all 10 cases pass (closed enum, every published code recorded, TOOLING_GAP open-work certification incl. regex-injection ids, floor-matches-config incl. unreadable-config fail-closed, all four fields on task dispatches, TOOLING_GAP counted separately, array-form default floor + shipped example config, meta-line forgery refusal, secondmate exclusion, promotion recompute)
  • bash bin/fm-test-run.sh --check-coverage and --list --lane portable-serial — confirmed the new suite is registered and selected by a CI lane rather than orphaned
  • bash bin/fm-test-run.sh tests/fm-spawn-dispatch-profile.test.sh tests/fm-task-delivery.test.sh tests/fm-trace-context-spawn.test.sh tests/fm-spawn-batch.test.sh tests/fm-gate-refuse.test.sh tests/fm-tangle-guard.test.sh tests/fm-spawn-worktree-settle.test.sh — 7/7 pass, covering the mechanical --reason-code call-site additions and the batch-spawn flag forwarding
  • bash bin/fm-test-run.sh tests/fm-secondmate-harness.test.sh tests/fm-documentation-audiences.test.sh — both pass, confirming the secondmate provisioning path still spawns under the deliberate exclusion and the AGENTS.md edit is well-formed
  • Manual end-to-end CLI demonstration driving the real bin/fm-spawn.sh and bin/fm-promote.sh against a real git worktree and fake tmux, reading back actual state/<id>.meta records for ship / yolo=on ship / local-only ship / scout / TOOLING_GAP dispatches plus every refusal path (/tmp/no-mistakes-evidence/01KZC20GY968DMHR4GE2R9XSGE/demo-dispatch-justification.sh)
  • Negative control A: mutated fm_reasoning_required_for in bin/fm-reasoning-lib.sh so TOOLING_GAP yields yes — suite went red on the TOOLING_GAP separation case; file restored via git checkout --
  • Negative control B: loosened fm_backlog_item_open to a plain substring grep — suite went red on the already-closed backlog-item row; file restored via git checkout --
  • Migration check: ran bin/fm-promote.sh against a hand-written pre-change meta carrying none of the new fields — promotion succeeds, adds the derived escalation_policy, and invents no reason_code (absent fields stay absent)
  • Pre-existing-failure verification: git checkout 345de4e -- bin tests docs AGENTS.md then re-ran tests/fm-backend.test.sh tests/fm-backend-orca.test.sh tests/fm-busy-adapter-wiring.test.sh; identical 57 ok / 3 not-ok lines to the target run; tree restored to a clean 5649c06
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

… chokepoint

CFVC-08. bin/fm-spawn.sh is the last gate before an agent turn exists and it
already writes state/<id>.meta, so it is where the justification record belongs.
Every ship and scout dispatch now records four fields:

  reasoning_required  derived from the reason code
  reason_code         a closed nine-token enum; an unknown value is refused
  capability_floor    verbatim from config/crew-dispatch.json
  escalation_policy   derived from kind plus the delivery contract

bin/fm-reasoning-lib.sh is the single owner of the enum, the derivations and the
stable refusal tokens. It records; it does not enforce - no dispatch is blocked
for reasoning too little. The vocabulary is closed because a free-text reason
cannot be counted, and the two derived fields are never caller-supplied so they
cannot disagree with the record they summarize.

TOOLING_GAP is the one code that is NOT a reasoning code. It names a turn taken
only because a deterministic reader is broken or absent. It records
reasoning_required=no so it can never be counted as justified reasoning, and it
requires --tooling-gap-item naming a work item that is currently OPEN in this
home's data/backlog.md. Without that check the code would launder every unfixed
tool into a permanent "necessary agent turn" - the single failure mode that
would make this record worse than no record at all.

SCOPE, and a documented replacement of completion criterion (a). The spec says
every new spawn record carries all four fields. A --secondmate spawn is excluded
and refuses all three flags: it provisions a standing home rather than
dispatching a task - AGENTS.md section 10 keeps a secondmate out of the backlog
for the same reason - and Lane B derived the enum entirely from task
invocations, so demanding one of its codes for a provisioning action would
manufacture exactly the rubber-stamp answer the enum exists to prevent. Absent
fields read as unknown and never as justified, so a secondmate record cannot be
miscounted either way. The criterion is replaced by a stronger tested pair:
every TASK dispatch carries all four fields, AND a secondmate spawn that passes
one is refused with a stable token rather than silently defaulted.

bin/fm-promote.sh recomputes escalation_policy, because promotion changes the
delivery contract that field is derived from. Leaving it would keep a scout's
report-only posture on a task that can now reach a merge gate.

RETIREMENT. The record was ABSENT, so no mechanism is replaced in code. What
retires is the untracked category of agent turns taken because a reader is
broken: before this, such a turn was indistinguishable in the record from
justified reasoning, and TOOLING_GAP plus its refusing filing check is what ends
that. No transitional second path is left alive - --reason-code is the only way
to record a reason, and it is required rather than optional.

CERTIFICATION. tests/fm-reasoning-required.test.sh, eight cases. Every case was
witnessed RED against the pre-change bin/fm-spawn.sh and bin/fm-promote.sh
before being accepted green. Three further targeted negative controls were run
and witnessed red: loosening the open-item match (the already-closed and prefix
rows go red), making reasoning_required always yes (the TOOLING_GAP row goes
red), and degrading an unreadable dispatch config to "unconfigured" (the
unverifiable-floor row goes red). The published-codes case asserts the recorded
value positively rather than the absence of a refusal, because the
absence-only version of it was vacuously green against the reverted
implementation and so proved nothing. The lint gate was also shown able to
reject (exit 1 on a deliberate violation) rather than trusted for being quiet.

Existing spawn call sites in 18 test files carry the new required flag.

Three failures remain in the touched set and were each proven PRE-EXISTING on the
unmodified contribution base by a stashed baseline run, not asserted: two scout
teardown decision gates refused because this environment has tasks-axi 0.2.3
against the required 0.2.4 floor, and one Pi extension case fails on a Node ESM
loader error. All three reproduce identically with these changes reverted.
@kunchenguid

Copy link
Copy Markdown
Owner

Automated reminder: thanks for the PR! This branch currently has a merge conflict with the base branch.

When you get a chance, please rebase onto (or merge) the latest base branch, resolve the conflict, and push. After that, checks will re-run and the PR will get looked at again.

Noted for firstmate#1830 at e17ecfe1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants