Skip to content

fix: verify archived captain answers in scout completion - #263

Merged
dnth merged 3 commits into
mainfrom
fm/fm-hold-complete-self-key
Oct 20, 2026
Merged

dnth merged 3 commits into
mainfrom
fm/fm-hold-complete-self-key

Conversation

@dnth

@dnth dnth commented Oct 19, 2026

Copy link
Copy Markdown
Owner

Intent

Fix a bug in the shared scout completion gate, bin/fm-captain-hold.sh complete / verify (shared tracked firstmate material; follow .agents/skills/firstmate-coding-guidelines/SKILL.md).

Observed (19 Oct, scout aceh-prod-deploy-1e22): fm-captain-hold.sh complete aceh-prod-deploy-1e22 --none fails with "captain-held task aceh-prod-deploy-1e22 is neither held for the captain nor closed with a recorded captain answer", so fm-teardown.sh refuses the scout forever. Its metadata carries decisions_reviewed=1/decision_keys= followed by decisions_reviewed=1/decision_keys=aceh-prod-deploy-1e22. The task originally had no backlog row (legacy scout) and now has a fresh queued row.
Second real case (firstmate): scout aceh-prod-continuous-ingest carries decision_keys=aceh-nul-owed-redo; that captain call WAS answered (captain answer A, 18 Oct) but its row was later archived out of the configured backlog by Done retention, so complete/verify fail with "no captain-held task aceh-nul-owed-redo ... in this home's configured backlog".

Accepted requirements and corrections from firstmate (current form):

  • A scout listing its OWN task id in decision_keys is legitimate (captain-hold-lifecycle prefers holding the originating work item). Self-keys must NOT be prevented.
  • Duplicate decisions_reviewed/decision_keys lines are deliberately append-only; do not change that.
  • Do not fabricate a captain decision, hand-edit the live scout metadata, or loosen the gate so real unresolved captain calls slip through. Reproduce in a scratch FM_HOME copy, never the live home.
  • The gate must verify a call that was answered and then archived (read the archive or the recorded resolution) without accepting calls that were never answered.
  • Required regressions: a valid self-held open call (still refused); a self-held call closed with a recorded answer (passes); a transferred call whose status decision is no longer open (passes); answered-then-archived passes while never-answered still refuses.
  • The gate still refuses a genuinely captain-held inventory entry that is neither held nor answered, and still refuses --none while the origin has an open keyed status decision.
  • Advisory accepted: a newer captain lifecycle of the same id archived unanswered from the Queued section (tasks-axi prune --state queued --keep 0) must not be settled by an older archived answer; unchecked archived rows are provenance-bearing refusal candidates.

Root cause found: tasks-axi Done retention (done_keep trim / prune) moved the answered captain-held rows into data/done-archive.md (aceh-prod-deploy-1e22 archived 2026-10-16 with hold-kind captain and resolution records; aceh-nul-owed-redo archived 2026-10-18), and the gate read only the live backlog. The self-key on 1e22 was written legitimately by the scout's own complete aceh-prod-deploy-1e22 aceh-prod-deploy-1e22 while it held itself; the 19 Oct row is a fresh never-held cleanup row reusing the id.

Design decisions:

  • An inventory entry that no live row carries, or whose live row never carried captain-hold provenance (captain hold kind, hold-set stamp, or resolution record), is judged by the newest archived row of that id (or its legacy <origin>-decision-<entry> identity) that did.
  • The archive path is the one tasks-axi's own backlog context resolves; rows are read through the installed tasks-axi parser/projection (the same approach due already uses), with every ## Archived heading and open bullet normalized so rows pruned from any section are read in file order.
  • An archived row is durable only when its resolution record settles its latest lifecycle: a body that opens with a hold-set stamp (re-held) needs a record linked to that stamp. An archived captain row with no such record refuses; an archived row never counts as still held; a live row with captain-hold provenance is always judged on its own as before.
  • complete names each entry the archive evidenced. Live-row semantics are unchanged.

Before/after evidence for the PR body:

  • Before (base 338a7ac), new tests fail with the production messages: "complete --none refused an answered self-held call shadowed by a fresh cleanup row: ... captain-held task sample-deploy-run is neither held for the captain nor closed with a recorded captain answer" and "complete --none refused a call that was answered and then archived: ... no captain-held task sample-redo-call and no migrated hold for it in this home's configured backlog".
  • Before the advisory fix (c20e3b5): "an older archived answer settled a newer lifecycle archived unanswered from the queue".
  • After: all lifecycle tests pass, including test_self_held_call_answered_then_archived_still_verifies and test_archive_never_settles_an_unanswered_call.
  • Scratch FM_HOME copy of the live backlog, archive, metas and status logs (live home untouched; metas byte-identical after the run):
    complete: aceh-prod-deploy-1e22 captain-call inventory reviewed (aceh-prod-deploy-1e22) [answered calls read from the configured Done archive: aceh-prod-deploy-1e22], verify passed;
    complete: aceh-prod-continuous-ingest captain-call inventory reviewed (aceh-nul-owed-redo) [answered calls read from the configured Done archive: aceh-nul-owed-redo], verify passed.

The PR must state the exact post-merge commands firstmate runs in the main home after it fast-forwards to the merge commit (do not run them from the PR):
cd ~/Desktop/firstmate
bin/fm-captain-hold.sh complete aceh-prod-deploy-1e22 --none && bin/fm-teardown.sh aceh-prod-deploy-1e22
bin/fm-captain-hold.sh complete aceh-prod-continuous-ingest --none && bin/fm-teardown.sh aceh-prod-continuous-ingest
Teardown's other checks are unchanged and were not exercised against the live home.

What Changed

  • Make complete and verify recognize answered captain calls in the configured tasks-axi archive, including self-held calls shadowed by fresh rows reusing their ids; complete names the archive-backed entries.

  • Require archived answers to settle the newest captain-hold lifecycle, refusing unanswered or re-held calls; add lifecycle regressions and document archive-backed verification.

  • After firstmate fast-forwards the main home to the merge commit, run:

    cd ~/Desktop/firstmate
    bin/fm-captain-hold.sh complete aceh-prod-deploy-1e22 --none && bin/fm-teardown.sh aceh-prod-deploy-1e22
    bin/fm-captain-hold.sh complete aceh-prod-continuous-ingest --none && bin/fm-teardown.sh aceh-prod-continuous-ingest

    Teardown's other checks are unchanged and were not exercised against the live home.

Risk Assessment

✅ Low: Captain, the archive fallback is bounded, preserves live-row semantics, and rejects archived lifecycles without a matching resolution.

Testing

Live CLI checks and selected regressions passed, with transcripts proving base failures and target recovery. The initial lifecycle-file run hit an unrelated secondmate nesting restriction imposed by workspace isolation. Temporary fixtures were removed; production data remained untouched.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An open keyed self-call refuses --none, while explicit self-inventory transfers the status decision and verifies ✅ pass live Live CLI transcript: self
Recording an answer closes a self-held call and permits completion and verification ✅ pass live Live CLI transcript: self
An archived self-call verifies beneath a fresh cleanup row without changing existing append-only metadata ✅ pass live Live CLI transcript: self; base-versus-target transcript
An answered call removed from the live backlog verifies using its named archive evidence ✅ pass live Live CLI transcript: separate; base-versus-target transcript
A newer live call whose hold was removed refuses despite an older archived answer ✅ pass live Live CLI transcript: separate
A newer unanswered lifecycle archived from Queued overrides an older archived answer and refuses ✅ pass live Live CLI transcript: separate
Unanswered archived calls and re-held rows carrying only an earlier answer refuse completion and verification ✅ pass live Live CLI transcript: never-answered and reheld
A legacy inventory key resolves an answered call through the configured custom archive ✅ pass live Live CLI transcript: custom-legacy
Evidence: Live CLI scenarios and persisted archive records

=== Disposable home: self ===
ok: added sample-self (scout, repo sample) -> In flight
task:
  id: sample-self
  title: Synthetic scout
  state: in_flight
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi done sample-self --pr <url>` when it ships
  - Run `tasks-axi block sample-self --by <other>` to record a dependency

$ fm-captain-hold.sh complete sample-self --none
complete: sample-self captain-call inventory reviewed

$ fm-captain-hold.sh hold sample-self --reason Synthetic\ choice\ pending
sample-self

$ fm-captain-hold.sh complete sample-self --none
fm-captain-hold: origin sample-self still has open captain decisions in its status stream; hold a captain task for what remains, or answer them, before attesting --none
Expected refusal observed.

$ fm-captain-hold.sh complete sample-self sample-self
complete: sample-self captain-call inventory reviewed (sample-self)

$ fm-captain-hold.sh verify sample-self
verified: sample-self captain-call inventory

Persisted append-only metadata and transferred status:
kind=scout
mode=scout
harness=codex
decisions_reviewed=1
decision_keys=
decisions_reviewed=1
decision_keys=sample-self
working: synthetic investigation
needs-decision [key=sample-self]: proceed or wait?
captain-held [key=sample-self]: tracked by sample-self

$ fm-captain-hold.sh answer sample-self --decision-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M5AFT8892XRADYAY8HABRDG6/.test-phase-tmp/live/self/answer.txt --close-live
answered: sample-self

$ fm-captain-hold.sh complete sample-self --none
complete: sample-self captain-call inventory reviewed (sample-self)

$ fm-captain-hold.sh verify sample-self
verified: sample-self captain-call inventory
ok: prune done -> archived 1 (kept 0)
help[1]:
  - Run `tasks-axi list --state done` to see retained Done items
ok: added sample-self (scout, repo sample) -> Queued
task:
  id: sample-self
  title: Fresh cleanup row
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start sample-self` to move it to in flight
  - Run `tasks-axi block sample-self --by <other>` to record a dependency

$ fm-captain-hold.sh complete sample-self --none
complete: sample-self captain-call inventory reviewed (sample-self) [answered calls read from the configured Done archive: sample-self]

$ fm-captain-hold.sh verify sample-self
verified: sample-self captain-call inventory
Metadata byte-identical after repeated completion.

Real tasks-axi archived answer:

## Archived 2026-10-20
- [x] sample-self - Synthetic scout (repo: sample) (kind: scout) (done 2026-10-20) (hold: Synthetic choice pending) (hold-kind: captain)
  Resolution recorded by fm-captain-hold.
  Decision digest: a913b71aeda3d873ae330bb630c91df9d640a209d0ba16bc051feb04ed2b8a8a
  Resolution mode: answered
  Hold stamp: 2026-10-19T16:29:31Z

  Captain decision:
  A: proceed with the synthetic operation.


=== Disposable home: separate ===
ok: added sample-origin (scout, repo sample) -> In flight
task:
  id: sample-origin
  title: Synthetic scout
  state: in_flight
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi done sample-origin --pr <url>` when it ships
  - Run `tasks-axi block sample-origin --by <other>` to record a dependency

$ fm-captain-hold.sh hold sample-call --title Synthetic\ call --reason Choose --origin sample-origin
sample-call

$ fm-captain-hold.sh complete sample-origin sample-call
complete: sample-origin captain-call inventory reviewed (sample-call)

$ fm-captain-hold.sh answer sample-call --decision-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M5AFT8892XRADYAY8HABRDG6/.test-phase-tmp/live/separate/answer.txt --close-live
answered: sample-call
ok: prune done -> archived 1 (kept 0)
help[1]:
  - Run `tasks-axi list --state done` to see retained Done items

$ fm-captain-hold.sh complete sample-origin --none
complete: sample-origin captain-call inventory reviewed (sample-call) [answered calls read from the configured Done archive: sample-call]

$ fm-captain-hold.sh verify sample-origin
verified: sample-origin captain-call inventory
ok: added sample-call (captain, repo sample) -> Queued
task:
  id: sample-call
  title: Next lifecycle
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: captain
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi start sample-call` to move it to in flight
  - Run `tasks-axi block sample-call --by <other>` to record a dependency

$ fm-captain-hold.sh hold sample-call --reason New\ unanswered\ choice
sample-call
ok: unhold sample-call -> cleared
task:
  id: sample-call
  title: Next lifecycle
  state: queued
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: captain
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: "Captain hold set: 2026-10-19T16:29:58Z\nCaptain hold review by: 2026-11-02"
help[1]:
  - Run `tasks-axi ready` to see dispatchable work

$ fm-captain-hold.sh complete sample-origin --none
fm-captain-hold: captain-held task sample-call is neither held for the captain nor closed with a recorded captain answer
Expected refusal observed.

$ fm-captain-hold.sh verify sample-origin
fm-captain-hold: captain-held task sample-call is neither held for the captain nor closed with a recorded captain answer
Expected refusal observed.

$ fm-captain-hold.sh hold sample-call --reason Still\ unanswered
sample-call
ok: prune queued -> archived 1 (kept 0)
help[1]:
  - Run `tasks-axi list --state done` to see retained Done items

$ fm-captain-hold.sh complete sample-origin --none
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

$ fm-captain-hold.sh verify sample-origin
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

Older answered and newer unchecked archived rows:

## Archived 2026-10-20
- [x] sample-call - Synthetic call (repo: firstmate) (kind: captain) (done 2026-10-20) (hold: Choose) (hold-kind: captain)
  Resolution recorded by fm-captain-hold.
  Decision digest: a913b71aeda3d873ae330bb630c91df9d640a209d0ba16bc051feb04ed2b8a8a
  Resolution mode: answered
  Hold stamp: 2026-10-19T16:29:46Z

  Captain decision:
  A: proceed with the synthetic operation.

  Origin: sample-origin


## Archived 2026-10-20
- [ ] sample-call - Next lifecycle (repo: sample) (kind: captain) (since 2026-10-20) (hold: Still unanswered) (hold-kind: captain)
  Captain hold set: 2026-10-19T16:30:04Z
  Captain hold review by: 2026-11-02

=== Disposable home: never-answered ===
ok: added sample-origin (scout, repo sample) -> In flight
task:
  id: sample-origin
  title: Synthetic scout
  state: in_flight
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi done sample-origin --pr <url>` when it ships
  - Run `tasks-axi block sample-origin --by <other>` to record a dependency

$ fm-captain-hold.sh hold sample-call --title Unanswered --reason Choose --origin sample-origin
sample-call

$ fm-captain-hold.sh complete sample-origin sample-call
complete: sample-origin captain-call inventory reviewed (sample-call)
ok: done sample-call -> Done; pruned 1
help[1]:
  - Run `tasks-axi ready` to dispatch work unblocked by this

$ fm-captain-hold.sh complete sample-origin --none
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

$ fm-captain-hold.sh verify sample-origin
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

=== Disposable home: reheld ===
ok: added sample-origin (scout, repo sample) -> In flight
task:
  id: sample-origin
  title: Synthetic scout
  state: in_flight
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi done sample-origin --pr <url>` when it ships
  - Run `tasks-axi block sample-origin --by <other>` to record a dependency

$ fm-captain-hold.sh hold sample-call --title First\ lifecycle --reason Choose --origin sample-origin
sample-call

$ fm-captain-hold.sh complete sample-origin sample-call
complete: sample-origin captain-call inventory reviewed (sample-call)

$ fm-captain-hold.sh answer sample-call --decision-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M5AFT8892XRADYAY8HABRDG6/.test-phase-tmp/live/reheld/answer.txt --release
released: sample-call

$ fm-captain-hold.sh hold sample-call --reason Second\ batch\ pending
sample-call
ok: done sample-call -> Done; pruned 1
help[1]:
  - Run `tasks-axi ready` to dispatch work unblocked by this

$ fm-captain-hold.sh complete sample-origin --none
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

$ fm-captain-hold.sh verify sample-origin
fm-captain-hold: captain-held task sample-call was archived out of this home's configured backlog with no recorded captain answer
Expected refusal observed.

=== Disposable home: custom-legacy ===
ok: added sample-origin (scout, repo sample) -> In flight
task:
  id: sample-origin
  title: Synthetic scout
  state: in_flight
  blocked: no
  blocked_by: none
  held: no
  hold_reason: "-"
  hold_kind: "-"
  hold_until: "-"
  kind: scout
  repo: sample
  priority: "-"
  created: 2026-10-20
  closed: "-"
  deps: none
  links: none
  body: ""
help[2]:
  - Run `tasks-axi done sample-origin --pr <url>` when it ships
  - Run `tasks-axi block sample-origin --by <other>` to record a dependency

$ fm-captain-hold.sh hold sample-origin-decision-choice --title Legacy\ call --reason Choose
sample-origin-decision-choice

$ fm-captain-hold.sh complete sample-origin choice
complete: sample-origin captain-call inventory reviewed (choice)

$ fm-captain-hold.sh answer sample-origin-decision-choice --decision-file ~/.no-mistakes/worktrees/dd71c22cc6d7/01M5AFT8892XRADYAY8HABRDG6/.test-phase-tmp/live/custom-legacy/answer.txt --close-live
answered: sample-origin-decision-choice
ok: prune done -> archived 1 (kept 0)
help[1]:
  - Run `tasks-axi list --state done` to see retained Done items

$ fm-captain-hold.sh complete sample-origin --none
complete: sample-origin captain-call inventory reviewed (choice) [answered calls read from the configured Done archive: sample-origin-decision-choice]

$ fm-captain-hold.sh verify sample-origin
verified: sample-origin captain-call inventory

Configured custom archive:

## Archived 2026-10-20
- [x] sample-origin-decision-choice - Legacy call (repo: firstmate) (kind: captain) (done 2026-10-20) (hold: Choose) (hold-kind: captain)
  Resolution recorded by fm-captain-hold.
  Decision digest: a913b71aeda3d873ae330bb630c91df9d640a209d0ba16bc051feb04ed2b8a8a
  Resolution mode: answered
  Hold stamp: 2026-10-19T16:30:34Z

  Captain decision:
  A: proceed with the synthetic operation.


All live CLI checks completed successfully.
Evidence: Base failures versus target success
$ base fm-captain-hold.sh complete sample-self --none
fm-captain-hold: captain-held task sample-self is neither held for the captain nor closed with a recorded captain answer

$ target fm-captain-hold.sh complete sample-self --none
complete: sample-self captain-call inventory reviewed (sample-self) [answered calls read from the configured Done archive: sample-self]

$ base fm-captain-hold.sh complete sample-origin --none (call absent from live backlog)
fm-captain-hold: no captain-held task sample-self and no migrated hold for it in this home's configured backlog (data directory ~/.no-mistakes/worktrees/dd71c22cc6d7/01M5AFT8892XRADYAY8HABRDG6/.test-phase-tmp/base-absent/data); the nearest legacy identity sample-origin-decision-sample-self also resolves to nothing

$ target fm-captain-hold.sh complete sample-origin --none
complete: sample-origin captain-call inventory reviewed (sample-self) [answered calls read from the configured Done archive: sample-self]
verified: sample-origin captain-call inventory

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
An open keyed self-call refuses --none, while explicit self-inventory transfers the status decision and verifies ✅ pass live Live CLI transcript: self
Recording an answer closes a self-held call and permits completion and verification ✅ pass live Live CLI transcript: self
An archived self-call verifies beneath a fresh cleanup row without changing existing append-only metadata ✅ pass live Live CLI transcript: self; base-versus-target transcript
An answered call removed from the live backlog verifies using its named archive evidence ✅ pass live Live CLI transcript: separate; base-versus-target transcript
A newer live call whose hold was removed refuses despite an older archived answer ✅ pass live Live CLI transcript: separate
A newer unanswered lifecycle archived from Queued overrides an older archived answer and refuses ✅ pass live Live CLI transcript: separate
Unanswered archived calls and re-held rows carrying only an earlier answer refuse completion and verification ✅ pass live Live CLI transcript: never-answered and reheld
A legacy inventory key resolves an answered call through the configured custom archive ✅ pass live Live CLI transcript: custom-legacy
  • bash .test-phase-tmp/live-check.sh: real captain-hold and tasks-axi CLIs against disposable homes.
  • bash .test-phase-tmp/baseline-check.sh: base and target commands against identical archived-answer records.
  • TMPDIR=&#34;$PWD/.test-phase-tmp&#34; bash tests/fm-captain-hold-lifecycle.test.sh: stopped at an unrelated secondmate fixture nesting restriction.
  • TMPDIR=&#34;$PWD/.test-phase-tmp&#34; bash tests/.test-phase-selected.test.sh: both new archive regressions plus terminal-status and out-of-band-close checks passed.
  • Removed disposable homes and temporary runners; confirmed the working tree was clean.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

dnth added 3 commits October 20, 2026 00:10
The scout completion gate (fm-captain-hold.sh complete/verify) read an
inventoried captain call only from the live backlog. tasks-axi's done_keep
trim moves answered calls into the configured Done archive, so:

- an origin whose inventoried call was answered and then archived failed
  with "no captain-held task <id>" (aceh-prod-continuous-ingest ->
  aceh-nul-owed-redo);
- a self-held origin whose answered row was archived and then re-added as a
  fresh cleanup row failed with "neither held for the captain nor closed
  with a recorded captain answer" (aceh-prod-deploy-1e22).

An entry no live row carries, or whose live row never carried a captain
hold, is now judged by the newest archived row of that id that did. It is
durable only when that row records a resolution. Archived calls with no
recorded answer still refuse, and a live row with captain-hold provenance is
always judged on its own, so an older archived answer cannot settle a newer
lifecycle. complete names each entry the archive evidenced.
The Done-archive reader parsed only checked rows, so a newer captain
lifecycle archived unanswered from the Queued or In flight section was
skipped and an older archived answer of the same id settled it. Normalize
every archived open bullet before parsing so the newest captain row decides.

An archived row also now counts as answered only when its resolution record
settles its latest lifecycle: a re-held row opens with a fresh hold-set
stamp, so an earlier lifecycle's record below it no longer settles the call.
@dnth
dnth merged commit 58a838e into main Oct 20, 2026
18 checks passed
@dnth
dnth deleted the fm/fm-hold-complete-self-key branch October 20, 2026 00:48
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