fix(bin): resolve captain-call records from the done archive as well as the backlog - #2759
fix(bin): resolve captain-call records from the done archive as well as the backlog#2759Inthuson wants to merge 12 commits into
Conversation
Confidence Score: 5/5The 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.
…plicate record lookups
…ted-header read scope
…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.
…the config-scope claim
0f9eaea to
73e3f07
Compare
|
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 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 |
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.shgains the shared, strictly read-only two-file record lookup:fm_tasks_axi_record_showtries 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 todata/backlog.mdanddata/done-archive.md), staging one parser-legible archive view per process keyed on path and byte size and removing it through a chainedEXITtrap. 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.shnow reads every durable captain-call record through that helper, sohold,answer,answers,complete, andverifyaccept an archived record, and thecompleteorigin-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,holdrefuses to mint a second row for an id the archive already carries, andverifyreports the archived count on a pass. Writes stay on the live backlog: an archived record takes an exactanswerreplay but refuses a drifted answer and--release.diverged's open-task scan andfm-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.shreads its ownresolvelookups through the same helper and skips blocked-by edge clearing for an archived routed task.tests/fm-captain-hold-lifecycle.test.shadds 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 runningtasks-axi prunein a throwaway home.docs/captain-hold-lifecycle.md,docs/configuration.md, anddocs/scripts.mddocument 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 keptEvidence: Full end-to-end transcript, fixed worktree (retention moves the answered row, gate passes, cleanup runs, all three states)
Evidence: Full end-to-end transcript, base commit (the reported defect reproduced)
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 worksEvidence: 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')Evidence: Colocated suite run (29 tests, tasks-axi 0.2.5)
/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.)
🔧 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_archivesays "$ARCHIVE_FILE exists but is not a readable regular file ... a symlink or an unreadable mode has to be fixed before this can tell"; theanswersrc-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/-rguard and the grep returns 4, andfm-captain-hold.sh complete <origin>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 regressiontest_an_archive_read_error_never_disowns_the_originasserts 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_loadhard-stops on an archive-read failure from the FIRST probe ([ "$rc" -eq 1 ] || return "$rc") 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 carryingdecision_keys=laterpick, the real record<origin>-decision-laterpickOPEN and captain-held in the livedata/backlog.md, anddata/done-archive.mdpresent as a symlink (the exact statetest_archived_origin_still_owns_a_later_review_passbuilds, or any grep read error).verify/completenow refuse with "the archive could not be searched", where before this changeresolve_entryfell 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 holdslaterpickwhile 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 incommand_answers(lines 913, 919, 925) are the same five lines three times over, differing only in the parenthesised reason: each tests one rc, printfsskipped: <RESOLVED_ID> (<reason>), incrementsskipped, and continues. A singlecase "$resolve_rc" in 2) reason='archived in ... but that record could not be read' ;; 3) ... ;; 4) ... ;; esacfollowed 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 ($keyrather 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 runningtasks-axi prune --keep 0 --state doneinside 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_distinguishablydrives the unstageable-copy path by pointing TMPDIR at a directory that does not exist (tests/fm-captain-hold-lifecycle.test.sh:1373), andtest_an_archive_read_error_never_disowns_the_origindrives the read-error path with a PATH-scopedgrepstub 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 trapscaptain_hold_cleanupat 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 onefm-archive-view.*per invocation, withtest_no_staged_archive_view_survives_a_readerunable to see it because it covers only the two existing entry points. Nothing in the tree reaches that ordering today (notrapafter line 173 in captain-hold, none at all in the shim, and noexecon 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_archivenow 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) callsunsearchable_archive "$1"with the origin, sofm-captain-hold.sh complete <origin>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) andtest_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, becausefm_tasks_axi_archive_has_entrycan only return 0, 1 or 4, soorigin_exists_herenever reachesunreadable_archiveorunstageable_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'srequire_recordalready 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 bytest_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 bytest_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) andcommand_complete(lines 1046-1057) now hold byte-identical inventory loops apart from the comment: split$keyson commas, skip empties,require_resolved_record "$origin" "$entry",verify_loaded_durable "$RESOLVED_ID", fed by the same<<EOF $(printf '%s\n' "$keys" | tr ',' '\n') EOFheredoc. One helper taking<origin> <comma-list>collapses both to a single call with identical behaviour and makes the real difference between the commands (where$keyscomes 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 acceptedanswersrc-block dedupe made in this branch. A smaller instance sits inrequire_resolved_record(line 548), which recomputeslegacy_hold_id "$origin" "$entry"for its message even thoughresolve_and_loadalready left that exact id inRESOLVED_IDwhenever 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 runningtasks-axi prune --keep 0 --state doneinside 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.tomlwithdone_keep = 1(:1706) and the row under test,sample-mid-call, reaches the archive through tasks-axi's own automatic trim duringtasks-axi doneinsideanswers(:1742-1749), not throughprune. 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 automaticdone_keeptrim (and that one home configuresdone_keep = 1) alongsideprunein the provenance sentence, and add the staged-view reuse plus its size-keyed invalidation to the paragraph, namingtest_a_row_archived_mid_batch_is_still_read_out_of_the_archivethe 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 59e7393into a temp dir): reproduces the defect, gate refuses the archived answer with the old "absent from .../data/backlog.md" wording andfm-teardown.shREFUSESSame transcript against the fixed worktree:fm-decision-hold.sh verifyandfm-captain-hold.sh verifyboth printverified: ... (1 answered and archived),fm-teardown.shexits 0 and keepsdata/<origin>/report.mdAll three gate states at the CLI: key in neither file (fails, names both files), key recorded but unanswered and archived (fails withclosed with no recorded captain answer), key recorded AND answered in the archive (passes)Sibling commands over an archived record:fm-captain-hold.sh answerexact replay, drifted answer,--release,holdduplicate refusal,answerskeyed replay and unknown-key skip,fm-decision-hold.sh resolvereplay, each withcmpon data/backlog.md and data/done-archive.mdArchive-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 exactlyMutation C:fm_tasks_axi_record_showreverted to the backlog-only lookup -> suite fails reproducing the reported defectMutation A: quoted["markdown"]table-key unwrap restored ->test_config_forms_tasks_axi_ignores_fall_back_the_same_wayfails with the false-absent messageMutation 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 greengit diff --name-only 59e7393..eca3f52 | grep '^data/'(no live backlog or archive edits) and an em-dash/en-dash scan over added lines and the range's commit messagesgit status --porcelain/git clean -ndafter restoring every mutation (worktree clean, library identical to HEAD, suite re-run green)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.