Skip to content

Let scout cleanup finish after a captain decision is answered and archived - #90

Merged
EvanAgee merged 16 commits into
mainfrom
fm/fm-decision-hold-verify-tolerates-archived
Sep 1, 2026
Merged

EvanAgee merged 16 commits into
mainfrom
fm/fm-decision-hold-verify-tolerates-archived

Conversation

@EvanAgee

@EvanAgee EvanAgee commented Sep 1, 2026

Copy link
Copy Markdown
Owner

What this fixes

The completion check that guards scout cleanup (bin/fm-decision-hold.sh, its verify and complete paths) 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, verify treated a genuinely answered decision as unverifiable and refused, so the investigation could never be cleaned up.

The change makes verify tolerate 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:

  • The captain answered it through a real close path (resolve, answer, decline, or repair), recorded durably in the origin's metadata as answered_keys. This record lives beside decision_keys, not in the trimmed backlog body, so it survives retention.
  • The decision was durably held before (its key is in decision_keys, which complete writes only after verifying the hold existed). A never-registered key still fails.
  • The backlog file is present, non-empty, and structurally a backlog. A missing, empty, or unstructured backlog fails loudly instead of being read as a trimmed hold.
  • The key is not currently open (a decision re-asked in a later round is not treated as answered by an earlier round's record).

The status-log answer route was removed. A worker writes its own status lines (bin/fm-brief.sh tells a crewmate or scout to append its own resolved [key=...] 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. 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 ok
  • fm-wake-drain-open-decisions — ok
  • fm-wake-drain-open-decisions-cursor — ok
  • fm-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 archival

bin/fm-lint.sh is 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-decision for the same key); the worker then writes its own bare resolved [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_keys when complete re-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

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.

…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.
… 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.
@EvanAgee
EvanAgee merged commit 1ff361b into main Sep 1, 2026
16 of 18 checks passed
@EvanAgee
EvanAgee deleted the fm/fm-decision-hold-verify-tolerates-archived branch September 1, 2026 17:23
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
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