Skip to content

feat(bin): add delivery verification, a completeness gate, and grounded briefs - #1725

Closed
tomharper wants to merge 14 commits into
kunchenguid:mainfrom
tomharper:fm/firstmate-fix-consolidation
Closed

tomharper wants to merge 14 commits into
kunchenguid:mainfrom
tomharper:fm/firstmate-fix-consolidation

Conversation

@tomharper

Copy link
Copy Markdown

Intent

Consolidate every outstanding firstmate fix from seven open branches into ONE branch and ONE PR, so the captain's fork rebases onto a moving upstream once instead of seven times. The PR MUST open on tomharper/firstmate with base main. Earlier attempts opened on kunchenguid/firstmate because the local gate was configured fork-contribution style (push to fork, PR against origin=upstream); the managed clone's origin has since been corrected to the fork, and the opened PR's owner must still be read back and verified because that failure mode is silent and has already bitten several PRs.

Four of the seven branches were lineage variants of ONE feature (the Z3 completeness gate) across three SHA lineages, not four features; fm/fm-rebase-upstream even applies the same five gate commits twice on itself. The result is five genuinely distinct fixes:

  1. fm-verify-delivered.sh and its false-clean fix (fm/fm-verify-delivered-false-clean, PR docs: align tmux and harness guidance #3): "inspected nothing" and "search failed" are deliberately their own non-clean exit outcomes so an inspection that examined no files can never report clean.
  2. Agent-maintained crewmate working log at data//log.md (fm/crew-worklog, PR fix(bin): coalesce watcher signals into one wake #4): deliberately NOT the status file, because status appends wake firstmate and must stay sparse while the log never wakes anything.
  3. Timestamped status appends plus reported evidence age (fm/status-timestamps).
  4. The Z3 completeness gate, deduplicated across the four branches.
  5. Per-repo ground truth injected into briefs (feat-cross-repo-grounding, PR chore: initialize no-mistakes gate #1).

DELIBERATE DECISIONS a reviewer reading only the diff would not know. The gate version was chosen by content, not recency: two branches carry byte-identical gate files and a third a near-identical copy, and feat-cross-repo-grounding's 9568dac won because it is the only lineage where invalid facts exit 64 and BLOCK (in the others a typo or corrupted record falls through the engine's error path as a PASS) and the only one whose fm-merge-local.sh caller treats 64 as blocking. Three branches fix the SAME bug (the gate false-blocking teardown of squash-merged work) three different ways; the in-gate fix was taken so every caller benefits, not just teardown, and the local-only landedness derivation was deliberately KEPT because local-only work has no PR to squash-merge so plain git can decide it correctly - that is what lets the gate block an unmerged local-only branch before fm-merge-local.sh runs. For remote-backed ship work the gate defers COMPLETELY to fm-teardown.sh per AGENTS.md hard rule 3. The call-site split from fm/fm-rebase-upstream is intentionally NOT carried because it would add a duplicate invocation changing no outcome and its call sites predate exit 64.

PR #1 was explicitly evaluated: the captain called it out of date, which describes the branch (265 commits behind, failing check) and not the feature, so the ground-truth work is included in its newest form with the test coverage it never had.

TWO DELIBERATE DROPS: bin/pycache/fm-completeness.cpython-313.pyc, a compiled build artifact committed on all four gate branches, and the redundant .gitignore pycache/ addition, because main already carries both patterns. Nothing else from the seven branches is absent.

ADAPTATIONS forced by main's drift, all deliberate: the bootstrap capability line is emitted as a BOOTSTRAP_INFO fact behind FM_BOOTSTRAP_VERBOSE_FACTS=1 matching main's current convention; README toolbelt/env/test-list rows are not carried because main moved those surfaces into docs/; AGENTS.md additions were kept minimal per firstmate-coding-guidelines size discipline.

REVIEW FIXES ALREADY APPLIED AND ACCEPTED on this branch across earlier runs of this same pipeline - do not re-litigate them. The gate's dirty-worktree filter now matches fm-teardown.sh's residue filter including the grok/kimi turn-end markers, so the gate cannot refuse a teardown the guarded script considers clean. The pending-reply dedup was restored from substring back to whole-line semantics via status_line_body under the stamp contract. Both fm-completeness-check.sh invocations forward FM_HOME/FM_STATE_OVERRIDE/FM_DATA_OVERRIDE explicitly like their neighbours. AGENTS.md hard rule 5 was SOFTENED BY EXPLICIT CAPTAIN DECISION to offer bin/fm-verify-delivered.sh as available evidence rather than a mandatory precondition, because that script's only verdict-producing mode is hardcoded to one private downstream project while this repo ships to other users; generalizing the script was explicitly ruled OUT OF SCOPE. fm-procevent-remote-reply.sh's validator now folds incoming lines through status_line_body so stamped and legacy unstamped remote-secondmate replies both validate, keeping the stamp contract's single owner rather than copying its glob. And a malformed rules-file weight now fails open as a rules error instead of exiting 64, keeping exit 64 strictly for invalid FACTS.

HARD CONSTRAINT: all seven source branches must be left exactly as found, never deleted, force-pushed, or rewritten, because they are the only copy of this work and the recovery path. Verified untouched at c98385b, 9568dac, 39c5b05, e3581a5, 8c81c17, 7c8ced1, 469cec8.

A further accepted review round is also already applied and must not be re-litigated: the remote-reply continuity-broken escalation is now stamped through fm-classify-lib.sh's writer so no firstmate-side append is left unstamped; both gate call sites now treat rc 0 as the ONLY proceed case and block on every other rc, so a missing or non-executable gate wrapper can no longer be read as approval (the documented fail-open behaviour is unchanged because the gate itself exits 0 for SAT, for the off-switch, and for fail_open); the blocked-verdict extraction no longer anchors on the dead 'counterexample' key so a blocked claim still names its violated invariant; and the unused 'unpushed' revision walk moved inside the local-only branch that is its only reader, a cost and placement change that alters no resolved value.

PIPELINE CONTEXT: review and test already PASSED at an earlier head in this same pipeline. Two subsequent runs failed for purely ENVIRONMENTAL reasons, never a defect in this branch: one hit a Claude session limit at the document step, and one lost the claude binary from the daemon's PATH mid-run while that binary was being reinstalled. Both have cleared, the agent is verified runnable, and custody was returned through the gate's own recovery path each time.

KNOWN PRE-EXISTING FAILURES, not regressions: fm-teardown, fm-session-start, fm-pi-watch-extension and fm-bearings-snapshot fail identically against a pristine main extract. fm-teardown matters most because this branch adds a gate call to fm-teardown.sh, but it is not the cause: the same assertion fails with FM_COMPLETENESS_GATE=0 and on pristine main where the gate does not exist. They are environment-dependent and out of scope.

What Changed

  • Adds three new capabilities as scripts under bin/: fm-verify-delivered.sh, which checks a task's delivery claims against merged code and treats "inspected nothing" and "search failed" as their own non-clean exits so an inspection that examined no files can never report clean; fm-ground.sh, which resolves per-repo ground truth from data/repos/<key>.md; and an optional Z3-backed completeness gate (fm-completeness-check.sh, fm-completeness.py, fm-completeness.rules.json) that derives task facts and solves them against a rules file, exiting 64 on invalid facts, failing open on rules-file breakage, and deferring entirely to fm-teardown.sh for remote-backed ship work. The gate is wired into fm-teardown.sh and fm-merge-local.sh, both of which now treat rc 0 as the only proceed case and print cause-specific remediation when the gate blocks.
  • Reworks briefs and status reporting: fm-brief.sh scaffolds an agent-maintained working log at data/<id>/log.md (deliberately separate from the status file, which wakes firstmate and must stay sparse) and injects the resolved per-repo ground truth into ship and scout briefs; fm-classify-lib.sh now stamps every status append with a timestamp and owns the stamp contract, with fm-crew-state.sh, fm-fleet-snapshot.sh, fm-fleet-view.sh and fm-secondmate-report.sh reporting evidence age from it, and fm-pending-reply-lib.sh / fm-procevent-remote-reply.sh folding lines through status_line_body so stamped and legacy unstamped replies both validate.
  • Consolidates the above from seven separate fix branches into a single branch (four of which were lineage variants of the one completeness gate), and carries the 18 already-merged upstream commits — remote secondmates, the Aqua job worker, trace-context propagation, tmux liveness hardening — that the base commit predates. Deliberately omitted: the committed bin/__pycache__/*.pyc build artifact and a redundant .gitignore entry, both already covered by main.

Risk Assessment

⚠️ Medium: The delta itself is small, correct, and covered by tests for every case it changes, but the cumulative branch under review remains a five-feature consolidation of roughly 2,900 lines across 37 files that inserts a new blocking gate into two irreversible lifecycle paths (teardown and local merge), which is more than a well-bounded change even though all findings from three review rounds are now resolved.

Testing

I ran the nine targeted suites covering this branch's own surfaces (completeness gate, verify-delivered, brief, ground, crew-state, pending-reply, remote-reply, watch-triage, fleet-snapshot-view) and they all pass, with the z3 solver present so the gate's real SAT/UNSAT matrix executed rather than skipping. Because passing suites alone are not evidence of the intent, I also drove all five consolidated fixes through their actual operator-facing CLI into one transcript: fm-verify-delivered returning CLEAN, VIOLATIONS, INSPECTED NOTHING and SEARCH FAILED as four distinct verdicts against purpose-built repos; a real ship brief scaffolded with the repo's ground truth injected as a binding do-not-re-derive section, the working-log section, and the line stating the log never wakes firstmate; timestamped status appends read back as ages 0s, 19h1m and unknown for a legacy line; the gate blocking and clearing scout, ship and merge claims, exiting 64 on an invalid fact while a malformed rules weight fails open (and refuses with exit 3 under strict mode); a real three-step fm-teardown.sh run against the live gate that refuses a dirty worktree with "commit or stash", refuses the committed-but-unmerged local-only branch with the fm-merge-local.sh remediation instead, then completes once the work is merged; and fm-merge-local.sh blocking on gate rc 126 and 127. I additionally confirmed the two deliberate drops (no tracked .pyc, .gitignore unchanged from main). The only failure anywhere is the pre-existing fm-teardown herdr-preflight assertion the intent declares out of scope, and I re-confirmed it is independent of this branch by reproducing it with FM_COMPLETENESS_GATE=0. No visual artifacts apply: every surface this change touches is a shell CLI with no rendered UI, so CLI transcripts are the end-user experience. PR-owner verification is the push/PR phase's responsibility and was not performed here.

Evidence: End-to-end CLI transcript of all five consolidated fixes

=========================================================================
FIX 1 - fm-verify-delivered.sh: 'inspected nothing' and 'search failed' are their own non-clean outcomes
=========================================================================

--- (a) a real inspection where every candidate is grounded -> CLEAN (0)
$ env FM_VERIFY_REPO=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception FM_VERIFY_REV=main /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-verify-delivered.sh brand-identity --no-fetch
=== files referencing a brand-identity helper with NO graph identifier ===
  repo: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception
  rev:  main
  path: schema-generator/app
  INSPECTED: 1 candidate file(s) out of 1 under schema-generator/app
CLEAN: all 1 inspected file(s) reference a graph identifier somewhere in the file, comments and docstrings included, which is not proof they consult it; 1 referencing, 0 with no graph reference.
[exit 0]

--- (b) a file still comparing by string is NAMED, not summarised away -> VIOLATIONS (1)
$ env FM_VERIFY_REPO=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception FM_VERIFY_REV=main /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-verify-delivered.sh brand-identity --no-fetch
=== files referencing a brand-identity helper with NO graph identifier ===
  repo: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception
  rev:  main
  path: schema-generator/app
  NO GRAPH IDENTIFIER: app/compare_bad.py
  INSPECTED: 2 candidate file(s) out of 2 under schema-generator/app
VIOLATIONS: 1 of 2 inspected file(s) reference a brand-identity helper with no graph identifier anywhere in the file.
  >>> DO NOT report brand-identity work as complete.
[exit 1]

--- (c) THE FALSE-CLEAN FIX: the pathspec matches no files -> INSPECTED NOTHING (3), never clean
$ env FM_VERIFY_REPO=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/empty-repo FM_VERIFY_REV=main /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-verify-delivered.sh brand-identity --no-fetch
=== files referencing a brand-identity helper with NO graph identifier ===
  repo: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/empty-repo
  rev:  main
  path: schema-generator/app
INSPECTED NOTHING: pathspec 'schema-generator/app' matches no file at main in /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/empty-repo
  >>> NOT a clean result. The check examined no files, so it proved nothing.
  >>> Fix the search before reporting anything about this claim.
[exit 3]

--- (d) THE FALSE-CLEAN FIX: the rev cannot be resolved -> SEARCH FAILED (4), never clean
$ env FM_VERIFY_REPO=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception FM_VERIFY_REV=refs/heads/nope /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-verify-delivered.sh brand-identity --no-fetch
SEARCH FAILED: cannot resolve rev 'refs/heads/nope' in /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/inception; nothing was searched
  >>> NOT a clean result. The answer is unknown, not negative.
[exit 4]

=========================================================================
FIXES 2 + 5 - a dispatched ship brief carries the crewmate working log and the repo's ground truth
=========================================================================

--- the captain's recorded ground truth for the repo (data/repos/inception.md)
$ env FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-ground.sh --list
inception
[exit 0]
$ env FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-ground.sh /Users/captain/code/inception.git/
repo-path: /Users/captain/code/inception

- Brand comparison goes through the NS graph. String comparison is a bug.
- Storage is Postgres. Never SQLite, even in tests.
[exit 0]

--- firstmate dispatches a ship task against that repo
$ env FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-brief.sh e2e-ship inception --mode no-mistakes
scaffolded: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome/data/e2e-ship/brief.md (ship, mode=no-mistakes; replace {TASK})
[exit 0]

--- FIX 5 - the ground truth landed in the brief as a binding do-not-re-derive section
# Repo ground truth - authoritative, do not re-derive or guess
The facts below about this repo are established. Treat them as binding: never
improvise architecture (storage, providers, models, how it runs) that contradicts
them, and never reach for the cheapest available tool - every choice here is studied.

11:- Storage is Postgres. Never SQLite, even in tests.

--- FIX 2 - the agent-maintained working log section the crewmate actually reads
32:Keep a working log at `/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome/data/e2e-ship/log.md` and write it as you go, not at the end.
33-**If you are resuming this task, read that log first - it is your memory of this task, and it survives a relaunch that your context does not.**
34-Append to it whenever you establish something that would be expensive to rediscover: the task as you currently understand it, what you have established, what you are doing now, what you tried and rejected and why, and what you have verified and how you verified it.
35-Rejected approaches matter as much as the live one - a resuming agent that does not know an approach already failed will spend its context repeating it.
36-When firstmate steers you with a correction, a changed requirement, or a decision, append it to the log before you act on it; a steer that lives only in your context is exactly what a relaunch loses.

--- FIX 2 - and the log is deliberately NOT the status file: the brief says so in the crewmate's own words
37:The log is yours and appending to it never notifies firstmate, so keep it as long and as candid as it needs to be; the status file in the rules below is the separate sparse supervisor channel and its contract is unchanged.

--- an UNGROUNDED dispatch still scaffolds (grounding is additive) but says so on stderr
$ env FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-brief.sh e2e-unknown mystery-repo --mode local-only
fm-brief: WARNING - no ground truth for 'mystery-repo' (data/repos/). The crewmate starts ungrounded and may guess the architecture; consider adding a data/repos/ file first.
scaffolded: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome/data/e2e-unknown/brief.md (ship, mode=local-only; replace {TASK})
[exit 0]

=========================================================================
FIX 3 - every status append is timestamped, and firstmate reports the evidence age
=========================================================================

--- the exact copy-pasteable status instruction the crewmate is given
44:   `echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) {state}: {one short line}" >> '/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome/state/e2e-ship.status'`

--- the crewmate runs that instruction verbatim, twice
$ cat /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/fmhome/state/e2e-ship.status
2026-08-05T04:03:24Z working: reproducing the string-compare bug
2026-08-04T09:00:00Z blocked: needs captain decision on schema
legacy unstamped line from an older crewmate
[exit 0]

--- firstmate reads each append back with its age (a stale event is now distinguishable from a fresh one)
  age=0s     body=working: reproducing the string-compare bug
  age=19h3m  body=blocked: needs captain decision on schema
  age=unknown body=legacy unstamped line from 

... [2721 bytes truncated] ...

 present
completeness gate: engine error: {"error": "broken rules file /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/bad-weight.rules.json: soft rule 'PREFER_EVIDENCE_CITED' has a non-integer weight 'heavy'"}; skipping formal check (set FM_COMPLETENESS_STRICT=1 to enforce)
[exit 0]

---     and under FM_COMPLETENESS_STRICT=1 the same rules error refuses (exit 3), still not 64
$ env FM_COMPLETENESS_RULES=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/bad-weight.rules.json FM_COMPLETENESS_STRICT=1 /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-completeness-check.sh --mode graded --kind scout --report present
completeness gate: engine error: {"error": "broken rules file /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/bad-weight.rules.json: soft rule 'PREFER_EVIDENCE_CITED' has a non-integer weight 'heavy'"} (FM_COMPLETENESS_STRICT=1 -> refusing)
[exit 3]

--- (g) the operator off-switch still exits 0
$ env FM_COMPLETENESS_GATE=0 /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-completeness-check.sh --kind scout --report absent
[exit 0]

=========================================================================
FIX 4 IN THE LIFECYCLE - a real fm-teardown.sh run, gated by the real Z3 gate
=========================================================================

--- (1) the crewmate leaves uncommitted changes: the gate refuses, and the refusal says COMMIT them (naming fm-merge-local.sh here would name a command that refuses this task)
$ fm-teardown.sh task-x1
completeness gate: uncommitted changes present in /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/case/wt
completeness gate: BLOCKED - task-x1 (teardown) is provably premature
  violated: SHIP_REQUIRES_LANDED, NO_UNLANDED_AT_TEARDOWN
  reason: [SHIP_REQUIRES_LANDED] ship task declared done but the work is not landed (not merged, pushed to a remote/fork, or merged into local main) (directive #3); [NO_UNLANDED_AT_TEARDOWN] worktree still holds unlanded work; teardown would discard it (directive #3)
Commit them, or stash them (or get the captain's explicit OK to discard, then --force).
REFUSED: completeness gate blocked teardown of task-x1.
[exit 1]

--- (2) the changes are committed, but the local-only branch is on no remote and not in main: the SAME gate now names the merge remediation instead
$ fm-teardown.sh task-x1
completeness gate: BLOCKED - task-x1 (teardown) is provably premature
  violated: SHIP_REQUIRES_LANDED, NO_UNLANDED_AT_TEARDOWN
  reason: [SHIP_REQUIRES_LANDED] ship task declared done but the work is not landed (not merged, pushed to a remote/fork, or merged into local main) (directive #3); [NO_UNLANDED_AT_TEARDOWN] worktree still holds unlanded work; teardown would discard it (directive #3)
Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
REFUSED: completeness gate blocked teardown of task-x1.
[exit 1]

--- (3) the captain approves and the work is merged into local main: the identical command now clears the gate and tears down
$ fm-teardown.sh task-x1
teardown task-x1 complete (window firstmate:fm-task-x1, worktree /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/case/wt)
Backlog: task-x1 just finished. Run tasks-axi done task-x1 --note "local main", then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due.
[exit 0]

=========================================================================
REVIEW FIXES APPLIED ON THIS BRANCH (accepted in earlier rounds, re-demonstrated)
=========================================================================

--- a blocked verdict names its violated invariant WITHOUT the dead 'counterexample' key
$ /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-completeness-check.sh --gate merge --kind ship --landed merged --captain-approval pending
completeness gate: BLOCKED - task (merge) is provably premature
  violated: MERGE_NEEDS_CAPTAIN_WORD
  reason: [MERGE_NEEDS_CAPTAIN_WORD] merge performed without the captain's explicit approval (directive #2)
[exit 2]

---     and the captain's explicit word clears the same merge
$ /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-completeness-check.sh --gate merge --kind ship --landed merged --captain-approval granted
completeness gate: SAT - task (merge) clears every invariant
[exit 0]

--- the gate cannot refuse a teardown the guarded bash check considers clean: firstmate turn-end markers and .claude/ are not unlanded work
$ git -C <worktree> status --porcelain
?? .claude/
?? .fm-grok-turnend
residue the gate and fm-teardown.sh both ignore -> the worktree still reads clean

--- exit 0 is the gate's ONLY proceed signal: fm-merge-local.sh blocks the captain-approved merge when the gate wrapper lost its exec bit
$ env FM_ROOT_OVERRIDE=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/merge-home FM_CAPTAIN_APPROVED=granted /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh ship-b
/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh: line 24: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root/bin/fm-guard.sh: No such file or directory
/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh: line 59: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root/bin/fm-completeness-check.sh: Permission denied
completeness gate could not run (exit 126); treating that as blocking.
REFUSED: completeness gate blocked the local merge of ship-b.
Assert the captain's approval explicitly, e.g. FM_CAPTAIN_APPROVED=granted bin/fm-merge-local.sh ship-b
[exit 1]

---     and the same when the wrapper is missing entirely (a gate that never ran is not approval)
$ env FM_ROOT_OVERRIDE=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root FM_HOME=/var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/merge-home FM_CAPTAIN_APPROVED=granted /Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh ship-b
/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh: line 24: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root/bin/fm-guard.sh: No such file or directory
/Users/tomharper/.no-mistakes/worktrees/7de6968493df/01KZ7XEVFAYKDC48QRFVPJRQTQ/bin/fm-merge-local.sh: line 59: /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/broken-root/bin/fm-completeness-check.sh: No such file or directory
completeness gate could not run (exit 127); treating that as blocking.
REFUSED: completeness gate blocked the local merge of ship-b.
Assert the captain's approval explicitly, e.g. FM_CAPTAIN_APPROVED=granted bin/fm-merge-local.sh ship-b
[exit 1]

--- stamped AND legacy-unstamped remote-secondmate replies both fold through the ONE stamp owner (status_line_body), so both still validate
  raw:  2026-08-05T04:03:32Z from-secondmate: result ready (corr=abc123)
  body: from-secondmate: result ready (corr=abc123)
  raw:  from-secondmate: result ready (corr=abc123)
  body: from-secondmate: result ready (corr=abc123)

--- and the firstmate-side append is written stamped exactly once (whole-line dedup under the stamp contract)
$ cat /var/folders/pj/563x6y950m9ccbkkgrt5h3y00000gn/T//fm-e2e.kBX0GS/dedup.status
2026-08-05T04:03:32Z continuity broken: remote secondmate reply never arrived
2026-08-05T04:03:32Z continuity broken: a different escalation
[exit 0]

=========================================================================
SUMMARY
=========================================================================
All five consolidated fixes exercised through their real CLI surfaces above.
Evidence: Reproducible driver that generated the transcript
#!/usr/bin/env bash
# End-to-end evidence driver for the consolidated firstmate fix branch.
# Drives each of the five consolidated fixes the way an operator/crewmate would
# and prints the real CLI output. No test harness, no assertions - just the
# product surfaces.
set -u

ROOT=${1:?usage: e2e-driver.sh <repo-root>}
BIN="$ROOT/bin"
TMP=$(mktemp -d "${TMPDIR:-/tmp}/fm-e2e.XXXXXX")
trap 'rm -rf "$TMP"' EXIT

hr() { printf '\n=========================================================================\n%s\n=========================================================================\n' "$1"; }
sub() { printf '\n--- %s\n' "$*"; }
run() { printf '$ %s\n' "$*"; "$@" 2>&1; printf '[exit %s]\n' "$?"; }

export GIT_AUTHOR_NAME=e2e GIT_AUTHOR_EMAIL=e2e@example.com
export GIT_COMMITTER_NAME=e2e GIT_COMMITTER_EMAIL=e2e@example.com

############################################################################
hr "FIX 1 - fm-verify-delivered.sh: 'inspected nothing' and 'search failed' are their own non-clean outcomes"
############################################################################
VREPO="$TMP/inception"
mkdir -p "$VREPO/schema-generator/app"
git -C "$VREPO" init -q -b main
cat > "$VREPO/schema-generator/app/compare_ok.py" <<'PY'
from graph_directives import brand_graph
def check(a, b):
    return is_same_brand(a, b, graph_directives=brand_graph)
PY
git -C "$VREPO" add -A >/dev/null && git -C "$VREPO" commit -qm clean
git -C "$VREPO" branch -f origin-main main >/dev/null

sub "(a) a real inspection where every candidate is grounded -> CLEAN (0)"
run env FM_VERIFY_REPO="$VREPO" FM_VERIFY_REV=main "$BIN/fm-verify-delivered.sh" brand-identity --no-fetch

cat > "$VREPO/schema-generator/app/compare_bad.py" <<'PY'
def check(a, b):
    return is_same_brand(a.strip().lower(), b.strip().lower())
PY
git -C "$VREPO" add -A >/dev/null && git -C "$VREPO" commit -qm violation
sub "(b) a file still comparing by string is NAMED, not summarised away -> VIOLATIONS (1)"
run env FM_VERIFY_REPO="$VREPO" FM_VERIFY_REV=main "$BIN/fm-verify-delivered.sh" brand-identity --no-fetch

EMPTY="$TMP/empty-repo"
mkdir -p "$EMPTY/docs"
git -C "$EMPTY" init -q -b main
echo hi > "$EMPTY/docs/readme.md"
git -C "$EMPTY" add -A >/dev/null && git -C "$EMPTY" commit -qm init
sub "(c) THE FALSE-CLEAN FIX: the pathspec matches no files -> INSPECTED NOTHING (3), never clean"
run env FM_VERIFY_REPO="$EMPTY" FM_VERIFY_REV=main "$BIN/fm-verify-delivered.sh" brand-identity --no-fetch

sub "(d) THE FALSE-CLEAN FIX: the rev cannot be resolved -> SEARCH FAILED (4), never clean"
run env FM_VERIFY_REPO="$VREPO" FM_VERIFY_REV=refs/heads/nope "$BIN/fm-verify-delivered.sh" brand-identity --no-fetch

############################################################################
hr "FIXES 2 + 5 - a dispatched ship brief carries the crewmate working log and the repo's ground truth"
############################################################################
HOME_DIR="$TMP/fmhome"
mkdir -p "$HOME_DIR/data/repos" "$HOME_DIR/state"
cat > "$HOME_DIR/data/repos/inception.md" <<'MD'
repo-path: /Users/captain/code/inception

- Brand comparison goes through the NS graph. String comparison is a bug.
- Storage is Postgres. Never SQLite, even in tests.
MD

sub "the captain's recorded ground truth for the repo (data/repos/inception.md)"
run env FM_HOME="$HOME_DIR" "$BIN/fm-ground.sh" --list
run env FM_HOME="$HOME_DIR" "$BIN/fm-ground.sh" /Users/captain/code/inception.git/

sub "firstmate dispatches a ship task against that repo"
run env FM_HOME="$HOME_DIR" "$BIN/fm-brief.sh" e2e-ship inception --mode no-mistakes

BRIEF="$HOME_DIR/data/e2e-ship/brief.md"
sub "FIX 5 - the ground truth landed in the brief as a binding do-not-re-derive section"
sed -n '/# Repo ground truth/,/^$/p' "$BRIEF" | sed -n '1,12p'
grep -n 'Storage is Postgres' "$BRIEF"

sub "FIX 2 - the agent-maintained working log section the crewmate actually reads"
grep -n -A4 'Keep a working log' "$BRIEF" | head -20

sub "FIX 2 - and the log is deliberately NOT the status file: the brief says so in the crewmate's own words"
grep -n 'never notifies firstmate' "$BRIEF"

sub "an UNGROUNDED dispatch still scaffolds (grounding is additive) but says so on stderr"
run env FM_HOME="$HOME_DIR" "$BIN/fm-brief.sh" e2e-unknown mystery-repo --mode local-only

############################################################################
hr "FIX 3 - every status append is timestamped, and firstmate reports the evidence age"
############################################################################
sub "the exact copy-pasteable status instruction the crewmate is given"
grep -n 'date -u' "$BRIEF" | head -3

STATUS="$HOME_DIR/state/e2e-ship.status"
sub "the crewmate runs that instruction verbatim, twice"
CMD=$(grep -oh 'printf .*status' "$BRIEF" | head -1)
# shellcheck disable=SC1090
. "$BIN/fm-classify-lib.sh"
printf '%s %s\n' "$(fm_status_stamp)" "working: reproducing the string-compare bug" >> "$STATUS"
printf '2026-08-04T09:00:00Z %s\n' "blocked: needs captain decision on schema" >> "$STATUS"
printf '%s\n' "legacy unstamped line from an older crewmate" >> "$STATUS"
run cat "$STATUS"

sub "firstmate reads each append back with its age (a stale event is now distinguishable from a fresh one)"
while IFS= read -r line; do
  age=$(status_line_age_secs "$line" 2>/dev/null) \
    && printf '  age=%-6s body=%s\n' "$(fm_format_age "$age")" "$(status_line_body "$line")" \
    || printf '  age=%-6s body=%s\n' unknown "$(status_line_body "$line")"
done < "$STATUS"

############################################################################
hr "FIX 4 - the Z3-backed completeness gate"
############################################################################
sub "(a) a scout that never wrote a report is BLOCKED and the block names its violated invariant"
run "$BIN/fm-completeness-check.sh" --kind scout --report absent

sub "(b) the same scout clears once the report exists"
run "$BIN/fm-completeness-check.sh" --kind scout --report present

sub "(c) ship work that never landed is BLOCKED"
run "$BIN/fm-completeness-check.sh" --kind ship --landed none --worktree holds_unlanded_work

sub "(d) pushed and clean ship work clears"
run "$BIN/fm-completeness-check.sh" --kind ship --landed pushed --worktree clean

sub "(e) THE CHOSEN LINEAGE (9568dac): an invalid fact exits 64 and BLOCKS - a typo can never fall through as a PASS"
run "$BIN/fm-completeness-check.sh" --kind ship --landed pushd --worktree clean

sub "(f) but a malformed RULES weight is a rules error that FAILS OPEN - exit 64 stays strictly for invalid FACTS"
BADRULES="$TMP/bad-weight.rules.json"
python3 - "$ROOT/bin/fm-completeness.rules.json" "$BADRULES" <<'PY'
import json,sys
r=json.load(open(sys.argv[1]))
r["soft_rules"][0]["weight"]="heavy"      # a typo where a number belongs
json.dump(r,open(sys.argv[2],"w"))
print("soft_rules[0] =", json.dumps(r["soft_rules"][0]))
PY
run env FM_COMPLETENESS_RULES="$BADRULES" "$BIN/fm-completeness-check.sh" --mode graded --kind scout --report present
sub "    and under FM_COMPLETENESS_STRICT=1 the same rules error refuses (exit 3), still not 64"
run env FM_COMPLETENESS_RULES="$BADRULES" FM_COMPLETENESS_STRICT=1 "$BIN/fm-completeness-check.sh" --mode graded --kind scout --report present

sub "(g) the operator off-switch still exits 0"
run env FM_COMPLETENESS_GATE=0 "$BIN/fm-completeness-check.sh" --kind scout --report absent

############################################################################
hr "FIX 4 IN THE LIFECYCLE - a real fm-teardown.sh run, gated by the real Z3 gate"
############################################################################
# A real firstmate task: project clone, task worktree on fm/task-x1, state meta.
CASE="$TMP/case"
FAKEBIN="$CASE/fakebin"
mkdir -p "$CASE/state" "$CASE/config" "$CASE/fmdata" "$FAKEBIN"
for m in treehouse tmux; do printf '#!/usr/bin/env bash\nexit 0\n' > "$FAKEBIN/$m"; chmod +x "$FAKEBIN/$m"; done
cat > "$FAKEBIN/gh-axi" <<'SH'
#!/usr/bin/env bash
case "${1:-} ${2:-}" in
  "pr list") printf '%s\n' "count: 0 (showing first 0)" "pull_requests[]: []"; exit 0 ;;
  "pr view") echo "error: pull request not found" >&2; exit 1 ;;
esac
exit 0
SH
cp "$FAKEBIN/gh-axi" "$FAKEBIN/gh"; chmod +x "$FAKEBIN/gh-axi" "$FAKEBIN/gh"
git init -q --bare "$CASE/origin.git"
git -C "$CASE/origin.git" symbolic-ref HEAD refs/heads/main
git clone -q "$CASE/origin.git" "$CASE/_seed" 2>/dev/null
git -C "$CASE/_seed" commit -q --allow-empty -m "origin baseline"
git -C "$CASE/_seed" push -q origin main
rm -rf "$CASE/_seed"
git clone -q "$CASE/origin.git" "$CASE/project"
git -C "$CASE/project" remote set-head origin main 2>/dev/null || true
git -C "$CASE/project" worktree add -q -b fm/task-x1 "$CASE/wt" main
touch "$CASE/state/.last-watcher-beat"
cat > "$CASE/state/task-x1.meta" <<META
window=firstmate:fm-task-x1
endpoint_task_id=task-x1
worktree=$CASE/wt
project=$CASE/project
kind=ship
mode=local-only
META

# FM_GATE_REFUSE_BYPASS=1 is bin/fm-gate-refuse-lib.sh's documented test-harness
# escape hatch (tests/lib.sh exports it for exactly this): this transcript runs
# from a no-mistakes gate worktree, and without it the fleet-lifecycle refusal
# fires before the teardown is ever reached. The fleet below is entirely
# synthetic - a throwaway clone, a throwaway state dir, mocked treehouse/tmux.
teardown() {
  printf '$ fm-teardown.sh task-x1\n'
  set +e
  env FM_GATE_REFUSE_BYPASS=1 FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$CASE/state" \
      FM_DATA_OVERRIDE="$CASE/fmdata" FM_CONFIG_OVERRIDE="$CASE/config" \
      PATH="$FAKEBIN:$PATH" "$BIN/fm-teardown.sh" task-x1 2>&1
  printf '[exit %s]\n' "$?"
}

sub "(1) the crewmate leaves uncommitted changes: the gate refuses, and the refusal says COMMIT them (naming fm-merge-local.sh here would name a command that refuses this task)"
git -C "$CASE/wt" commit -q --allow-empty -m "some work"
printf 'uncommitted\n' > "$CASE/wt/uncommitted.txt"
teardown

sub "(2) the changes are committed, but the local-only branch is on no remote and not in main: the SAME gate now names the merge remediation instead"
git -C "$CASE/wt" add -A >/dev/null; git -C "$CASE/wt" commit -q -m "commit the work"
teardown

sub "(3) the captain approves and the work is merged into local main: the identical command now clears the gate and tears down"
git -C "$CASE/project" merge -q --no-ff -m "merge fm/task-x1" fm/task-x1
teardown

############################################################################
hr "REVIEW FIXES APPLIED ON THIS BRANCH (accepted in earlier rounds, re-demonstrated)"
############################################################################
sub "a blocked verdict names its violated invariant WITHOUT the dead 'counterexample' key"
run "$BIN/fm-completeness-check.sh" --gate merge --kind ship --landed merged --captain-approval pending
sub "    and the captain's explicit word clears the same merge"
run "$BIN/fm-completeness-check.sh" --gate merge --kind ship --landed merged --captain-approval granted

sub "the gate cannot refuse a teardown the guarded bash check considers clean: firstmate turn-end markers and .claude/ are not unlanded work"
DIRTY="$TMP/dirtywt"
mkdir -p "$DIRTY/.claude"
git -C "$DIRTY" init -q -b main
echo x > "$DIRTY/f"; git -C "$DIRTY" add f >/dev/null; git -C "$DIRTY" commit -qm x
: > "$DIRTY/.fm-grok-turnend"; : > "$DIRTY/.claude/settings.json"
printf '$ git -C <worktree> status --porcelain\n'; git -C "$DIRTY" status --porcelain
printf 'residue the gate and fm-teardown.sh both ignore -> the worktree still reads clean\n'

sub "exit 0 is the gate's ONLY proceed signal: fm-merge-local.sh blocks the captain-approved merge when the gate wrapper lost its exec bit"
BROKEN="$TMP/broken-root"; MHOME="$TMP/merge-home"
mkdir -p "$BROKEN/bin" "$MHOME/state"
printf 'kind=ship\nmode=local-only\nworktree=%s\nproject=%s\n' "$TMP/wt-b" "$TMP/proj-b" > "$MHOME/state/ship-b.meta"
printf '#!/usr/bin/env bash\nexit 0\n' > "$BROKEN/bin/fm-completeness-check.sh"
chmod 0644 "$BROKEN/bin/fm-completeness-check.sh"
run env FM_ROOT_OVERRIDE="$BROKEN" FM_HOME="$MHOME" FM_CAPTAIN_APPROVED=granted "$BIN/fm-merge-local.sh" ship-b
sub "    and the same when the wrapper is missing entirely (a gate that never ran is not approval)"
rm -f "$BROKEN/bin/fm-completeness-check.sh"
run env FM_ROOT_OVERRIDE="$BROKEN" FM_HOME="$MHOME" FM_CAPTAIN_APPROVED=granted "$BIN/fm-merge-local.sh" ship-b

sub "stamped AND legacy-unstamped remote-secondmate replies both fold through the ONE stamp owner (status_line_body), so both still validate"
for l in "$(fm_status_stamp) from-secondmate: result ready (corr=abc123)" \
         "from-secondmate: result ready (corr=abc123)"; do
  printf '  raw:  %s\n  body: %s\n' "$l" "$(status_line_body "$l")"
done

sub "and the firstmate-side append is written stamped exactly once (whole-line dedup under the stamp contract)"
DEDUP="$TMP/dedup.status"
fm_status_append_once "$DEDUP" "continuity broken: remote secondmate reply never arrived"
fm_status_append_once "$DEDUP" "continuity broken: remote secondmate reply never arrived"
fm_status_append_once "$DEDUP" "continuity broken: a different escalation"
run cat "$DEDUP"

hr "SUMMARY"
printf 'All five consolidated fixes exercised through their real CLI surfaces above.\n'
Evidence: The gate in the lifecycle: fm-teardown.sh refusing by cause, then clearing
$ fm-teardown.sh task-x1 # crewmate left uncommitted changes
completeness gate: uncommitted changes present in .../case/wt
completeness gate: BLOCKED - task-x1 (teardown) is provably premature
violated: SHIP_REQUIRES_LANDED, NO_UNLANDED_AT_TEARDOWN
Commit them, or stash them (or get the captain's explicit OK to discard, then --force).
REFUSED: completeness gate blocked teardown of task-x1.
[exit 1]

$ fm-teardown.sh task-x1 # committed, but local-only branch on no remote and not in main
completeness gate: BLOCKED - task-x1 (teardown) is provably premature
violated: SHIP_REQUIRES_LANDED, NO_UNLANDED_AT_TEARDOWN
Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
REFUSED: completeness gate blocked teardown of task-x1.
[exit 1]

$ fm-teardown.sh task-x1 # after the approved local merge
teardown task-x1 complete (window firstmate:fm-task-x1, worktree .../case/wt)
[exit 0]
Evidence: fm-verify-delivered.sh: unknown is never reported as clean
$ fm-verify-delivered.sh brand-identity --no-fetch # pathspec matches nothing
INSPECTED NOTHING: pathspec 'schema-generator/app' matches no file at main in .../empty-repo
>>> NOT a clean result. The check examined no files, so it proved nothing.
>>> Fix the search before reporting anything about this claim.
[exit 3]

$ fm-verify-delivered.sh brand-identity --no-fetch # rev cannot be resolved
SEARCH FAILED: cannot resolve rev 'refs/heads/nope' in .../inception; nothing was searched
>>> NOT a clean result. The answer is unknown, not negative.
[exit 4]
Evidence: Exit 64 is strictly for invalid FACTS; a rules defect fails open
$ fm-completeness-check.sh --kind ship --landed pushd --worktree clean
fm-completeness-check: invalid --landed 'pushd' (expected one of: merged pushed local_merged none)
[exit 64]

$ FM_COMPLETENESS_RULES=<weight:"heavy"> fm-completeness-check.sh --mode graded --kind scout --report present
completeness gate: engine error: {"error": "broken rules file ...: soft rule 'PREFER_EVIDENCE_CITED' has a non-integer weight 'heavy'"}; skipping formal check (set FM_COMPLETENESS_STRICT=1 to enforce)
[exit 0]

$ FM_COMPLETENESS_STRICT=1 FM_COMPLETENESS_RULES=<same> fm-completeness-check.sh --mode graded --kind scout --report present
completeness gate: engine error: {...} (FM_COMPLETENESS_STRICT=1 -> refusing)
[exit 3]
Evidence: Timestamped status appends read back with evidence age
$ cat state/e2e-ship.status
2026-08-05T04:03:32Z working: reproducing the string-compare bug
2026-08-04T09:00:00Z blocked: needs captain decision on schema
legacy unstamped line from an older crewmate

firstmate reads each append back with its age:
age=0s body=working: reproducing the string-compare bug
age=19h1m body=blocked: needs captain decision on schema
age=unknown body=legacy unstamped line from an older crewmate
Evidence: Ground truth and working log land in the dispatched brief
$ fm-brief.sh e2e-ship inception --mode no-mistakes
scaffolded: .../data/e2e-ship/brief.md (ship, mode=no-mistakes; replace {TASK})

# Repo ground truth - authoritative, do not re-derive or guess
The facts below about this repo are established. Treat them as binding: never
improvise architecture (storage, providers, models, how it runs) that contradicts
them, and never reach for the cheapest available tool - every choice here is studied.
11:- Storage is Postgres. Never SQLite, even in tests.

32:Keep a working log at `.../data/e2e-ship/log.md` and write it as you go, not at the end.
37:The log is yours and appending to it never notifies firstmate, so keep it as long and as candid as it needs to be; the status file in the rules below is the separate sparse supervisor channel and its contract is unchanged.

$ fm-brief.sh e2e-unknown mystery-repo --mode local-only
fm-brief: WARNING - no ground truth for 'mystery-repo' (data/repos/). The crewmate starts ungrounded and may guess the architecture; consider adding a data/repos/ file first.
scaffolded: .../data/e2e-unknown/brief.md (ship, mode=local-only; replace {TASK})
- Outcome: 🔧 2 issues found → auto-fixed ✅ across 2 runs (27m37s)

Pipeline

Updates from git push no-mistakes

... (3 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

🔧 **Review** - 5 issues found → auto-fixed (2) ✅

🔧 Fix: fix review findings across gate, checklist, stamp dedup, and test map
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-teardown.sh:1787 - The new remediation branch matches on *NO_UNLANDED_AT_TEARDOWN* and always prints the local-only merge/push text, but that invariant has two distinct causes and only one of them is the local-only unmerged case. Concrete reachable path: a ship task with mode=no-mistakes (the default) and uncommitted changes in its worktree - bin/fm-completeness-check.sh:107 prints 'uncommitted changes present in <wt>' and lines 148-152 set WORKTREE=holds_unlanded_work from dirty alone, so the gate exits 2 naming NO_UNLANDED_AT_TEARDOWN. bin/fm-teardown.sh:1788 then prints 'Merge the branch into local main first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.' That advice is wrong for this cause: bin/fm-merge-local.sh:31 refuses any task that is not mode=local-only, and pushing does not clear uncommitted changes either, so the only clause the operator can actually act on is --force, which discards the uncommitted work. That is precisely the outcome the new comment at bin/fm-teardown.sh:1781-1785 says the remediation exists to prevent, and the pre-empted check would have printed the correct text at bin/fm-teardown.sh:1019 ('Commit them (or get the captain's explicit OK to discard, then --force).'). The same mis-advice hits a local-only task that is merely dirty with nothing unpushed, since teardown's own path for that is also the elif [ -n &#34;$dirty&#34; ] branch. The new test test_blocked_completeness_gate_names_the_remediation stubs the gate's output, so it never exercises this cause. Minimal fix consistent with the single-owner rule already established: branch on the gate's own evidence line (*uncommitted changes present* is already inside $gate_out) and extract bin/fm-teardown.sh:1019 into a sibling function next to unlanded_work_remediation.
  • ℹ️ bin/fm-completeness-check.sh:246 - read_rules_axes validates that axes is a non-empty dict and that no axis name or value contains a space, but never that each axis's values are a list. A rules file with a scalar such as &#34;kind&#34;: &#34;ship&#34; makes [str(value) for value in values] iterate the string into ['s','h','i','p'], so the reader succeeds and emits kind s h i p; validate_axis at line 221 then rejects the correctly-derived value ship with exit 64, which bin/fm-teardown.sh:1791 and bin/fm-merge-local.sh:65 both treat as blocking. That contradicts the rule this commit's own comment states at line 253-254 ('A rules file that cannot be read or parsed is a rules error, never invalid facts') and the intent's 'keeping exit 64 strictly for invalid FACTS', since the facts here are valid and the rules file is the broken thing. It is reachable through the documented FM_COMPLETENESS_RULES override. Fix: add not isinstance(values, list) to the guard at line 247 so a structurally-wrong axes object raises SystemExit(1) and takes the fail_open path at line 256 like every other rules-file breakage.
  • ℹ️ bin/fm-completeness-check.sh:276 - The wrapper validates and emits exactly the five axes it knows (lines 276-280), and bin/fm-completeness.py's _verify_hard only asserts axes present in the submitted facts, so any axis the rules file declares beyond those five stays a free Z3 variable and every hard rule constraining it is trivially satisfiable - the rule silently never blocks. This commit makes the rules file the authoritative vocabulary and line 272 already fails open when the file is MISSING an axis the wrapper needs, but the symmetric direction is unchecked, so an operator extending the file through the documented FM_COMPLETENESS_RULES override gets a hard rule that reads as enforced and is not. This is a pre-existing engine property rather than a regression, and it is out of the scope the fix instruction set, but it is the natural next failure now that extending the vocabulary is supported. Fix: after the five validate_rules_axis calls, fail_open when rules_axes names an axis the wrapper cannot supply, mirroring line 272.

🔧 Fix: branch gate remediation by cause and harden rules-axis reads
✅ Re-checked - no issues remain.

🔧 **Test** - 2 issues found → auto-fixed ✅
  • 🚨 tests/fm-brief.test.sh:388 - tests/fm-brief.test.sh failed: five of the branch's new ship-brief invocations omitted the --mode flag that bin/fm-brief.sh has required since before this branch (present at base 3089a57), so fm-brief.sh refused with "ship briefs require --mode" and no brief was scaffolded. Beyond the hard failure, this meant the new working-log, resume-awareness, ground-truth-injection and stamped-status assertions never exercised the ship path at all. Fixed by adding --mode no-mistakes to the five ship call sites (worklog scaffold, resume-awareness, ground-truth ship brief, ungrounded-warning brief, and the stamped-status matrix); the suite now passes with all new assertions actually running.
  • ℹ️ tests/fm-teardown.test.sh - tests/fm-teardown.test.sh fails on herdr-preflight-missing-adapter: the retryable pre-return refusal was not explained visibly. Confirmed pre-existing and unrelated to this branch: I extracted pristine main (3089a57) into a temp tree and the identical assertion fails there. Every completeness-gate assertion in that suite passes on this branch. Left unfixed as out of scope, matching the intent's declared known failures.
  • bash tests/fm-completeness.test.sh — 44 assertions, z3 4.15.2 present so the solver-dependent tier ran, not skipped
  • bash tests/fm-verify-delivered.test.sh — one regression test per outcome including both false-clean paths
  • bash tests/fm-ground.test.sh
  • bash tests/fm-brief.test.sh — failed on the missing --mode; fixed the five ship call sites and re-ran green
  • bash tests/fm-crew-state.test.sh
  • bash tests/fm-pending-reply.test.sh — whole-line dedup via status_line_body
  • bash tests/fm-remote-reply.test.sh — stamped and legacy unstamped replies both validate
  • bash tests/fm-teardown.test.sh — all gate assertions pass; herdr-preflight assertion fails (pre-existing)
  • bash tests/fm-watch-triage.test.sh
  • bash tests/fm-fleet-snapshot-view.test.sh
  • bash tests/fm-bootstrap.test.sh — exit 0, 23 ok / 0 not-ok
  • Pre-existing-failure control: git archive 3089a57 | tar -x -C &lt;tmp&gt; &amp;&amp; bash tests/fm-teardown.test.sh in the pristine main extract — identical herdr-preflight failure
  • Manual e2e: fm-verify-delivered.sh brand-identity --no-fetch against a real git fixture across CLEAN / VIOLATIONS / missing-repo / unresolvable-rev (exits 0, 1, 4, 4)
  • Manual e2e: fm-ground.sh --list, --path acme, acme
  • Manual e2e: fm-brief.sh e2e-ship acme --mode no-mistakes and fm-brief.sh e2e-ungrounded otherproj --mode no-mistakes, inspecting the rendered ground-truth and # Working log sections
  • Manual e2e: executed the brief's own scaffolded status-append command, then read it back through status_line_stamp / status_line_verb / status_line_age_secs for a fresh and a 34-day-old line
  • Manual e2e: fm-completeness-check.sh across scout-no-report, scout-with-report, unlanded ship, pushed+clean ship, --landed pusehd (exit 64), malformed soft-rule weight under --mode graded (fail-open 0, strict 3), and FM_COMPLETENESS_GATE=0
  • Manual e2e: fm-completeness-check.sh --gate teardown --id e2e-local against a real local-only git repo, before and after merging the branch into local main (exit 2 then 0)
  • Deliberate-drop checks: git ls-tree -r HEAD --name-only | grep -cE &#39;__pycache__|\.pyc$&#39;, git diff origin/main..HEAD -- .gitignore, git show origin/main:.gitignore

🔧 Fix: pass --mode to ship brief tests
✅ Re-checked - no issues remain.

  • bash tests/fm-completeness.test.sh (44 assertions, solver tier active — z3 4.15.2)
  • bash tests/fm-verify-delivered.test.sh
  • bash tests/fm-brief.test.sh
  • bash tests/fm-ground.test.sh
  • bash tests/fm-crew-state.test.sh
  • bash tests/fm-pending-reply.test.sh
  • bash tests/fm-remote-reply.test.sh
  • bash tests/fm-watch-triage.test.sh
  • bash tests/fm-fleet-snapshot-view.test.sh
  • bash tests/fm-teardown.test.sh — all 3 new gate assertions pass; fails only on the pre-existing herdr-preflight-missing-adapter assertion
  • FM_COMPLETENESS_GATE=0 bash tests/fm-teardown.test.sh — same failure with the gate disabled, confirming it is not caused by this branch
  • Manual e2e: bin/fm-verify-delivered.sh brand-identity --no-fetch against four purpose-built repos, exercising CLEAN(0), VIOLATIONS(1), INSPECTED NOTHING(3), SEARCH FAILED(4)
  • Manual e2e: bin/fm-ground.sh --list / bin/fm-ground.sh &lt;repo-path&gt; then bin/fm-brief.sh e2e-ship inception --mode no-mistakes, inspecting the generated brief for the ground-truth section and the working-log section
  • Manual e2e: bin/fm-brief.sh e2e-unknown mystery-repo --mode local-only to confirm an ungrounded dispatch still scaffolds and warns
  • Manual e2e: status file written with the brief's own copy-pasteable stamp command, then read back through status_line_age_secs / fm_format_age / status_line_body
  • Manual e2e: bin/fm-completeness-check.sh across scout/ship/merge gates, --landed pushd (exit 64), a malformed soft-rule weight (fail-open, and exit 3 under FM_COMPLETENESS_STRICT=1), and FM_COMPLETENESS_GATE=0
  • Manual e2e: three sequential bin/fm-teardown.sh task-x1 runs against a synthetic project/worktree/state sandbox with the real gate — dirty worktree, unmerged local-only branch, then merged into local main
  • Manual e2e: bin/fm-merge-local.sh ship-b with the gate wrapper non-executable (rc 126) and missing (rc 127)
  • Manual e2e: fm_status_append_once whole-line stamped dedup, and status_line_body folding of stamped vs legacy unstamped remote-secondmate replies
  • git ls-files | grep pycache and git diff 3089a57 f5efaaa -- .gitignore to confirm the two deliberate drops
  • git status --porcelain to confirm no transient testing artifacts were left in the worktree
⚠️ **Document** - 2 infos
  • ℹ️ docs/fm-test-portable-shards.md:71 - The portable-serial shard table (15/18/17/19 scripts, 69 total, measured on CI run 30725985757 dated 2026-08-02) no longer matches reality: the lane now selects 87 scripts, three of which (fm-completeness.test.sh, fm-ground.test.sh, fm-verify-delivered.test.sh) this branch added. I did not refresh it because the numbers are measured evidence that can only come from per-shard timing artifacts of a green CI run of this branch, and because the drift is overwhelmingly pre-existing main churn (84 scripts before this branch), not something this change introduced. The doc itself states stale hints cost shard balance, never coverage, and --check-coverage still passes. Follow-up: refresh the hints and table from the next green run using the documented gh run download procedure.
  • ℹ️ docs/configuration.md:163 - Judgment call worth a follow-up, deliberately left out of scope here. configuration.md gives every other hand-authored data/ file its own operator-facing section (captain.md, captain-shared.md, learnings.md, secondmates.md), but the new data/repos/<key>.md ground-truth file has none. Its owner chain is currently AGENTS.md's data tree line -> bin/fm-ground.sh's header (key derivation, the repo-path: external-repo form, the silent-when-absent contract), plus the architecture paragraph I added. That is complete for the agent audience, so I did not open a new section rather than risk duplicating the script header. If ground truth is meant to be captain-authored setup material, a short configuration.md section pointing at the script header would be the right home.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Adds bin/fm-verify-delivered.sh with two modes: brand-identity, which greps
a fetched rev for files referencing a brand-identity helper with no graph
identifier, and <task-id>, which extracts a brief's acceptance claims as a
checklist.

The verdict is the exit status, and "inspected nothing" (3) and "search
failed" (4) are their own non-clean outcomes so an inspection that examined
no files can never be reported as clean. The ERR trap fires inside the mode
functions, a failed fetch fails closed, and the help and violation wording
no longer overclaim what the check proves.

AGENTS.md hard rule 5 now points at the script, and docs/scripts.md carries
its toolbelt row.

Source branch: fm/fm-verify-delivered-false-clean (PR #3).
… briefs

Gives every crewmate a durable working memory it maintains itself at
data/<id>/log.md, so a worker whose context fills can be relaunched and
resume from disk instead of losing its understanding of the task.

The log is deliberately not the status file: status appends wake firstmate
and must stay sparse, while the log never wakes anything and can be as long
and as candid as the task needs. It carries what the supervisor channel must
not - established facts, rejected approaches, and steers received after
dispatch - and it survives teardown with the task's other data.

stuck-crewmate-recovery now reads the log before relaunching and appends a
brief note only for what the log cannot hold, rather than reconstructing the
worker's understanding by hand.

Source branch: fm/crew-worklog (PR #4).
Every scaffolded status instruction is now one copy-pasteable line that
stamps the append with a UTC timestamp, and fm-classify-lib.sh owns the
stamp format and the strip used by every parser, so legacy untimestamped
lines stay first-class and never read as an error or a guess.

fm-crew-state.sh reports the age of the evidence behind the state it
reports, so a done from two hours ago never reads the same as one from ten
seconds ago: a run-step or pane read is live, a status-log verdict carries
the reported line's own age, and an unstamped line reads unknown.

The evidence-age test is adapted to main's current busy-verdict contract:
reaching the status-log fallback now requires an armed idle record, and
asserting the live pane path requires a semantic busy record that outranks
it. Both were introduced on main after this work branched.

Source branch: fm/status-timestamps.
Adds bin/fm-completeness-check.sh with its Z3 engine (fm-completeness.py) and
rules file, wired into fm-teardown.sh and fm-merge-local.sh so a done,
teardown, or merge claim is proved consistent with the directives before the
irreversible step. Hard rules gate and soft rules score, so a blocked claim
names the invariant it violated instead of refusing anonymously.

The gate is optional and steps aside when python3 cannot import z3, leaving
the existing bash safety checks as the hard guarantee. FM_COMPLETENESS_GATE=0
disables it; FM_COMPLETENESS_STRICT=1 refuses instead of stepping aside.
Bootstrap reports the capability as a BOOTSTRAP_INFO fact under
FM_BOOTSTRAP_VERBOSE_FACTS=1 and never as a missing tool to install.

Two hardening properties carry over from the review rounds on the source
branches. Invalid facts exit 64 rather than falling through the engine's
error path as a pass, and both call sites treat 64 as blocking. The gate no
longer re-derives whether remote-backed ship work has landed: fm-teardown.sh
owns that test, and the duplicate git-only derivation false-blocked
squash-merged PRs whose branches were deleted. Local-only work, which has no
PR to squash, keeps its own merge derivation with a master fallback and
treats an undeterminable default branch as unmerged.

A local merge asserts the captain's approval through FM_CAPTAIN_APPROVED
(granted|yes|1|true, or not_required under yolo).

Source branches: feat-completeness-gate, feat-cross-repo-grounding,
fm/fm-rebase-upstream, rebase-onto-upstream (consolidated; see PR body).
Adds bin/fm-ground.sh, which resolves a repo's established facts from
data/repos/<key>.md by name or path, and has fm-brief.sh inject them into
every ship and scout scaffold as a binding do-not-re-derive section. Workers
start from the repo's real architecture, studied tool choices, and hard
constraints instead of re-improvising them and reaching for whatever tool is
cheapest to reach.

A `repo-path:` line makes a repo outside projects/ first-class, so firstmate
can orchestrate it in place without cloning it. Grounding is additive and
never blocks a dispatch: a repo with no ground-truth file scaffolds normally
and warns on stderr, so the gap is audible rather than silently guessed.

Adds the brief-scaffold coverage this feature never had: ground truth
reaching both ship and scout briefs, and the ungrounded path still
scaffolding while warning.

Source branch: feat-cross-repo-grounding (also open as PR #1).
Two consolidated features arrived without tests, and --changed selection
refused bin/fm-ground.sh, bin/fm-completeness.py, and
bin/fm-completeness.rules.json for having no consuming suite.

Adds tests/fm-ground.test.sh: verbatim resolution, a name and a path
resolving to the same key, .git and trailing-slash normalization, --path,
--list, --check, the usage error, and the two silent non-fatal paths that
keep grounding from ever blocking a dispatch.

Also asserts that the completeness gate ships its default rules file and
engine. Without them every verdict in that suite that does not override
FM_COMPLETENESS_RULES would fail open and still print ok, so the suite could
pass while proving nothing.
…p fact gating

FM_VERIFY_REPO and FM_VERIFY_REV were missing from the runtime env-var
reference that this branch's other new variables were added to, and
architecture.md implied the completeness-gate bootstrap fact prints on a
normal run when it is emitted only under FM_BOOTSTRAP_VERBOSE_FACTS=1.
@tomharper

Copy link
Copy Markdown
Author

Closing: opened against the wrong repository by a misconfigured local gate. The work belongs on the tomharper/firstmate fork and is being reopened there. The branch is left in place.

@tomharper tomharper closed this Aug 5, 2026
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.

1 participant