feat(bin): record why every agent dispatch was necessary at the spawn chokepoint - #1830
Open
sbracewell64 wants to merge 3 commits into
Open
feat(bin): record why every agent dispatch was necessary at the spawn chokepoint#1830sbracewell64 wants to merge 3 commits into
sbracewell64 wants to merge 3 commits into
Conversation
… 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.
…h, meta injection
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.shnow requires--reason-codeon ship and scout dispatches and records four fields into thestate/<id>.metait already writes:reason_code(a closed vocabulary of eight reasoning codes plusTOOLING_GAP), derivedreasoning_requiredandescalation_policy, andcapability_floortaken fromconfig/crew-dispatch.json. Out-of-enum codes, undefined floors, unreadable dispatch configs, and newline-bearing flag values are refused with stable tokens;TOOLING_GAPadditionally requires--tooling-gap-itemnaming a currently-opendata/backlog.mditem, matched literally.--secondmatespawns refuse all three new flags rather than carrying the fields, since they provision a home rather than dispatch a task.bin/fm-reasoning-lib.shis the single owner of the enum, the derivations, the refusal tokens, the floor-vocabulary read (handling both object- and array-formdefaultin crew-dispatch config), and the open-backlog-item check;bin/fm-promote.shrecomputesescalation_policyon promotion because promotion changes the delivery contract it is derived from, while preserving the recordedreason_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.shadds 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, anddocs/examples/crew-dispatch.jsondocument 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 --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-gatesEvidence: Reproducible demo script that generated the transcript
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)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.4Evidence: 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 unknownEvidence: 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/tmp/no-mistakes-evidence/01KZC20GY968DMHR4GE2R9XSGE/fm-spawn-help.txt) - Outcome: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 whendefaultis the array form that docs/configuration.md:269 documents as valid and that docs/examples/crew-dispatch.json ships. jq exits 5, the|| return 2fires, 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_vocabularyon 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 correctprofiles()object-or-array normalization; reuse that shape instead of indexing.defaultdirectly.bin/fm-reasoning-lib.sh:144- fm_backlog_item_open interpolates the caller-supplied item id into agrep -qEpattern, but the sed escape class[][\.*^$/&]omits the ERE metacharacters(,),+,?,{,},|. Verified:fm_backlog_item_open <data> '|'and'x|'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 '|'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 usegrep -Fagainst a normalized line.bin/fm-reasoning-lib.sh:119- The floor vocabulary is read from_floors,rules[].flooranddefault.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_flooralways recordsunconfigured(conflating "no dispatch config" with "config that defines no floors", contrary to the constant's own comment at line 74), and--capability-flooris 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 documentfloor/_floorsin 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 '%s\n' "$VOCAB" | grep -qxF -- $'F-GEN\nkind=secondmate'succeeds, and fm_backlog_item_open accepts $'fleet-view-exits-nonzero\nkind=secondmate'. The value then reachesecho "capability_floor=$CAPABILITY_FLOOR"(line 2338) andecho "tooling_gap_item=$TOOLING_GAP_ITEM"(line 2340) verbatim, writing extra forged lines such askind=secondmateorworktree=...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<kind> <mode> <yolo>and both call sites pass mode (bin/fm-spawn.sh:2291, bin/fm-promote.sh:86), but the body binds only$1and$3—$2is never read. The derivation is therefore kind+yolo only, so alocal-onlyyolo=off ship records the samecaptain-approves-gatesas ano-mistakesyolo=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.
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.shandtests/fm-backend-orca.test.shfail their scout-teardown decision-gate cases withfm-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 — andtests/fm-busy-adapter-wiring.test.shfails its Pi extension case withERR_UNKNOWN_FILE_EXTENSIONfor a.tsmodule. 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-coverageand--list --lane portable-serial— confirmed the new suite is registered and selected by a CI lane rather than orphanedbash 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 forwardingbash 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-formedManual 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: mutatedfm_reasoning_required_forin bin/fm-reasoning-lib.sh so TOOLING_GAP yieldsyes— suite went red on the TOOLING_GAP separation case; file restored viagit checkout --Negative control B: loosenedfm_backlog_item_opento a plain substring grep — suite went red on the already-closed backlog-item row; file restored viagit checkout --Migration check: ranbin/fm-promote.shagainst 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.mdthen re-rantests/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.