Let scout cleanup finish after a captain decision is answered and archived - #90
Merged
Merged
Conversation
…acklog The shared completion gate that scout teardown runs (fm-decision-hold verify, and complete) failed once a captain decision had been answered, marked Done, and then dropped from the backlog by Done-history retention. verify_hold_durable treats a missing hold as an error, so an origin whose decision was genuinely answered could no longer clear the gate and its investigation could not be cleaned up. Add verify_reviewed_hold: for a reviewed key, a hold absent from the backlog passes only on positive answer evidence. That evidence is an explicit resolved or captain-held line in the status log that closed the exact key, exposed by a new status_key_answered helper in fm-classify-lib.sh. Absence of the key from the open set is not proof of an answer, so a reviewed key that is still open, or that never appeared in the status log at all, keeps failing when its hold is gone. status_key_answered reuses the same resolve and captain-held verbs, key extraction, and reserved-namespace guard the open-decisions fold uses, so the two never disagree on what counts as answered. Open holds are unaffected: they are never absent, so their path is unchanged. Tests: a lifecycle regression proving an archived answered hold passes verify and teardown while an open hold and a hold with no answer line both still fail, plus unit tests for status_key_answered covering resolved, captain-held, colon-first positions, still-open, never-seen, missing-file, wrong-key, and the reserved-key guard. Also fixed a latent set -e leak in this test file: three blocks turned on set -e to capture a nonzero exit but never restored it under the file's set -u base mode, so it leaked forward and aborted the last-invoked test.
…sh verification evidence
… closed Drop the forgeable status-log answer route from the archival tolerance. A worker writes its own status lines (fm-brief.sh tells a crewmate or scout to append its own 'resolved [key=<slug>]: ...' line when a blocker clears without a firstmate reply), so a 'resolved [key=X]: answered: ...' line is forgeable by the very agent whose source deletion this gate guards. key_was_answered now accepts an absent hold only when the key is recorded in the origin metadata's answered_keys, which only a real close path (resolve, answer, decline, repair) writes and which survives Done-history retention because it lives beside decision_keys, not in the trimmed backlog body. Also in this pass: - key_was_answered reads the raw status_open_decisions fold for its re-open guard, deliberately not origin_open_decisions: the latter suppresses the open set once the origin's last status line is done/failed (the normal teardown state), which would let a stale round-one answer wave an unanswered re-opened round through. The comment now states this intent instead of claiming the two folds are the same. - backlog_is_intact resolves the backlog file from FM_HOME/.tasks.toml the same way tasks-axi does, instead of a hardcoded data/backlog.md, so the intactness check and the not-found verdict are decided from the one file tasks-axi reads. - The close paths (answer/decline via close_unrouted_hold, and resolve) record the durable answered_keys proof before the irreversible tasks-axi done, so a metadata-write failure aborts before the close rather than reporting an already-closed hold as not closed. - record_answered_key creates the origin meta when absent instead of silently skipping the record, so a close performed before the meta exists still leaves durable proof. Tests: a forged 'answered:' status line with no close-path record must fail the gate, alongside the self-close negative; the real completion-then-answer ordering through the actual scripts records answered_keys and passes after archival. The banked headings-only-wipe residual is tracked in issue #87.
…chival Extend the chat-channel test so it does not stop at the pre-archival gate. After the real fm-send --resolve-key answer routes through the hold-close path, assert the origin's answered_keys durably records the key, then trim the hold out of the backlog and confirm verify still clears the gate on that durable record alone. This exercises the production ordering end to end through the actual scripts (hold, complete, fm-send answer, retention trim, verify), not a hand-written answer marker, so the archival tolerance is proven reachable on the flow it was written for.
…r, match tasks-axi backlog path
EvanAgee
added a commit
that referenced
this pull request
Sep 21, 2026
… backlog The completion gate fails once an answered captain-held task is dropped from the backlog by Done-history retention, because verify_hold_durable treats the missing row as an error, so a reviewed investigation could no longer be cleaned up. A close now records the answered entry in the origin's metadata under the same lock command_complete uses, and verify tolerates a not-found entry only when that durable record names it. An unreadable backend, a read-bound overrun, and a re-opened status decision all still fail, so the tolerance never waves an unresolved call through. This re-implements the fork's commit 1ff361b (#90) on the root's fm-captain-hold.sh, which replaced the fork's fm-decision-hold.sh and records the origin in a hold's body and its reviewed inventory. Refs #90
EvanAgee
added a commit
that referenced
this pull request
Sep 21, 2026
…ct map, and handover Seed docs/specs/upstream-rebase.md from the fork's main, because the spec did not exist on c/main, then add the sections this ticket calls for: - A conflict map assigning all 138 conflicting files from `git merge-tree --write-tree c/main origin/main` to the C1 to C8 ticket that owns each one, with the six cross-cutting files that fit no ticket and the per-ticket totals. - A running-the-suite section with the exact runner and CI-lane commands and the observed baseline from a full `bin/fm-test-run.sh --all` on this Mac: 219 scripts, 18 failures, 28 gate skips, 4h07m under a machine load near 40, with every failure re-run alone and classified as root defect, missing tool, this-machine environment, or load. - The trial-home handover: the exact remote, checkout, state-archive, and session-start steps firstmate runs at /Users/evanagee/Sites/firstmate-trial. - The outcome of the four rideable fixes: #71, #90, and the calm half of #86 ported; #77 dropped because the root already carries it. Refs #77 Refs #71 Refs #90 Refs #86
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
The completion check that guards scout cleanup (
bin/fm-decision-hold.sh, itsverifyandcompletepaths) used to fail once a captain decision had been answered, marked done, and then aged out of the backlog by Done-history retention. Because the hold was gone,verifytreated a genuinely answered decision as unverifiable and refused, so the investigation could never be cleaned up.The change makes
verifytolerate that archived-but-answered case without ever waving through a decision the captain did not answer.The rule the gate now enforces
An absent hold passes only when all of these hold:
resolve,answer,decline, orrepair), recorded durably in the origin's metadata asanswered_keys. This record lives besidedecision_keys, not in the trimmed backlog body, so it survives retention.decision_keys, whichcompletewrites only after verifying the hold existed). A never-registered key still fails.The status-log answer route was removed. A worker writes its own status lines (
bin/fm-brief.shtells a crewmate or scout to append its ownresolved [key=...]line when a blocker clears without a firstmate reply), so aresolved [key=X]: answered: ...line is forgeable by the very agent whose source deletion this gate guards. The only positive proof is now the metadata record that only a real close path writes.Supporting fixes in the same branch: the intactness check resolves the backlog path the way tasks-axi does; the durable answer is recorded before the irreversible close so a metadata-write failure never reports an already-closed hold as not closed; and the record step no longer resurrects a torn-down origin's metadata.
Validation
All four decision-hold suites pass by hand on this head (
932e8ab3):fm-classify-decision-key— 29 okfm-wake-drain-open-decisions— okfm-wake-drain-open-decisions-cursor— okfm-decision-hold-lifecycle— 32 ok, including the archived-answered case, the never-registered refusal, the broken-backlog refusals, the forged-marker refusal, the re-asked-key refusal, and the real fm-send chat-answer flow driven end to end into the metadata record and through archivalbin/fm-lint.shis clean.Why the automated pipeline was skipped
This branch cleared the pipeline's review, test, and document steps across earlier rounds. It was not merged through the pipeline because the review-fix agent hit three consecutive 30-minute infrastructure timeouts (the agent went silent and was killed each time). That is an infrastructure failure, not a verdict on the code. After the third timeout the captain authorized shipping on the passing steps via a direct push and PR, the same fallback used for firstmate PR #75.
Known-open item, deferred by decision
One answer-proof hole is not closed on this head and is called out plainly so it is not mistaken for fixed:
A worker can defeat the re-open guard with its own self-close. Chain: the captain answers round one (records
answered_keys); the worker re-asks the decision (needs-decisionfor the same key); the worker then writes its own bareresolved [key=X]: ...line, which closes the key in the open-decision fold; the re-open guard checks "is the key open now," sees "no," finds the stale round-one record, and passes an unanswered round two.The fix was approved (clear the key from
answered_keyswhencompletere-inventories a re-opened key, plus a RED-then-green regression test that reproduces the bypass through the real scripts). It is not in this PR because it was the exact fix round that hit the third pipeline timeout, so it never committed. The existing re-open test covers only the case where round two is left open, which the current guard already catches; it does not cover the self-close bypass. This should be the first follow-up on this code.Also tracked as follow-ups
pathkeys, a#inside a quoted path). Approved fix, not yet applied.Authority
Merge stays firstmate-gated. This PR was opened by direct push under explicit captain authorization after the three pipeline timeouts; it is not a claim that the automated gate passed.