Skip to content

fix(bin): stop reporting live runs as failed when their unpushed tip is unresolvable - #1855

Open
sbracewell64 wants to merge 2 commits into
kunchenguid:mainfrom
sbracewell64:fm/run-attribution-rejects-unpushed-pipeline-tip
Open

fix(bin): stop reporting live runs as failed when their unpushed tip is unresolvable#1855
sbracewell64 wants to merge 2 commits into
kunchenguid:mainfrom
sbracewell64:fm/run-attribution-rejects-unpushed-pipeline-tip

Conversation

@sbracewell64

Copy link
Copy Markdown

Intent

Fix a measured false-verdict defect in the shared no-mistakes run-attribution rule, bin/fm-nm-run-lib.sh:fm_nm_head_matches_worktree, and its two consumers (bin/fm-crew-state.sh, bin/fm-teardown.sh).

RESUMING: this branch already carries commit 078c7eff, the fix round produced by THIS pipeline's own review gate on run 01KZC6Q944NDA19ET6955Q530V (review completed, 0 findings remaining). That run then died at the test step because the agent's Claude session limit was hit mid-step ("You've hit your session limit"), not because any test failed; custody was returned with no-mistakes axi sync --recover, preserving the review fixes. Re-validating the same content.

THE DEFECT (measured live 2026-08-06). A lane actively running its lint step was reported "state: failed / source: run-step / run failed". Mechanism, traced end to end: fm_nm_head_matches_worktree resolves the run head THROUGH THE WORKTREE (git -C "$wt" rev-parse --verify "${run_head}^{commit}" || return 1). During validation, no-mistakes commits its fix rounds in its own gate-repo clone and does not push until the push step, so the live run's tip is routinely NOT an object the crew worktree has (measured: worktree HEAD d4032f0, live run head 5152b3a, "Not a valid object name"). The lookup failure returned "no match", collapsing "I cannot determine this" into "this is not mine". The helper's own header documents that the descendant case MUST match because "pipeline fix commits advanced the run tip past local HEAD" - yet the implementation cannot see those commits, so the documented normal case was structurally unmatchable during every fix round. The rejected run then fell through to the coarse runs-list scan, which applies the same rule per row: the live running row was skipped and an OLDER, genuinely FAILED run sitting at the worktree's own head matched and won (COARSE_STATUS=failed, RUN_SOURCE=coarse, RUN_STATE=failed). A working lane was reported dead.

THE FIX.

  1. fm_nm_head_matches_worktree is now three-valued instead of two-valued: 0 MATCH (head resolves here and equals or descends from local HEAD), 1 NO MATCH (no head reported at all, so no identity claim to test; or the head resolves here and is a strict ancestor of, or diverged from, local HEAD), 2 UNRESOLVABLE (a head was reported but this worktree cannot answer for it - object not in reach, no readable local HEAD, or the ancestry check itself errored). The header states the full contract and the measured rationale, and states that neither caller may turn 2 into a terminal verdict. Resolution stays read-only and side-effect free: the helper never fetches to make an absent object appear. The helper remains the single owner of the rule - no second copy per consumer.
  2. bin/fm-crew-state.sh, primary attribution path: a branch-name match whose head is UNRESOLVABLE and whose run makes no terminal claim is attributed as this worktree's live run (the expected pre-push state) instead of being rejected into the coarse fallback. The review gate tightened the terminal test: nm_run_claims_terminal treats EITHER a non-empty outcome OR a terminal status word (completed/failed/cancelled) as a terminal claim, so an unresolvable head can never be reported failed/done through the no-outcome mapping.
  3. bin/fm-crew-state.sh, coarse fallback: the runs list is newest-first, so the FIRST row for the branch IS the branch's current run and any row below it can only be staler. The scan now binds that newest row and stops, accepting it on verdict 0, accepting it on verdict 2 only while it is still "running", and otherwise declining - never walking past it to let an older terminal row win.
  4. bin/fm-teardown.sh: unchanged behavior, with an explicit comment recording the direction. Both non-zero verdicts (1 and 2) DECLINE to abort, so teardown never aborts a run it cannot positively attribute. That is the safe direction and it is preserved: the previous false "no match" already failed toward declining, and verdict 2 fails the same way.
  5. Three-value discipline throughout: "cannot determine" is never coerced into PASS or FAIL - the same contract fm-verify enforces for check results, applied here to run attribution.

TESTS. New tests/fm-nm-run-lib.test.sh (7 cases) pins all three verdicts directly, including the case no existing test covered: a run head genuinely absent from the worktree, reproduced faithfully by cloning the fixture origin into a separate gate clone, committing there, and asserting the sha is unreachable from the worktree - the real unpushed-pipeline-tip shape. tests/fm-crew-state.test.sh gains 4 reader tests, two of which reproduced the measured "state: failed / source: run-step / run failed" verbatim before the fix. tests/fm-teardown.test.sh gains a test that a parked run with an unresolvable head is never aborted. Every test was witnessed red against the pre-fix logic or a mutation before green. bin/fm-test-run.sh registers the new file in the pure-contract-unit family. Test fixtures pin every git call to the test temp root after an earlier draft's empty repo path ran destructive git against the real checkout; that lesson is recorded in the firstmate-coding-guidelines skill's repo style rules, with the bash mechanism corrected by the review gate.

DOCS. docs/architecture.md's one-line description of the attribution boundary had become inaccurate, so it now describes the three-valued rule and the unknown-head behavior and points at bin/fm-nm-run-lib.sh as the owner.

CONSTRAINTS HONORED. Upstream PR 1816 owns current-run SELECTION and is deliberately not touched or duplicated; note that 1816 does NOT fix this class, because it applies the same worktree-visibility test and therefore rejects the same live rows. bin/fm-crew-state.sh is concurrently being changed by another lane (structured --json mode), so the reader change is kept minimal and confined to the attribution/fallback path so an ordinary rebase can reconcile.

KNOWN PRE-EXISTING FAILURE, NOT CAUSED BY THIS CHANGE: tests/fm-backend.test.sh fails at "not ok - old fm-teardown.sh (scout, report present) should succeed" with "fm-decision-hold: compatible tasks-axi is required". Verified via git stash to fail identically at the unmodified contribution base 345de4e - it is an environment tooling requirement, unrelated to this change. tests/fm-nm-run-lib.test.sh, tests/fm-crew-state.test.sh, tests/fm-teardown.test.sh, and bin/fm-lint.sh all pass at this head.

What Changed

  • fm_nm_head_matches_worktree in bin/fm-nm-run-lib.sh now returns a three-valued verdict — 0 match, 1 no match, 2 unresolvable — instead of collapsing "this worktree cannot resolve the run head" into "not mine". An unresolvable head is the routine mid-validation shape (no-mistakes commits fix rounds in its own gate clone and doesn't push until the push step), and the previous two-valued rule made the documented descendant case structurally unmatchable during every fix round. Resolution stays read-only: the helper never fetches to make an absent object appear.
  • bin/fm-crew-state.sh now attributes a branch-matched run with an unresolvable head as this worktree's live run whenever it makes no terminal claim (a new nm_run_claims_terminal guard treats either a non-empty outcome or a terminal status word as terminal, so verdict 2 can never surface as failed/done), and the coarse runs-list fallback binds only the newest row for the branch and stops — never walking past it to let an older finished run win, which is how a working lane got reported "state: failed / run failed". bin/fm-teardown.sh behavior is unchanged: both non-match verdicts still decline to abort, now with the direction documented, so teardown never kills a run it cannot positively attribute.
  • New tests/fm-nm-run-lib.test.sh pins all three verdicts (including a genuinely unreachable gate-clone tip), tests/fm-crew-state.test.sh gains attribution/fallback tests that reproduced the measured false "run failed" verbatim before the fix, tests/fm-teardown.test.sh gains an unresolvable-head decline test — each witnessed red against the pre-fix base per the pipeline's test step — and docs/architecture.md now describes the three-valued rule with bin/fm-nm-run-lib.sh as its owner.

Risk Assessment

✅ Low: The change keeps the attribution rule in a single owner, moves both consumers in the fail-safe direction (unknown never becomes a terminal verdict, teardown still declines), is pinned by red-then-green tests for all three verdicts including the measured incident shapes, and is byte-identical in change-owned files to the fix round already reviewed to zero findings, merely rebased onto the newer base.

Testing

Ran the three targeted suites at head (all green), witnessed the new unit and crew-state regression tests red against the pre-fix base — reproducing the measured false 'run failed' verdict verbatim — and captured a before/after transcript of the real fm-crew-state.sh CLI showing the live-lane scenario corrected from 'state: failed' to 'state: working'; teardown's decline-on-unknown behavior was additionally proven unchanged by running its suite at the base commit, and runner registration of the new test file was verified. No visual artifacts because this is a CLI/shell-library change with no rendered surface; transcripts are the end-user surface.

Evidence: Before/after fm-crew-state.sh CLI transcript (measured defect scenario)

BEFORE (base fb368dc, pre-fix): $ fm-crew-state.sh wedge state: failed · source: run-step · run failed AFTER (head d6bf74c, this change): $ fm-crew-state.sh wedge state: working · source: run-step · validating (background run)

==================================================================
BEFORE (base fb368dc, pre-fix): live lane misreported as dead
==================================================================
# worktree HEAD (short):        1cdaca7   <- older FAILED run sits here
# live running run tip (short): 5d4a4de   <- exists only in the gate clone (unpushed)
# runs list served to the reader:
  running    fm/other-crew aaaaaaa  2026-08-06 14:10
  running    fm/wedge-aging 5d4a4de  2026-08-06 14:05
  failed     fm/wedge-aging 1cdaca7  2026-08-05 09:00

$ fm-crew-state.sh wedge
state: failed · source: run-step · run failed

==================================================================
AFTER (head d6bf74c, this change): live lane attributed as working
==================================================================
# worktree HEAD (short):        cf0e6bf   <- older FAILED run sits here
# live running run tip (short): 25c167d   <- exists only in the gate clone (unpushed)
# runs list served to the reader:
  running    fm/other-crew aaaaaaa  2026-08-06 14:10
  running    fm/wedge-aging 25c167d  2026-08-06 14:05
  failed     fm/wedge-aging cf0e6bf  2026-08-05 09:00

$ fm-crew-state.sh wedge
state: working · source: run-step · validating (background run)
Evidence: Red witness: new unit tests vs pre-fix helper

not ok - live run tip committed in the gate-repo clone, not pushed yet: expected verdict 2, got 1

ok - a run head equal to the worktree HEAD matches
ok - a descendant run head the worktree can resolve matches
ok - a run head the local branch has advanced past does not match
ok - a run head on diverged history does not match
ok - a run that reported no head makes no claim and does not match
not ok - live run tip committed in the gate-repo clone, not pushed yet: expected verdict 2, got 1
Evidence: Red witness: crew-state tests vs pre-fix reader (verbatim measured defect)

not ok - an older failed row must never answer for a live run (unexpected: 'state: failed') --- output --- state: failed · source: run-step · run failed

ok - active run-step is authoritative
ok - stale needs-decision over active run is superseded
ok - stale blocked over active run is superseded
ok - genuine parked run is not flagged superseded
ok - scalar gate parked run is not flagged superseded
ok - gate block parked run is not flagged superseded
ok - ci-ready status log beats monitoring run
ok - ci-monitoring run with checks already green surfaces done
ok - top-level ci status uses ci log green marker
ok - terminal no-checks ci-monitor marker surfaces done
ok - base-advance rearm after green stays working
ok - pending no-checks ci-monitor marker stays working
ok - ci-monitoring run with checks not yet green stays working
ok - a fresh issue after an earlier green reading is not masked
ok - stale checks-green status log does not mask CI relapse
ok - ci fixing is not overridden by an earlier green marker
ok - top-level fixing is not overridden by a stale ci running row
ok - top-level fixing is not overridden by a stale done log
ok - terminal passed run is authoritative
ok - terminal failed run is authoritative
ok - cross-branch run is attributed via the real runs list
ok - cross-branch attribution picks the branch's most recent row
ok - coarse run does not probe another branch's ci log
ok - another branch's run is ignored, falls back
ok - no run + a busy semantic record reads working, attributed to its source
ok - a converted adapter never reads working from rendered footer text
ok - grok still reads working through its isolated rendered-tail fallback
ok - herdr's native busy verdict reads working with no record present
ok - a mid-tool-call crew stays working because its record outranks herdr's generation state
ok - an idle record with idle agent_status stays not-busy (no regression for a human-blocked agent)
ok - no run + idle pane uses the status-log verb
ok - no run + idle pane parses keyed status syntax
ok - no run + idle pane on a paused: status reports state: paused with its reason
ok - no run + idle pane honors the configured paused verb
ok - a trailing resolved: event does not corrupt state render (idle stays idle)
ok - dead window ignores stale status log
ok - closed pane still reports a terminal run-step
ok - closed pane still reports an active run-step
ok - no timeout command uses perl bound
ok - scout skips the run lookup
ok - torn-down worktree is handled gracefully
ok - missing meta is handled gracefully
ok - crew_is_provably_working absorbs a validating crew found only via the runs-list fallback
ok - crew_is_provably_working still surfaces a genuinely stopped crew (safety property preserved)
ok - usage error exits 2
ok - historical same-branch rewritten head is not attributed as current
ok - active run with valid descendant fix head remains current
ok - local work advanced past run head invalidates attribution
ok - missing run head falls back instead of matching by branch
not ok - an older failed row must never answer for a live run (unexpected: 'state: failed')
--- output ---
state: failed · source: run-step · run failed
Evidence: Green run: fm-nm-run-lib unit tests at head
ok - a run head equal to the worktree HEAD matches
ok - a descendant run head the worktree can resolve matches
ok - a run head the local branch has advanced past does not match
ok - a run head on diverged history does not match
ok - a run that reported no head makes no claim and does not match
ok - an unpushed pipeline tip is unresolvable, not a mismatch
ok - a head naming no reachable object is unresolvable, not a mismatch
ok - an unreadable local HEAD is unresolvable, not a mismatch
all fm-nm-run-lib tests passed
Evidence: Green run: fm-crew-state tests at head
ok - active run-step is authoritative
ok - stale needs-decision over active run is superseded
ok - stale blocked over active run is superseded
ok - genuine parked run is not flagged superseded
ok - scalar gate parked run is not flagged superseded
ok - gate block parked run is not flagged superseded
ok - ci-ready status log beats monitoring run
ok - ci-monitoring run with checks already green surfaces done
ok - top-level ci status uses ci log green marker
ok - terminal no-checks ci-monitor marker surfaces done
ok - base-advance rearm after green stays working
ok - pending no-checks ci-monitor marker stays working
ok - ci-monitoring run with checks not yet green stays working
ok - a fresh issue after an earlier green reading is not masked
ok - stale checks-green status log does not mask CI relapse
ok - ci fixing is not overridden by an earlier green marker
ok - top-level fixing is not overridden by a stale ci running row
ok - top-level fixing is not overridden by a stale done log
ok - terminal passed run is authoritative
ok - terminal failed run is authoritative
ok - cross-branch run is attributed via the real runs list
ok - cross-branch attribution picks the branch's most recent row
ok - coarse run does not probe another branch's ci log
ok - another branch's run is ignored, falls back
ok - no run + a busy semantic record reads working, attributed to its source
ok - a converted adapter never reads working from rendered footer text
ok - grok still reads working through its isolated rendered-tail fallback
ok - herdr's native busy verdict reads working with no record present
ok - a mid-tool-call crew stays working because its record outranks herdr's generation state
ok - an idle record with idle agent_status stays not-busy (no regression for a human-blocked agent)
ok - no run + idle pane uses the status-log verb
ok - no run + idle pane parses keyed status syntax
ok - no run + idle pane on a paused: status reports state: paused with its reason
ok - no run + idle pane honors the configured paused verb
ok - a trailing resolved: event does not corrupt state render (idle stays idle)
ok - dead window ignores stale status log
ok - closed pane still reports a terminal run-step
ok - closed pane still reports an active run-step
ok - no timeout command uses perl bound
ok - scout skips the run lookup
ok - torn-down worktree is handled gracefully
ok - missing meta is handled gracefully
ok - crew_is_provably_working absorbs a validating crew found only via the runs-list fallback
ok - crew_is_provably_working still surfaces a genuinely stopped crew (safety property preserved)
ok - usage error exits 2
ok - historical same-branch rewritten head is not attributed as current
ok - active run with valid descendant fix head remains current
ok - local work advanced past run head invalidates attribution
ok - missing run head falls back instead of matching by branch
ok - an unresolvable live run tip never loses attribution to a stale failed row
ok - an active run on this branch with an unresolvable tip is attributed, not rejected
ok - a terminal run whose head cannot be bound never becomes a terminal verdict
ok - a terminal status with no outcome never bypasses the unresolvable-head guard
ok - an unbindable finished coarse row is not attributed either
all fm-crew-state tests passed
Evidence: Green run: fm-teardown tests at head
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
ok - teardown prompts tasks-axi backlog refresh when compatible
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression)
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
ok - teardown completes when an exact busy-state sidecar is already absent
ok - herdr teardown removes pane-owned escalation dedupe state
ok - herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly
ok - herdr flat teardown never erases records when pane presence is unparseable
ok - herdr flat teardown preflight refuses before every destructive change
ok - forced secondmate teardown preflights every Herdr child before cleanup mutation
ok - forced secondmate teardown retains Herdr child identity until exact pane disappearance
ok - forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed
ok - herdr projection teardown retires its journal only after confirming the exact recorded pane is gone
ok - herdr projection teardown retains every record when post-close presence is unknown
ok - herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure
ok - squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)
ok - squash-merged PR accepts a local HEAD that is an ancestor of the final PR head
ok - teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded
ok - squash-merged PR accepts replayed unpushed local patches contained in the PR head
ok - merged PR does not allow teardown after a later local commit
ok - fm-pr-check does not refresh PR head after HEAD moves
ok - fm-pr-check records the remote PR head when the local worktree lags
ok - worktree whose content already landed in the default branch is torn down (content fallback)
ok - content fallback refreshes origin default before comparing trees
ok - dirty worktree is refused even when its committed work has landed (dirty always wins)
ok - gh lookup error with content not in default refuses (fail-safe)
ok - provably-stale worktree index.lock (old, no live holder) is cleared and teardown succeeds
ok - live-held worktree index.lock is never removed and teardown refuses
ok - lsof errors leave worktree index.lock in place and refuse teardown
ok - stale lock cleanup rechecks and refuses dirty worktree before return
ok - normal repo index.lock is resolved from the worktree and cleared when stale
ok - lock mtime read failures leave worktree index.lock in place and refuse teardown
ok - transient index.lock cleared after first failed return is retried successfully without force-remove
ok - persistent index.lock exhausts retries and refuses without force-removing the lock
ok - empty retry wait overrides use the default without aborting teardown
ok - fractional legacy retry wait remains supported without arithmetic
ok - a task's own parked no-mistakes run is aborted, not orphaned, before the worker is removed
ok - teardown refuses before reap or removal when a task-owned run remains parked
ok - a different run cannot confirm the targeted abort
ok - empty post-abort status is not accepted as confirmation
ok - the CLI's exact run-not-found signal confirms completion
ok - a parked run on another branch is never aborted by this task's teardown (ownership is precise)
ok - a parked run whose head cannot be resolved is never aborted (unknown declines, it does not guess)
ok - a task-owned autonomous running step is left alone rather than aborted
ok - a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving
ok - a leaked descendant process rooted under the task's per-task tasktmp is reaped by teardown too
ok - missing lsof falls back to reaping the tmux pane process group
ok - an erroring lsof scan refuses teardown and preserves the task
ok - a reused pid with a different start time is never force-killed
ok - an exec change preserves birth identity and the process is reaped
ok - a process spawned during grace is reaped on a later pass
ok - persistent leaked processes refuse teardown after bounded retries
ok - a process exiting during identity lookup does not block teardown
ok - the run abort and the leaked-process reap both complete before the destructive worktree return
Evidence: Teardown suite at pre-fix base (proves decline-on-unknown unchanged)
ok - local-only worktree with HEAD on a fork remote is torn down (fix holds)
ok - teardown prompts tasks-axi backlog refresh when compatible
ok - teardown honors config/backlog-backend=manual even when tasks-axi is compatible
ok - local-only worktree with truly unpushed work is refused (safety preserved)
ok - local-only worktree with work merged into local main is torn down (no regression)
ok - no-mistakes worktree with HEAD on origin is torn down (no regression)
ok - no-mistakes worktree with genuinely unlanded work is refused (safety preserved)
ok - local-only worktree with unpushed work is torn down under --force (escape hatch)
ok - teardown completes when an exact busy-state sidecar is already absent
ok - herdr teardown removes pane-owned escalation dedupe state
ok - herdr flat teardown refuses before returning the isolated copy under lock contention and the retry completes cleanly
ok - herdr flat teardown never erases records when pane presence is unparseable
ok - herdr flat teardown preflight refuses before every destructive change
ok - forced secondmate teardown preflights every Herdr child before cleanup mutation
ok - forced secondmate teardown retains Herdr child identity until exact pane disappearance
ok - forced teardown retains a nested secondmate home and its grandchild's Herdr identity when the grandchild close is unconfirmed
ok - herdr projection teardown retires its journal only after confirming the exact recorded pane is gone
ok - herdr projection teardown retains every record when post-close presence is unknown
ok - herdr projection teardown surfaces failed focus restoration without turning confirmed cleanup into a hard failure
ok - squash-merged + deleted-branch worktree (PR merged) is torn down (the fix)
ok - squash-merged PR accepts a local HEAD that is an ancestor of the final PR head
ok - teardown discovers a merged PR by branch name and tears down when no pr= was ever recorded
ok - squash-merged PR accepts replayed unpushed local patches contained in the PR head
ok - merged PR does not allow teardown after a later local commit
ok - fm-pr-check does not refresh PR head after HEAD moves
ok - fm-pr-check records the remote PR head when the local worktree lags
ok - worktree whose content already landed in the default branch is torn down (content fallback)
ok - content fallback refreshes origin default before comparing trees
ok - dirty worktree is refused even when its committed work has landed (dirty always wins)
ok - gh lookup error with content not in default refuses (fail-safe)
ok - provably-stale worktree index.lock (old, no live holder) is cleared and teardown succeeds
ok - live-held worktree index.lock is never removed and teardown refuses
ok - lsof errors leave worktree index.lock in place and refuse teardown
ok - stale lock cleanup rechecks and refuses dirty worktree before return
ok - normal repo index.lock is resolved from the worktree and cleared when stale
ok - lock mtime read failures leave worktree index.lock in place and refuse teardown
ok - transient index.lock cleared after first failed return is retried successfully without force-remove
ok - persistent index.lock exhausts retries and refuses without force-removing the lock
ok - empty retry wait overrides use the default without aborting teardown
ok - fractional legacy retry wait remains supported without arithmetic
ok - a task's own parked no-mistakes run is aborted, not orphaned, before the worker is removed
ok - teardown refuses before reap or removal when a task-owned run remains parked
ok - a different run cannot confirm the targeted abort
ok - empty post-abort status is not accepted as confirmation
ok - the CLI's exact run-not-found signal confirms completion
ok - a parked run on another branch is never aborted by this task's teardown (ownership is precise)
ok - a parked run whose head cannot be resolved is never aborted (unknown declines, it does not guess)
ok - a task-owned autonomous running step is left alone rather than aborted
ok - a leaked descendant process rooted under the task's worktree is reaped by teardown, not left surviving
ok - a leaked descendant process rooted under the task's per-task tasktmp is reaped by teardown too
ok - missing lsof falls back to reaping the tmux pane process group
ok - an erroring lsof scan refuses teardown and preserves the task
ok - a reused pid with a different start time is never force-killed
ok - an exec change preserves birth identity and the process is reaped
ok - a process spawned during grace is reaped on a later pass
ok - persistent leaked processes refuse teardown after bounded retries
ok - a process exiting during identity lookup does not block teardown
ok - the run abort and the leaked-process reap both complete before the destructive worktree return
Evidence: Reproducible evidence harness script (before/after scenario)
#!/usr/bin/env bash
# Evidence harness: reproduce the measured 2026-08-06 attribution scenario
# against a given firstmate tree and print the REAL bin/fm-crew-state.sh output.
#
# Scenario (as measured live): a lane mid-validation has a LIVE running
# no-mistakes run whose tip exists only in no-mistakes' own gate-repo clone
# (not pushed yet, so unresolvable from the crew worktree), plus an OLDER,
# genuinely FAILED run still sitting at the worktree's own head. The repo-wide
# `axi status` answer belongs to another crew, so attribution goes through the
# coarse runs-list fallback.
#
# Usage: nm-evidence-scenario.sh <firstmate-tree>
set -u
TREE=$1
# shellcheck source=/dev/null
. "$TREE/tests/lib.sh"

TMP_ROOT=$(fm_test_tmproot nm-evidence)
fm_git_identity fmtest fmtest@example.invalid

d="$TMP_ROOT/case"
mkdir -p "$d/state"

# Crew worktree on its branch.
wt="$d/wt"
mkdir -p "$wt"
git -C "$wt" init -q
git -C "$wt" commit -q --allow-empty -m init
git -C "$wt" checkout -q -b fm/wedge-aging
short=$(git -C "$wt" rev-parse --short=7 HEAD)

# The live run's tip: a real descendant commit made in a SEPARATE clone
# (no-mistakes' gate repo), genuinely absent from the worktree's object store.
gate="$d/gate-clone"
git clone -q "$wt" "$gate" 2>/dev/null
git -C "$gate" commit -q --allow-empty -m 'pipeline fix commit'
tip=$(git -C "$gate" rev-parse HEAD)
git -C "$wt" cat-file -e "${tip}^{commit}" 2>/dev/null && {
  echo "fixture not honest: tip reachable from worktree" >&2; exit 1; }

# Fake no-mistakes / tmux / herdr on PATH (same fakes the behavior tests use).
fb="$d/fakebin"; mkdir -p "$fb"
cat > "$fb/no-mistakes" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  axi)
    shift
    case "${1:-}" in
      status)
        shift
        if [ "${1:-}" = --run ]; then printf '%s\n' "${FM_FAKE_AXI_STATUS_RUN:-}"
        else printf '%s\n' "${FM_FAKE_AXI_STATUS:-}"; fi ;;
      logs)
        printf '%s\n' "${FM_FAKE_CI_LOGS:-}" ;;
    esac
    ;;
  runs)
    printf '%s\n' "${FM_FAKE_RUNS_LIST:-}" ;;
esac
exit 0
SH
cat > "$fb/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "${1:-}" in
  display-message) printf '%%1\n' ;;
  capture-pane) printf 'all quiet\n> \n' ;;
esac
exit 0
SH
cat > "$fb/herdr" <<'SH'
#!/usr/bin/env bash
exit 1
SH
chmod +x "$fb/no-mistakes" "$fb/tmux" "$fb/herdr"

fm_write_meta "$d/state/wedge.meta" "window=fm:fm-wedge" "worktree=$wt" "kind=ship" "harness=claude"
printf 'working: implementation committed, validating\n' > "$d/state/wedge.status"

# Repo-wide axi status answers for ANOTHER crew, forcing the coarse fallback.
FM_FAKE_AXI_STATUS="run:
  id: \"01RUN\"
  branch: fm/other-crew
  status: running
  head: \"aaaaaaa\"
  pr: \"\"
  findings: none
  steps[2]{step,status,findings,duration_ms}:
    intent,completed,0,0
    review,running,0,0"
FM_FAKE_RUNS_LIST="  running    fm/other-crew aaaaaaa  2026-08-06 14:10
  running    fm/wedge-aging ${tip:0:7}  2026-08-06 14:05
  failed     fm/wedge-aging ${short}  2026-08-05 09:00"
FM_FAKE_BUSY=0
export FM_FAKE_AXI_STATUS FM_FAKE_RUNS_LIST FM_FAKE_BUSY

gen=$("$TREE/bin/fm-busy-event.sh" arm "$d/state" wedge)
"$TREE/bin/fm-busy-event.sh" apply "$d/state" wedge idle --gen "$gen" \
  --source claude-hook --event stop >/dev/null

echo "# worktree HEAD (short):        $short   <- older FAILED run sits here"
echo "# live running run tip (short): ${tip:0:7}   <- exists only in the gate clone (unpushed)"
echo "# runs list served to the reader:"
printf '%s\n' "$FM_FAKE_RUNS_LIST"
echo
echo "\$ fm-crew-state.sh wedge"
PATH="$fb:$PATH" FM_STATE_OVERRIDE="$d/state" "$TREE/bin/fm-crew-state.sh" wedge

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ bin/fm-nm-run-lib.sh:88 - The helper's contract header says "Neither caller may turn 2 into a terminal verdict: fm-crew-state.sh reports working/validating or unknown", but a verdict-2-attributed run in fm-crew-state.sh can also legitimately be reported parked (gate present with non-terminal status — the routine mid-fix-round shape) or done via the ci-checks-green log override (status=ci after push while the worktree has not fetched the pushed tip). Both downstream states are correct product behavior (parked is non-terminal; checks-green is PR/branch-scoped, not object-store-scoped), so this is a comment-precision note only — the guard that matters (nm_run_claims_terminal blocking the failed/done no-outcome mapping) is correctly in place and tested.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-nm-run-lib.test.sh (8/8 green at head, all three verdicts incl. unpushed gate-clone tip)
  • bash tests/fm-crew-state.test.sh (all green at head incl. 5 new attribution/fallback tests)
  • bash tests/fm-teardown.test.sh (58 green at head incl. new unresolvable-head decline test)
  • bin/fm-test-run.sh tests/fm-nm-run-lib.test.sh (runner accepts file, family=pure-contract-unit)
  • red witness: bash tests/fm-nm-run-lib.test.sh against base fb368dc via git-archive tree (fails 'expected verdict 2, got 1' on the unpushed-tip case)
  • red witness: bash tests/fm-crew-state.test.sh against base fb368dc (fails dumping the verbatim measured defect 'state: failed · source: run-step · run failed')
  • unchanged-behavior check: bash tests/fm-teardown.test.sh against base fb368dc (new decline test passes pre-fix too, exit 0)
  • manual scenario harness driving the real bin/fm-crew-state.sh with unpushed gate-clone tip + stale failed row, run against both base and head trees
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

`fm_nm_head_matches_worktree` resolved the run head THROUGH THE WORKTREE and
treated a failed lookup as "no match". During validation no-mistakes commits
its fix rounds in its own gate-repo clone and does not push until the push
step, so the live run's tip is routinely an object the crew's worktree has
never seen. Measured 2026-08-06 on a lane actively running its lint step:
worktree HEAD d4032f0, live run head 5152b3a, `git cat-file -t 5152b3a` in
the worktree "Not a valid object name". The helper's own header said the
descendant case MUST match because pipeline fix commits advance the run tip
past local HEAD - the implementation could not see those commits, so the
documented normal case was structurally unmatchable during every fix round.

The rejected run then fell through to the coarse runs-list scan, which applied
the same rule per row: the live running row was skipped as unresolvable and an
OLDER, genuinely failed run sitting at the worktree's own head matched and won.
The answer was not "unknown" but confidently wrong, in the direction that makes
a working lane look dead: `state: failed - source: run-step - run failed`.

The rule is now three-valued - match, no match, unresolvable - and stays the
single owner shared by both consumers:

- fm-crew-state.sh: an active run on this crew's own branch whose tip cannot
  be resolved is attributed and reads as working, since bare `axi status`
  answers for the queried branch whenever that branch has a run. A TERMINAL run
  whose head cannot be bound is not attributed at all, because binding its head
  was the only thing that could tie its verdict to this worktree. The coarse
  scan now binds the branch's newest row and stops, rather than walking past
  the live row to an older one whose sha happens to match; an unresolvable
  newest row answers only "a run is active on this branch", never a terminal
  status.
- fm-teardown.sh: the direction is unchanged and deliberate. Only a positive
  code-identity MATCH authorizes aborting a parked run; both non-zero verdicts
  DECLINE, so teardown never aborts a run it cannot positively attribute. The
  cost of declining is a run left parked for firstmate to see; the cost of
  guessing is killing another crew's live validation.

Upstream PR 1816 does not fix this class: it owns current-run selection and
applies the same visibility test, so it rejects the same live rows.

Tests: new tests/fm-nm-run-lib.test.sh pins all three verdicts, including a
descendant commit created in a SEPARATE clone so it is genuinely absent from
the worktree's object store - the condition that produced this defect and the
one no existing test covered. Four reader cases and one teardown case cover the
consumers. Every case was witnessed red against the pre-fix logic or a targeted
mutation before green; the two coarse/reader regressions reproduce the measured
`state: failed - source: run-step - run failed` verbatim.

The test fixture rules added to firstmate-coding-guidelines are not incidental:
an earlier draft of these fixtures resolved a repo path to empty and ran
`git -C "" reset --hard` against the checkout the tests live in.
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