Skip to content

fix(bin): resolve captain-call records from the done archive as well as the backlog - #2759

Open
Inthuson wants to merge 12 commits into
kunchenguid:mainfrom
Inthuson:fm/decision-gate-retention-prune-g9
Open

fix(bin): resolve captain-call records from the done archive as well as the backlog#2759
Inthuson wants to merge 12 commits into
kunchenguid:mainfrom
Inthuson:fm/decision-gate-retention-prune-g9

Conversation

@Inthuson

@Inthuson Inthuson commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Intent

Make the unresolved-decision completion gate look in the archive as well as the live backlog, so an investigation whose captain decisions were all properly answered can actually be cleaned up.

The defect: bin/fm-decision-hold.sh verify searched only data/backlog.md. Backlog retention MOVES answered decision records into data/done-archive.md, so verify reported a safely archived decision as "absent" and the originating investigation could never pass its gate.

What is explicitly NOT wrong, and must not be "fixed": nothing is being lost. Retention archives, it does not destroy. Do not add any preservation mechanism, do not change retention, and do not raise the retention count, because that only delays the same failure.

The fix: treat data/done-archive.md as an equally valid location for a decision record. Check the sibling subcommands before deciding the scope of the change: resolve, answer, decline and repair may share the same backlog-only lookup, and if they do they need the same treatment; if they genuinely should not, say why rather than leaving it implicit.

Do not weaken the gate. A key in NEITHER file must still fail loudly. A key recorded but genuinely unanswered must still fail. Only a key that is recorded AND answered, in either file, may pass. The failure message must distinguish those cases: the old wording said "absent", which sent the reader looking for a missing decision instead of a file-scope bug and cost real time.

Tests must cover all three states, cover the archived-but-answered case explicitly, extend the existing colocated test pattern, and assert on the distinguishing message text. Do not modify the live data/backlog.md or data/done-archive.md to make a test pass; build test fixtures instead. No em-dashes anywhere, including the commit message and PR body.

Decisions taken during this work, so they do not read as accidents in the diff:

The captain-hold surface had already collapsed decisions into ordinary backlog tasks, so the work landed on bin/fm-captain-hold.sh and bin/fm-tasks-axi-lib.sh with bin/fm-decision-hold.sh remaining a thin shim. The sibling check was done: the two-file read is now owned once by fm_tasks_axi_record_show in bin/fm-tasks-axi-lib.sh and every sibling reads through it, rather than each composing its own lookup. The one deliberate exception is diverged, whose subject is by definition a still-open task the archive cannot hold.

The archive read is strictly read-only. Every tasks-axi mutation still targets the active backlog alone, so an archived record can be reported on and replayed but never rewritten or re-minted.

Archive-read failures are kept apart from "no record" on purpose. An archive that exists and cannot be read might well hold the record, so it refuses in its own words; only a genuinely absent record is quiet, because a home that has never had a row trimmed has no archive file at all and that is the healthy normal state.

Two findings were raised against my own earlier work in this branch and both were ruled on, so the current diff is the corrected version rather than the first attempt.

First, this branch's own path resolver diverged from tasks-axi in both directions. It unwrapped a quoted ["markdown"] table key, which tasks-axi ignores: under that spelling retention writes the fallback archive while the read resolved the configured one, reintroducing the exact false "absent" for one config spelling. The ruling was to drop the unwrap and accept the plain form only, and to correct the header comment, which had stated the rule inverted. Two alternatives were rejected: keeping the unwrap with a caveat comment was rejected because a silent path back to the false absent is unacceptable regardless of how rare the config form is, and making the resolver authoritative over tasks-axi was rejected outright. Verifying past that finding showed the divergence was wider: the value side matched only a double-quoted string, so a single-quoted TOML literal string, which tasks-axi honors, fell back to a default naming a file that does not exist, and the value was taken from the first double-quoted run anywhere on the line, so a trailing comment could be mistaken for the path. Both are fixed here. All forms were measured empirically against 0.2.4, the FM_TASKS_AXI_MIN floor, and 0.2.5 rather than reasoned from the TOML spec; the measurement also showed tasks-axi rejects an [[markdown]] config outright, so that header needs no carve-out and behaviour was deliberately left unchanged there.

Second, one existing assertion in this branch was vacuous: it claimed to prove the missing-archive-file boundary while the record was still in the live backlog, so the backlog answered first, the archive read never ran, and the assertion passed whatever that read did. That was ruled a defect worse than no test, because it reported coverage that did not exist on the exact boundary the change was written to protect. It now drives the case where the archive genuinely answers, by taking the archive file away once retention has moved the only copy of the record into it.

Both fixes were verified non-vacuous by mutation rather than by inspection: restoring the quoted-key unwrap fails the new mirror test with the false-absent message, and reporting an absent archive as unreadable fails the repaired assertion on its own line.

A separate pre-existing discrepancy was found and deliberately NOT touched: this read's fallback defaults are data/backlog.md and data/done-archive.md, which are what the tracked .tasks.toml pins, while tasks-axi's own unconfigured defaults can differ. That is being filed separately and must not be pulled into this change.

Two local test suites fail in this sandbox for reasons unrelated to this change, confirmed by reproducing both on a clean checkout of the base commit: tests/fm-on.test.sh is flaky here and also asserts a mise shim ordering this machine does not produce, and tests/fm-remote-backlog-handoff.test.sh fails because its fake remote has no compatible tasks-axi for the atomic receipt. Neither touches this change's surface.

What Changed

  • bin/fm-tasks-axi-lib.sh gains the shared, strictly read-only two-file record lookup: fm_tasks_axi_record_show tries the active backlog first and then the configured archive, resolving both paths from the home's own [markdown] config table (accepting the key and value forms tasks-axi itself honors, falling back to data/backlog.md and data/done-archive.md), staging one parser-legible archive view per process keyed on path and byte size and removing it through a chained EXIT trap. Found-in-backlog, found-in-archive, no record in either file, an archive copy that cannot be parsed, one that cannot be staged, and an archive path that cannot be read are returned as six distinct statuses so no failure can degrade into "absent".
  • bin/fm-captain-hold.sh now reads every durable captain-call record through that helper, so hold, answer, answers, complete, and verify accept an archived record, and the complete origin-ownership check falls back to the archive too. The gate is not loosened: the three failing inventory shapes are worded apart (no record in either file, recorded but closed with no recorded answer, recorded but not held for the captain), each archive-read failure refuses in its own words rather than falling through to the legacy derived identity, hold refuses to mint a second row for an id the archive already carries, and verify reports the archived count on a pass. Writes stay on the live backlog: an archived record takes an exact answer replay but refuses a drifted answer and --release. diverged's open-task scan and fm-send.sh's answerable-candidate scan stay backlog-only with the reason recorded in comments, since retention moves only closed rows.
  • bin/fm-decision-hold.sh reads its own resolve lookups through the same helper and skips blocked-by edge clearing for an archived routed task. tests/fm-captain-hold-lifecycle.test.sh adds 12 colocated cases covering the archived-but-answered pass, all three still-failing states with assertions on the distinguishing message text, each archive-read refusal, staged-view cleanup, a row archived mid batch, legacy-identity precedence, and the config spellings that locate or fall back from the archive, with every fixture built by running tasks-axi prune in a throwaway home. docs/captain-hold-lifecycle.md, docs/configuration.md, and docs/scripts.md document the read scope and the config keys it resolves.

Risk Assessment

✅ Low: Every source-verifiable intent criterion is met, all six archive-read outcomes are fail-closed with no path that reports an existing or possibly-existing record as absent, the round-4 refactor is provably behavior-preserving, and the test file changed by addition only so no prior assertion was weakened; the single remaining finding is a documentation precision gap in the verification record.

Testing

Ran the colocated captain-hold lifecycle suite (29 tests, all green) after fixing a local setup gap where the suite silently skipped because tasks-axi was only available through an unpinned mise install, then demonstrated the intent at the product surface with an operator-level CLI transcript run against both the base and fixed trees: the base refuses an answered-then-archived captain call with the old "absent from data/backlog.md" wording and blocks teardown forever, while the fix passes with "(1 answered and archived)" on both command spellings and lets cleanup finish. Companion transcripts show the gate is not weakened (no record, recorded-but-unanswered, and recorded-and-answered each behave distinctly and name the files searched), that the archive read is strictly read-only across answer, answers, hold and the shim's resolve with byte-identical files, and that an unreadable archive refuses loudly instead of reading as absent. Mutation runs confirm the new assertions are load-bearing, including reverting to the backlog-only lookup to reproduce the original defect, and four adjacent suites touching the changed library and docs also pass; source was restored and the worktree is clean. This change has no UI surface, so the reviewer-visible evidence is CLI transcripts rather than screenshots; only tasks-axi 0.2.5 is installed here, so the 0.2.4-floor half of the author's config measurements was not re-measured.

Evidence: Defect and fix side by side (before/after CLI transcript)

BEFORE (base 59e7393) $ fm-decision-hold.sh verify sample-retention-review fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/data/backlog.md fm-captain-hold: captain-held task is absent from <home>/data/backlog.md [exit 1] $ fm-teardown.sh sample-retention-review REFUSED: scout task sample-retention-review has not passed the captain-call completion gate. [exit 1] the finished investigation can never be cleaned up AFTER (eca3f52) $ fm-decision-hold.sh verify sample-retention-review verified: sample-retention-review captain-call inventory (1 answered and archived) [exit 0] $ fm-captain-hold.sh verify sample-retention-review verified: sample-retention-review captain-call inventory (1 answered and archived) [exit 0] $ fm-teardown.sh sample-retention-review teardown sample-retention-review complete (...) [exit 0] the investigation is cleaned up, its report deliverable kept

The reported defect, and the fix, side by side
Same script, same fixture, same tasks-axi 0.2.5. An investigation whose ONE captain
call was properly answered, after backlog retention MOVED that answered row into
data/done-archive.md. Full transcripts: archive-gate-demo.before-fix.txt / .after-fix.txt

---------------- BEFORE (base 59e7393) ----------------
=== Act 3 - the completion gate rules on the archived answer ===

# the surface the defect was reported against, the retired spelling:
$ fm-decision-hold.sh verify sample-retention-review
fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/retention/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/retention/data/backlog.md
[exit 1]

# and the current spelling it delegates to:
$ fm-captain-hold.sh verify sample-retention-review
fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/retention/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/retention/data/backlog.md
[exit 1]


$ fm-teardown.sh sample-retention-review
REFUSED: scout task sample-retention-review has not passed the captain-call completion gate.
[exit 1]   the finished investigation can never be cleaned up

---------------- AFTER (eca3f52) ----------------
=== Act 3 - the completion gate rules on the archived answer ===

# the surface the defect was reported against, the retired spelling:
$ fm-decision-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory (1 answered and archived)
[exit 0]

# and the current spelling it delegates to:
$ fm-captain-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory (1 answered and archived)
[exit 0]


$ fm-teardown.sh sample-retention-review
teardown sample-retention-review complete (...)
[exit 0]   the investigation is cleaned up, its report deliverable kept
Evidence: Full end-to-end transcript, fixed worktree (retention moves the answered row, gate passes, cleanup runs, all three states)
firstmate archive-aware completion gate - end-to-end CLI transcript
repo under test : /local/home/inthuson/.no-mistakes/worktrees/d4c7ad84348d/01M0KACD4MMK5PFK4AHJQ03FRN
commit          : eca3f52
tasks-axi       : 0.2.5

=== Act 1 - a captain call is held, answered, and attested ===

$ fm-captain-hold.sh hold sample-retention-call --title 'Choose the retention window' --reason 'captain retention choice pending' --repo sample
sample-retention-call
[exit 0]

# the captain's own words, in a file the operator hands to the gate:
$ cat answer.txt
Keep the sample retention window at thirty days.

$ fm-captain-hold.sh answer sample-retention-call --decision-file answer.txt
answered: sample-retention-call
[exit 0]

$ fm-captain-hold.sh complete sample-retention-review sample-retention-call
complete: sample-retention-review captain-call inventory reviewed (sample-retention-call)
[exit 0]

# the gate passes while the answered row is still in the live backlog:
$ fm-captain-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory
[exit 0]


=== Act 2 - backlog retention MOVES the answered row into the archive ===

$ tasks-axi prune --keep 0 --state done   # retention, as the backlog contract runs it
  ok: prune done -> archived 1 (kept 0)
  help[1]:
    - Run `tasks-axi list --state done` to see retained Done items
[exit 0]

# nothing was lost - the row and the captain's words are in the archive:
$ grep -c sample-retention-call data/backlog.md
0

$ grep -n -e sample-retention-call -e "thirty days" data/done-archive.md
  3:- [x] sample-retention-call - Choose the retention window (repo: sample) (done 2026-08-22) (hold: captain retention choice pending) (hold-kind: captain)
  9:  Keep the sample retention window at thirty days.


=== Act 3 - the completion gate rules on the archived answer ===

# the surface the defect was reported against, the retired spelling:
$ fm-decision-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory (1 answered and archived)
[exit 0]

# and the current spelling it delegates to:
$ fm-captain-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory (1 answered and archived)
[exit 0]


=== Act 4 - cleanup of the finished investigation, the gate's real consumer ===

$ fm-teardown.sh sample-retention-review
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
teardown sample-retention-review complete (window firstmate:fm-sample-retention-review, worktree <home>/retention/projects/missing-sample-retention-review)
Backlog: sample-retention-review just finished. Run tasks-axi done sample-retention-review --report data/sample-retention-review/report.md, 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]

$ ls data/sample-retention-review/
  report.md
$ test -e state/sample-retention-review.meta && echo "investigation still open" || echo "investigation cleaned up"
  investigation cleaned up

------------------------------------------------------------

=== Act 5 - the gate is not weakened: all three states ===

# State 1: a key recorded in the attested inventory with NO record in EITHER file.
$ fm-captain-hold.sh verify sample-guarded-review   # expect: still fails loudly
fm-captain-hold: captain call sample-vanished-call has no record: neither sample-vanished-call nor its legacy identity sample-guarded-review-decision-sample-vanished-call is in <home>/guard/data/backlog.md or <home>/guard/data/done-archive.md
[exit 1]

# State 2: a key RECORDED but genuinely unanswered - closed outside 'answer',
#          then archived by retention. Being in the archive is not an answer.
$ grep -c sample-unanswered-call data/done-archive.md
1

$ fm-captain-hold.sh verify sample-guarded-review   # expect: still fails loudly
fm-captain-hold: captain call sample-unanswered-call is recorded in <home>/guard/data/done-archive.md but closed with no recorded captain answer; record what the captain said with: fm-captain-hold.sh answer sample-unanswered-call --decision-file <path>
[exit 1]

# and cleanup of that investigation stays refused:
$ fm-teardown.sh sample-guarded-review
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fm-captain-hold: captain call sample-unanswered-call is recorded in <home>/guard/data/done-archive.md but closed with no recorded captain answer; record what the captain said with: fm-captain-hold.sh answer sample-unanswered-call --decision-file <path>
REFUSED: scout task sample-guarded-review has not passed the captain-call completion gate.
Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown.
[exit 1]

# State 3: a key recorded AND answered, whose only copy retention archived.
$ grep -c sample-answered-call data/backlog.md
0
$ grep -c sample-answered-call data/done-archive.md
1

$ fm-captain-hold.sh verify sample-guarded-review   # expect: passes, and says WHY
verified: sample-guarded-review captain-call inventory (1 answered and archived)
[exit 0]
Evidence: Full end-to-end transcript, base commit (the reported defect reproduced)
firstmate archive-aware completion gate - end-to-end CLI transcript
repo under test : <base-checkout>
commit          : 59e7393 (base, before the fix)
tasks-axi       : 0.2.5

=== Act 1 - a captain call is held, answered, and attested ===

$ fm-captain-hold.sh hold sample-retention-call --title 'Choose the retention window' --reason 'captain retention choice pending' --repo sample
sample-retention-call
[exit 0]

# the captain's own words, in a file the operator hands to the gate:
$ cat answer.txt
Keep the sample retention window at thirty days.

$ fm-captain-hold.sh answer sample-retention-call --decision-file answer.txt
answered: sample-retention-call
[exit 0]

$ fm-captain-hold.sh complete sample-retention-review sample-retention-call
complete: sample-retention-review captain-call inventory reviewed (sample-retention-call)
[exit 0]

# the gate passes while the answered row is still in the live backlog:
$ fm-captain-hold.sh verify sample-retention-review
verified: sample-retention-review captain-call inventory
[exit 0]


=== Act 2 - backlog retention MOVES the answered row into the archive ===

$ tasks-axi prune --keep 0 --state done   # retention, as the backlog contract runs it
  ok: prune done -> archived 1 (kept 0)
  help[1]:
    - Run `tasks-axi list --state done` to see retained Done items
[exit 0]

# nothing was lost - the row and the captain's words are in the archive:
$ grep -c sample-retention-call data/backlog.md
0

$ grep -n -e sample-retention-call -e "thirty days" data/done-archive.md
  3:- [x] sample-retention-call - Choose the retention window (repo: sample) (done 2026-08-22) (hold: captain retention choice pending) (hold-kind: captain)
  9:  Keep the sample retention window at thirty days.


=== Act 3 - the completion gate rules on the archived answer ===

# the surface the defect was reported against, the retired spelling:
$ fm-decision-hold.sh verify sample-retention-review
fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/retention/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/retention/data/backlog.md
[exit 1]

# and the current spelling it delegates to:
$ fm-captain-hold.sh verify sample-retention-review
fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/retention/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/retention/data/backlog.md
[exit 1]


=== Act 4 - cleanup of the finished investigation, the gate's real consumer ===

$ fm-teardown.sh sample-retention-review
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fm-captain-hold: no captain-held task sample-retention-call and no legacy identity sample-retention-review-decision-sample-retention-call in <home>/retention/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/retention/data/backlog.md
REFUSED: scout task sample-retention-review has not passed the captain-call completion gate.
Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown.
[exit 1]

$ ls data/sample-retention-review/
  report.md
$ test -e state/sample-retention-review.meta && echo "investigation still open" || echo "investigation cleaned up"
  investigation still open

------------------------------------------------------------

=== Act 5 - the gate is not weakened: all three states ===

# State 1: a key recorded in the attested inventory with NO record in EITHER file.
$ fm-captain-hold.sh verify sample-guarded-review   # expect: still fails loudly
fm-captain-hold: no captain-held task sample-vanished-call and no legacy identity sample-guarded-review-decision-sample-vanished-call in <home>/guard/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/guard/data/backlog.md
[exit 1]

# State 2: a key RECORDED but genuinely unanswered - closed outside 'answer',
#          then archived by retention. Being in the archive is not an answer.
$ grep -c sample-unanswered-call data/done-archive.md
1

$ fm-captain-hold.sh verify sample-guarded-review   # expect: still fails loudly
fm-captain-hold: no captain-held task sample-unanswered-call and no legacy identity sample-guarded-review-decision-sample-unanswered-call in <home>/guard/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/guard/data/backlog.md
[exit 1]

# and cleanup of that investigation stays refused:
$ fm-teardown.sh sample-guarded-review
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
●  WATCHER DOWN - SUPERVISION IS OFF
●  1 task(s) in flight, but no watcher has a fresh beacon (last beat: never, grace 300s).
●  Trust the emitted supervision protocol for this harness; do not use shell & for watcher repair.
●  This is a supervision warning only; the guarded operation WILL still run.
●  watcher supervision needs Stop-owned automatic recovery; inspect the hook registration and startup status before ending the turn.
●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
fm-captain-hold: no captain-held task sample-unanswered-call and no legacy identity sample-guarded-review-decision-sample-unanswered-call in <home>/guard/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/guard/data/backlog.md
REFUSED: scout task sample-guarded-review has not passed the captain-call completion gate.
Inventory its report and any visual review through bin/fm-captain-hold.sh before teardown.
[exit 1]

# State 3: a key recorded AND answered, whose only copy retention archived.
$ grep -c sample-answered-call data/backlog.md
0
$ grep -c sample-answered-call data/done-archive.md
1

$ fm-captain-hold.sh verify sample-guarded-review   # expect: passes, and says WHY
fm-captain-hold: no captain-held task sample-answered-call and no legacy identity sample-guarded-review-decision-sample-answered-call in <home>/guard/data/backlog.md
fm-captain-hold: captain-held task  is absent from <home>/guard/data/backlog.md
[exit 1]
Evidence: Sibling commands over an archived record: read-only proof and archive-read failure wording

$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt answered: sample-archived-call $ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md both files byte-identical: the replay wrote nothing $ fm-captain-hold.sh answer sample-archived-call --decision-file other.txt fm-captain-hold: captain call sample-archived-call is archived in <home>/data/done-archive.md recording a different captain decision; the archive is not writable, so this answer cannot replace it # missing archive file stays quiet: fm-captain-hold: captain call sample-archived-call has no record: no task sample-archived-call in <home>/data/backlog.md and none in <home>/data/done-archive.md # unopenable archive refuses loudly: fm-captain-hold: the archive could not be searched for captain call sample-archived-call: <home>/data/done-archive.md is not a readable regular file, or the read of it did not complete; a record may well be in there, so this cannot tell until that read works

firstmate sibling commands over an archived record - CLI transcript
repo under test : /local/home/inthuson/.no-mistakes/worktrees/d4c7ad84348d/01M0KACD4MMK5PFK4AHJQ03FRN
commit          : eca3f52
tasks-axi       : 0.2.5

=== Part A - the sibling subcommands reach the archived record too ===

# the only copy of this answered call is the archived one:
$ grep -c sample-archived-call data/backlog.md ; grep -c sample-archived-call data/done-archive.md
  0
  1

# answer: an exact replay returns what it returned before retention ran
$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt
answered: sample-archived-call
[exit 0]

$ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md
  both files byte-identical: the replay wrote nothing

# answers (the keyed channel intake): a redelivered answer reads as closed, not absent
$ printf "sample-keyed-call\tTake the keyed option.\tcaptain reply\n" | fm-captain-hold.sh answers --source "test channel"
closed: sample-keyed-call
answers: closed=1 skipped=0

$ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md
  both files byte-identical: the redelivered keyed answer wrote nothing

# answers: a key that names nothing anywhere is skipped, naming BOTH files searched
$ printf "sample-nowhere-call\tSome answer.\tcaptain reply\n" | fm-captain-hold.sh answers --source "test channel"
skipped: sample-nowhere-call (no captain-held task with that id in <home>/siblings/data/backlog.md or <home>/siblings/data/done-archive.md)
answers: closed=0 skipped=1

# hold: minting the same id again would split one identity across the two files
$ fm-captain-hold.sh hold sample-archived-call --title 'Choose again' --reason 'captain archived choice pending'
fm-captain-hold: task sample-archived-call is already closed and archived in <home>/siblings/data/done-archive.md; a new captain call needs its own task
[exit 1]

# the retired shim's resolve, the one command there with lookups of its own:
$ grep -cE "^- \[[ x]\] sample-shim-origin-decision-routedpick - " data/backlog.md ; grep -cE "^- \[[ x]\] sample-shim-origin-decision-routedpick - " data/done-archive.md
  0
  1
$ tasks-axi show sample-shim-origin-decision-routedpick --full   # does the LIVE backlog still answer for it?
  the live backlog does not answer; the archive is the only copy

$ fm-decision-hold.sh resolve sample-shim-origin routedpick --decision-file route.txt --routed-to sample-shim-work
answered: sample-shim-origin-decision-routedpick
resolved: sample-shim-origin-decision-routedpick -> sample-shim-work
[exit 0]

$ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md
  both files byte-identical: the shim's replay wrote nothing


=== Part B - read-only, and a failed archive read is never 'no record' ===

# a DIFFERENT answer against the archived record has nowhere to go and says so:
$ fm-captain-hold.sh answer sample-archived-call --decision-file other.txt
fm-captain-hold: captain call sample-archived-call is archived in <home>/siblings/data/done-archive.md recording a different captain decision; the archive is not writable, so this answer cannot replace it
[exit 1]

$ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md
  both files byte-identical: the refused answer wrote nothing

# --release cannot reopen an archived row either:
$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt --release
fm-captain-hold: captain call sample-archived-call is archived in <home>/siblings/data/done-archive.md with mode answered; --release cannot reopen an archived task
[exit 1]

$ cmp data/done-archive.md archive-before.md && cmp data/backlog.md backlog-before.md
  both files byte-identical: the refused release wrote nothing

# and the two ways the archive read itself can fail, each in its own words.
# (a) no archive file at all - a home that never had a row trimmed - stays QUIET:
$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt   # archive file taken away
fm-captain-hold: captain call sample-archived-call has no record: no task sample-archived-call in <home>/siblings/data/backlog.md and none in <home>/siblings/data/done-archive.md; hold the task its question gates, then answer it
[exit 1]

# (b) an archive path that cannot be opened refuses LOUDLY - the record may well be in there:
$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt   # archive path is a symlink
fm-captain-hold: the archive could not be searched for captain call sample-archived-call: <home>/siblings/data/done-archive.md is not a readable regular file, or the read of it did not complete; a record may well be in there, so this cannot tell until that read works
[exit 1]

# readable again, and the same record replays exactly as before:
$ fm-captain-hold.sh answer sample-archived-call --decision-file answer.txt
answered: sample-archived-call
[exit 0]
Evidence: Mutation evidence: the new assertions fail when the fix is broken

MUTATION C (backlog-only lookup restored): not ok - the completion gate refused an answered captain call the archive still holds: fm-captain-hold: captain call sample-retention-call has no record ... MUTATION A (quoted ["markdown"] key unwrap restored): not ok - the fallback archive tasks-axi actually wrote was not the file this read searched ... MUTATION B2 (absent archive reported unreadable, record-read path): not ok - an archive file that does not exist must read as no record at all (missing: 'has no record')

Mutation evidence: the new assertions are load-bearing, not decorative
Run on 2026-08-22 against tasks-axi 0.2.5, suite tests/fm-captain-hold-lifecycle.test.sh
Each mutation was applied to the fix, the suite was run, and the source was restored.

============================================================
MUTATION C - revert the two-file read to the backlog-only lookup
  fm_tasks_axi_record_show: skip the archive read (the pre-fix behaviour)
  expected: the archived-answer test reproduces the reported defect

18:not ok - the completion gate refused an answered captain call the archive still holds: fm-captain-hold: captain call sample-retention-call has no record: neither sample-retention-call nor its legacy identity sample-retention-review-decision-sample-retention-call is in /tmp/fm-captain-hold.3u66Gh/retention-archive/data/backlog.md or /tmp/fm-captain-hold.3u66Gh/retention-archive/data/done-archive.md

  suite exit: 1

============================================================
MUTATION A - honour a quoted ["markdown"] table key, the divergence from tasks-axi
  fm_tasks_axi_markdown_path: unwrap quotes around the table key
  expected: the mirror test fails with the FALSE ABSENT for that one config spelling

29:not ok - the fallback archive tasks-axi actually wrote was not the file this read searched: fm-captain-hold: captain call sample-quoted-call has no record: no task sample-quoted-call in /tmp/fm-captain-hold.TIEu5C/markdown-config-quoted-key/data/live-rows.md and none in /tmp/fm-captain-hold.TIEu5C/markdown-config-quoted-key/data/retired-rows.md; hold the task its question gates, then answer it

  suite exit: 1

============================================================
MUTATION B - report an archive file that does not exist as unreadable
  fm_tasks_axi_archive_has_entry: return 4 instead of 1 for a missing archive
  expected: caught by the suite (the hold-time fail-closed guard trips first)

3:not ok - could not register the captain-held task

  suite exit: 1

============================================================
MUTATION B2 - the same defect confined to the RECORD READ path, so the repaired
  missing-archive-file assertion is reached on its own line
  fm_tasks_axi_archive_show: map "no entry" (1) to "archive unreadable" (4)
  expected: the repaired boundary assertion fails, and it does

not ok - an archive file that does not exist must read as no record at all (missing: 'has no record')
  --- output ---
  fm-captain-hold: the archive could not be searched for captain call sample-unopenable-call:
  <home>/data/done-archive.md is not a readable regular file, or the read of it did not
  complete; a record may well be in there, so this cannot tell until that read works

  single-test exit: 1 (the same test passes unmutated: exit 0)

============================================================
Restored source, suite green again: see captain-hold-lifecycle-0.2.5.log
Evidence: Colocated suite run (29 tests, tasks-axi 0.2.5)
ok - report-only unresolved captain call is reproduced and completion refuses before loss
ok - the completion gate attests captain-held inventory and transfers open status decisions
ok - answer records the captain's words, closes idempotently, and releases routed work
ok - release frees held work with the captain's words recorded and the body preserved
ok - a deferred captain call leaves the live Captain's Call until its date and stays answerable
ok - an out-of-band close is recordable with the captain's word and nothing else
ok - ended visual review follows the same captain-hold completion owner
ok - resolved findings and decision-like prose do not create captain-held tasks
ok - terminal single-owner stale status decisions do not block empty inventory
ok - main-home and secondmate-home captain calls remain correctly routed
ok - a bound channel's captured answers close their captain-held tasks at answer time
ok - a channel source with no decision binding closes nothing
ok - legacy identities, metadata, bindings, and the shim keep working
ok - the chat channel feeds the same keyed-answer intake a captured review does
ok - completion and verification validate origins before constructing paths
ok - a status resolution over a still-open captain-held task is signalled, not closed
ok - a captain call with no routed work, a verified transfer, an open decision, and an answered call all stay silent
ok - an answered captain call retention moved to the archive still completes and cleans up
ok - no-record, recorded-but-unanswered, archived-but-unanswered, unheld, unstageable, and unreadable captain-call records all still fail, distinguishably
ok - archived captain-call records are readable, replayable, and never written or duplicated
ok - a row archived mid-batch is still read out of the archive
ok - an origin whose own row retention archived still owns a later review pass
ok - an archive path that cannot be opened refuses loudly and blocks a duplicate hold
ok - an archive read that fails refuses in its own words instead of disowning the origin
ok - an archive read that cannot be settled outranks an answered legacy identity
ok - an archive-read skip names the probed legacy identity, not the delivered key
ok - no staged copy of the archive survives either reader entry point
ok - every [markdown] config form tasks-axi honors still locates the configured archive
ok - a config form tasks-axi ignores falls back here exactly as it does there
EXIT=0
- Evidence: Reproduction scripts for both transcripts (local file: /tmp/no-mistakes-evidence/01M0KACD4MMK5PFK4AHJQ03FRN/archive-gate-demo.sh) - Evidence: Reproduction script for the sibling/read-only transcript (local file: /tmp/no-mistakes-evidence/01M0KACD4MMK5PFK4AHJQ03FRN/sibling-commands-demo.sh)

Pipeline

Updates from git push no-mistakes

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

⚠️ **Review** - 1 info

🔧 Fix: normalize archive-read status, name probed ids, own view cleanup
5 issues (1 warning, 4 infos) still open:

  • ⚠️ bin/fm-captain-hold.sh:322 - The review-1 fix folded a failed archive READ into status 4, and the library's contract was widened to match (bin/fm-tasks-axi-lib.sh:269: "the archive path exists but is not a readable regular file, OR THE READ ITSELF FAILED"). The three operator-facing rc-4 messages were not widened with it, so a read failure is now reported with a cause that is false. unsearchable_archive says "$ARCHIVE_FILE exists but is not a readable regular file ... a symlink or an unreadable mode has to be fixed before this can tell"; the answers rc-4 skip (line 926) and the shim's rc-4 refusal (bin/fm-decision-hold.sh:100) say the same thing. Concrete path, confirmed by running the function body with a PATH-scoped grep that exits 2: an EIO, a stale network handle, or the archive unlinked between the -f/-r guard and the grep returns 4, and fm-captain-hold.sh complete &lt;origin&gt; then prints that the archive "is not a readable regular file" and instructs the operator to fix "a symlink or an unreadable mode" on a file whose mode is fine (or that no longer exists at all). That is a lookup confidently naming the wrong repair, which is the same defect class as the "absent" wording this change exists to remove; the new regression test_an_archive_read_error_never_disowns_the_origin asserts only the true substring "the archive could not be searched", so nothing pins the false clause. Widen the three messages to the disjunction the contract already states, for example "the archive could not be searched for <id>: $ARCHIVE_FILE is not a readable regular file, or the read did not complete", and drop the single-cause repair instruction. No status or call-site behavior needs to change.
  • ℹ️ bin/fm-captain-hold.sh:506 - resolve_and_load hard-stops on an archive-read failure from the FIRST probe ([ &#34;$rc&#34; -eq 1 ] || return &#34;$rc&#34;) before it ever tries the legacy identity, so a definitive positive from the second probe is discarded in favour of an unknown. Reachable: pre-collapse metadata carrying decision_keys=laterpick, the real record &lt;origin&gt;-decision-laterpick OPEN and captain-held in the live data/backlog.md, and data/done-archive.md present as a symlink (the exact state test_archived_origin_still_owns_a_later_review_pass builds, or any grep read error). verify/complete now refuse with "the archive could not be searched", where before this change resolve_entry fell through to the legacy identity, found it in the backlog, and passed - so scout teardown is blocked on a record that is right there. The composite question the caller asks is "is there a record for the entry OR for its legacy identity", and a successful legacy probe answers it regardless of the archive; the comment's justification ("so a second lookup cannot bury it") only holds for a second lookup that also fails to settle it. The counter-argument is real too: if the unreadable archive holds laterpick while the legacy id is live, the two probes name different records and ruling on the legacy one could be the wrong record. Because it is a deliberate, commented fail-closed choice with that genuine ambiguity behind it, the call on whether to fall through on rc 1 only, or to fall through and re-refuse when the legacy probe cannot settle it either, is yours.
  • ℹ️ bin/fm-captain-hold.sh:913 - The three archive-failure skips in command_answers (lines 913, 919, 925) are the same five lines three times over, differing only in the parenthesised reason: each tests one rc, printfs skipped: &lt;RESOLVED_ID&gt; (&lt;reason&gt;), increments skipped, and continues. A single case &#34;$resolve_rc&#34; in 2) reason=&#39;archived in ... but that record could not be read&#39; ;; 3) ... ;; 4) ... ;; esac followed by one printf/increment/continue collapses 24 lines to about 10 with byte-identical output, and makes the rc-1 case's deliberately different subject ($key rather than $RESOLVED_ID) visible as the one real distinction rather than a fourth copy. Non-functional; the three message texts and the exit contract stay exactly as they are.
  • ℹ️ docs/captain-hold-lifecycle.md:111 - "Every archive fixture is built by running tasks-axi prune --keep 0 --state done inside the throwaway home, never by hand-editing a backlog or archive file; the error paths then damage only that disposable archive, its own section heading or the readability of its path." The first clause is true and worth keeping. The second is now false: test_unanswered_and_absent_captain_calls_still_fail_distinguishably drives the unstageable-copy path by pointing TMPDIR at a directory that does not exist (tests/fm-captain-hold-lifecycle.test.sh:1373), and test_an_archive_read_error_never_disowns_the_origin drives the read-error path with a PATH-scoped grep stub that exits 2 for that one file (:1800). Both are safe, fixture-scoped injections, but a reader auditing test safety from this paragraph would conclude neither is used. Extend the sentence to name the two injections rather than dropping the fixture-provenance claim.
  • ℹ️ bin/fm-tasks-axi-lib.sh:330 - Informational, no action needed for this change. The library now seizes the process EXIT trap the first time a view is staged, chaining whatever handler is installed AT THAT MOMENT. The header states the precondition ("A script has finished its own setup long before it reads a record"), and it holds for both current readers: bin/fm-captain-hold.sh traps captain_hold_cleanup at source time (line 173) and bin/fm-decision-hold.sh traps nothing. I confirmed the chaining works end to end (view removed, caller handler ran, exit status preserved) and also confirmed the failure mode it depends on: a caller that installs its EXIT trap AFTER its first record read replaces the library handler and leaks one fm-archive-view.* per invocation, with test_no_staged_archive_view_survives_a_reader unable to see it because it covers only the two existing entry points. Nothing in the tree reaches that ordering today (no trap after line 173 in captain-hold, none at all in the shim, and no exec on any path that stages a view), so this is a documented assumption rather than a defect. Noting it because it is new shared-library behaviour whose safety is an ordering property, and a future reader entry point is where it would first bite.

🔧 Fix: widen archive-read refusals and pin the legacy-fallback stop
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-captain-hold.sh:326 - unsearchable_archive now prefixes its argument with "captain call", but one of its two call sites passes an investigation ORIGIN id, not a captain call. origin_exists_here (bin/fm-captain-hold.sh:365) calls unsearchable_archive &#34;$1&#34; with the origin, so fm-captain-hold.sh complete &lt;origin&gt; against an unsearchable archive prints "the archive could not be searched for captain call <origin>" about a scout/investigation row. The wording this fix round replaced had no label at all ("...so the archive could not be searched for $1"), so this is a regression introduced by the round, and it is the same misdirection class the change exists to remove: an operator told the failure concerns a "captain call sample-archived-origin" will look for a captain-held task by that id and find none, when the real subject is origin ownership. Both reachable paths are already exercised and neither pins the subject: test_archived_origin_still_owns_a_later_review_pass (tests/fm-captain-hold-lifecycle.test.sh:1583, symlinked archive) and test_an_archive_read_error_never_disowns_the_origin (:1804, grep stub) each produce this exact message and assert only "not a readable regular file" / "the archive could not be searched". The other two failure wordings are unaffected, because fm_tasks_axi_archive_has_entry can only return 0, 1 or 4, so origin_exists_here never reaches unreadable_archive or unstageable_archive. Repair: drop the "captain call " label from this one message (restoring the pre-round subject), or take the label as a parameter the way the shim's require_record already does (bin/fm-decision-hold.sh:92), and extend one of the two tests above to assert the subject so it stays honest.
  • ℹ️ docs/captain-hold-lifecycle.md:108 - The verification record is now narrower than the code and the suite it claims to describe. (a) Line 107 says the archive-carries-the-entry class has exactly "two ways the read can fail" and line 108 calls "An archive path that exists but cannot be read as a regular file" the third refusal, but the library contract was widened in this branch to "the archive path exists but is not a readable regular file, OR the read itself failed" (bin/fm-tasks-axi-lib.sh:269) and all three operator messages were widened to that disjunction; the doc's enumeration never was. The grep-stub read failure appears only in the fixture-injection sentence at line 112, so a reader auditing outcomes from lines 107-108 would not know rc 4 covers two causes. (b) Two behaviours the change added after the stated "Verification date: 2026-08-21" (line 97) are absent: that an archive read which cannot be settled outranks an answered legacy identity rather than falling through (bin/fm-captain-hold.sh:508-526, pinned by test_an_unsettled_archive_read_outranks_the_legacy_identity), and that no staged archive view survives either reader entry point (bin/fm-tasks-axi-lib.sh:324-359, pinned by test_no_staged_archive_view_survives_a_reader). Neither is mentioned anywhere in docs/ or in .agents/skills/captain-hold-lifecycle/SKILL.md, and both are observable behaviour of a shared library this doc claims to record. Widen line 108 to the disjunction the contract states and add the two behaviours to the paragraph at lines 102-113, refreshing the date.
  • ℹ️ bin/fm-captain-hold.sh:1105 - command_verify (lines 1104-1113) and command_complete (lines 1046-1057) now hold byte-identical inventory loops apart from the comment: split $keys on commas, skip empties, require_resolved_record &#34;$origin&#34; &#34;$entry&#34;, verify_loaded_durable &#34;$RESOLVED_ID&#34;, fed by the same &lt;&lt;EOF $(printf &#39;%s\n&#39; &#34;$keys&#34; | tr &#39;,&#39; &#39;\n&#39;) EOF heredoc. One helper taking &lt;origin&gt; &lt;comma-list&gt; collapses both to a single call with identical behaviour and makes the real difference between the commands (where $keys comes from: the union of previous and supplied ids versus the recorded attestation) the only thing left at each site. Purely non-functional; the same refactor the accepted answers rc-block dedupe made in this branch. A smaller instance sits in require_resolved_record (line 548), which recomputes legacy_hold_id &#34;$origin&#34; &#34;$entry&#34; for its message even though resolve_and_load already left that exact id in RESOLVED_ID whenever that branch is entered.

🔧 Fix: name the archive-read subject and share the inventory loop
1 info still open:

  • ℹ️ docs/captain-hold-lifecycle.md:116 - The verification record is still narrower than the suite it claims to describe, in the same paragraph round 4 just widened. (a) "Every archive fixture is built by running tasks-axi prune --keep 0 --state done inside the throwaway home, never by hand-editing a backlog or archive file" is now literally false for one fixture: test_a_row_archived_mid_batch_is_still_read_out_of_the_archive (tests/fm-captain-hold-lifecycle.test.sh:1701) writes its own .tasks.toml with done_keep = 1 (:1706) and the row under test, sample-mid-call, reaches the archive through tasks-axi's own automatic trim during tasks-axi done inside answers (:1742-1749), not through prune. The safety property the sentence exists to assert still holds, since that is tasks-axi's own retention rather than a hand edit, but the mechanism named is incomplete and the one home that deliberately deviates from the tracked config is unnamed, which is exactly what a reader auditing fixture provenance from this paragraph would want to see. (b) The behaviour that test pins is absent from the record entirely: the archive read stages ONE parser-legible copy per process and reuses it across lookups, keyed on the resolved path AND the archive's byte size, so a row the same process archives mid-batch is still read out of the archive rather than misreported as a layout tasks-axi cannot parse (bin/fm-tasks-axi-lib.sh:297-310, :364-394). That is observable behaviour of a shared library this document claims to record, and its failure mode is a false alarm about a perfectly healthy file, which is the same wrong-confident-lookup class the whole change removes. Line 114 records only the REMOVAL of the staged copy, not its reuse or its invalidation. Both gaps were introduced by f14c87b and survived the two later rounds that reworked this exact paragraph. Fix: name the automatic done_keep trim (and that one home configures done_keep = 1) alongside prune in the provenance sentence, and add the staged-view reuse plus its size-keyed invalidation to the paragraph, naming test_a_row_archived_mid_batch_is_still_read_out_of_the_archive the way the surrounding sentences already name the tests that pin them.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-captain-hold-lifecycle.test.sh (29 tests green, tasks-axi 0.2.5 on PATH from its mise install dir)
  • End-to-end CLI transcript against the BASE checkout (git archive 59e7393 into a temp dir): reproduces the defect, gate refuses the archived answer with the old "absent from .../data/backlog.md" wording and fm-teardown.sh REFUSES
  • Same transcript against the fixed worktree: fm-decision-hold.sh verify and fm-captain-hold.sh verify both print verified: ... (1 answered and archived), fm-teardown.sh exits 0 and keeps data/&lt;origin&gt;/report.md
  • All three gate states at the CLI: key in neither file (fails, names both files), key recorded but unanswered and archived (fails with closed with no recorded captain answer), key recorded AND answered in the archive (passes)
  • Sibling commands over an archived record: fm-captain-hold.sh answer exact replay, drifted answer, --release, hold duplicate refusal, answers keyed replay and unknown-key skip, fm-decision-hold.sh resolve replay, each with cmp on data/backlog.md and data/done-archive.md
  • Archive-read failure modes at the CLI: missing archive file stays quiet (has no record), symlinked archive path refuses loudly (not a readable regular file), readable again replays exactly
  • Mutation C: fm_tasks_axi_record_show reverted to the backlog-only lookup -> suite fails reproducing the reported defect
  • Mutation A: quoted [&#34;markdown&#34;] table-key unwrap restored -> test_config_forms_tasks_axi_ignores_fall_back_the_same_way fails with the false-absent message
  • Mutation B/B2: an absent archive reported as unreadable -> caught by the suite, and confined to the record-read path it fails the repaired missing-archive assertion on its own line (an archive file that does not exist must read as no record at all)
  • bash tests/fm-gotmp.test.sh, bash tests/fm-bootstrap.test.sh, bash tests/fm-documentation-audiences.test.sh, bash tests/fm-classify-decision-key.test.sh (adjacent suites that source the changed tasks-axi library or cover the changed docs) all green
  • git diff --name-only 59e7393..eca3f52 | grep &#39;^data/&#39; (no live backlog or archive edits) and an em-dash/en-dash scan over added lines and the range's commit messages
  • git status --porcelain / git clean -nd after restoring every mutation (worktree clean, library identical to HEAD, suite re-run green)
⚠️ **Document** - 2 infos
  • ℹ️ docs/configuration.md:38 - Judgment call on placement: the two measured resolver-versus-tasks-axi config divergences (archive set only in the user-level ~/.tasks-axi/config.toml layer, and a key declared twice in one [markdown] table) are left where the code put them, in the bin/fm-tasks-axi-lib.sh header beside the resolver, with docs/captain-hold-lifecycle.md now scoping its claim and pointing there, and docs/configuration.md carrying only the one operator-actionable line (declare path and archive in the project file). No operator doc restates the divergences, because both are latent while the tracked .tasks.toml pins each key once and the project layer wins. If the follow-up work item widens where the resolver looks, docs/configuration.md is the surface to revisit.
  • ℹ️ docs/captain-hold-lifecycle.md:96 - Out-of-scope consolidation worth a follow-up: docs/captain-hold-lifecycle.md is classified maintainer-architecture yet carries the only inline '## Verification record' among docs/*.md, and this change grew it by roughly 18 lines of dated test-by-test evidence. Every other mechanism keeps that material in a classified maintainer-verification page under docs/verification/ (for example docs/verification/process-event-sources.md). Proposed follow-up: move the verification record to docs/verification/captain-hold-lifecycle.md, inventory it in docs/documentation-audiences.json, and leave a one-line pointer behind. Not done here because it is a documentation architecture migration rather than a fact this change made stale.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Inthuson
Inthuson marked this pull request as draft August 21, 2026 22:42
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (2): Last reviewed commit: "no-mistakes(document): correct archive r..." | Re-trigger Greptile

…backlog

The captain-call completion gate searched only the active backlog. Backlog
retention does not delete a closed task, it moves it into the configured
archive, so an investigation whose captain calls were all properly answered
became permanently uncleanable once retention trimmed one of its answered
rows: verify reported the safely archived record as absent, and the gate it
guards can only be satisfied by a record it can see.

Nothing was ever being lost, so nothing here preserves or retains more. Both
files hold real records, and every existence read now consults both.
bin/fm-tasks-axi-lib.sh owns that second lookup: tasks-axi exposes no
archive-aware read and refuses --file against its own configured archive, so
an archived row is read back through tasks-axi from a private umask 077 copy
whose "## Archived <date>" headings are rewritten to the "## Done" heading the
parser accepts, and a copy that cannot be parsed is reported as its own
failure rather than as an absent record.

The gate is not looser. A key with no record in either file still fails, a
recorded key with no recorded captain answer still fails, and only a key that
is both recorded and answered passes. The three refusals are now worded apart,
because the old single "absent" wording is what sent the reader looking for a
missing decision instead of a file-scope bug: a missing record names both
searched files, a recorded-but-unanswered one names the file its record is in
plus the command that records an answer, and a recorded-but-unheld one says it
is not the captain's item. A pass earned by archived records says how many, so
a confirmed answer is distinguishable from a weakened check.

Reads and writes are deliberately asymmetric. tasks-axi writes the active
backlog alone, so an archived record can be reported on and replayed but never
changed: an exact answer replay returns what it returned before the trim, every
drifted answer or mode mismatch is refused naming the archive and the fact that
it cannot be written, and hold refuses an id the archive already closed instead
of minting a second row for the same call. diverged stays backlog-only on
purpose, since retention only moves closed rows and a divergence is by
definition an open one.

The siblings share the scope. answer, decline, repair and verify already exec
into fm-captain-hold.sh and inherit the fix; the retired shim's own resolve
path had the same backlog-only lookup and now uses the same two-file read, with
its dependency-edge cleanup staying live-only under an explicit rationale,
because an archived row has no edges left to clear.

Two subshell hazards fixed alongside: complete and verify called
verify_hold_durable "$(resolve_entry ...)", where a fail inside $( ) exits only
the subshell, so a refusal continued with an empty task id and emitted a second
misleading diagnostic naming nothing.

tests/fm-captain-hold-lifecycle.test.sh grows three cases: an answered call
retention moved to the archive completes and cleans up with the archived count
named; the no-record, recorded-but-unanswered and archived-but-unanswered
states all still fail with three messages asserted to differ; and an archived
record is proven readable and replayable while leaving both files
byte-identical, refusing a drifted answer and a duplicate hold. Every archive
fixture is built by running tasks-axi prune inside a throwaway home, never by
hand-editing a backlog or archive file.
…rchive boundary

The archive lookup resolves the `[markdown]` paths out of a home's own
.tasks.toml, so it has to agree with tasks-axi about which spellings name that
table and which value forms it reads. It diverged in both directions.

It unwrapped a quoted `["markdown"]` table key, which tasks-axi ignores: under
that spelling retention writes the fallback archive while this read resolved the
configured one, so an answered record would have read as absent again for one
config spelling. Drop the unwrap. It also matched only a double-quoted value, so
a single-quoted TOML literal string, which tasks-axi honors, fell back to the
default and named a file that does not exist. Read both quote forms, and take
the value from the opening quote to its own closing quote rather than the first
double-quoted run anywhere on the line, so a trailing comment can no longer be
mistaken for the path. The header comment stated the rule inverted; it now
states the mirror rule once, in both directions, with what was measured against
0.2.4 and 0.2.5, including why an `[[markdown]]` header needs no carve-out.

One existing assertion claimed the missing-archive-file boundary while the
record was still in the live backlog, so the backlog answered first and the
archive read never happened: it passed whatever that read did. Drive it where
the archive genuinely answers, by taking the file away once retention has moved
the only copy of the record into it. Verified non-vacuous: reporting an absent
archive as unreadable fails on that assertion.

The config-form test now covers single-quoted values, and a new mirror test
pins the opposite guarantee, that a form tasks-axi ignores falls back here
exactly as it does there. Verified non-vacuous: restoring the quoted-key unwrap
fails it with the false-absent message.
@Inthuson
Inthuson force-pushed the fm/decision-gate-retention-prune-g9 branch from 0f9eaea to 73e3f07 Compare August 22, 2026 04:17
@Inthuson Inthuson changed the title fix(bin): read captain-call records from the archive as well as the live backlog fix(bin): resolve captain-call records from the done archive as well as the backlog Aug 22, 2026
@Inthuson
Inthuson marked this pull request as ready for review August 22, 2026 04:43
@Inthuson

Copy link
Copy Markdown
Contributor Author

Speaking as Inthuson's firstmate: this is now ready for review. It was deliberately held as a draft with a do-not-merge warning because an earlier revision went green while still carrying a defect I had ruled against, after a fix was routed to a pipeline step that could not apply it. That defect is fixed and I have verified all three parts myself against the branch rather than taking the report on trust: the quoted-table-key carve-out now matches what tasks-axi actually honours, single-quoted literal values are read, and the fail-closed refusal on an unreadable archive is deliberately kept at bin/fm-captain-hold.sh:532 rather than falling through.

That last one is worth a line for a reviewer, because it looks like over-caution and is not. Falling through to a legacy identity does not skip the answeredness check, it changes which record the check runs against, so a decision closed-but-unanswered inside an unreadable archive could pass on a different record's genuine answer. The comment at the refusal says so, so please do not remove it as excess.

One info finding ships open by my decision: this mechanism's verification record sits inline where eight others keep theirs under docs/verification/. Nothing enforces that convention, so it is filed rather than done on a branch already rewritten four times, with the real question being whether the convention should be enforced at all.

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