Skip to content

fix(bin): preserve captain calls during teardown - #3502

Open
kunchenguid wants to merge 13 commits into
mainfrom
fm/fm-teardown-closes-captain-held-scout-r1
Open

fix(bin): preserve captain calls during teardown#3502
kunchenguid wants to merge 13 commits into
mainfrom
fm/fm-teardown-closes-captain-held-scout-r1

Conversation

@kunchenguid

@kunchenguid kunchenguid commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Intent

Fix a captain-hold-lifecycle INTEGRITY bug at the design level: bin/fm-teardown.sh closed a captain-held scout task, dropping the captain call without a recorded answer - exactly what the captain-hold-lifecycle skill says must NEVER happen.

Reproduced in the main home on 2026-09-01: a scout finished its report; firstmate ran fm-captain-hold.sh hold on that same task (the skill says 'prefer holding the work item the question gates'); fm-captain-hold.sh complete passed; then fm-teardown.sh (report present, landed-work test passed) moved the task to Done, CLOSING the captain-held task with NO recorded captain answer. The contract was self-contradictory: the skill tells firstmate to HOLD the originating work item while teardown's automatic backlog transition CLOSES that item.

The task was to fix it at the design level with ONE owner, choosing the cleaner of (A) teardown refuses or DEFERS the Done transition while the item is captain-held with no recorded answer, keeping the item open with its report recorded as its artifact, or (B) the skill mints a SEPARATE held task so the scout's own row can close. Whichever was chosen had to keep a SINGLE durable captain-call row and NEVER close it without fm-captain-hold.sh answer, and the choice had to be stated in the PR.

Chose (A) deliberately: (B) contradicts the skill's own 'prefer holding the work item the question gates', is enforceable only by agent memory instead of deterministically in the code that actually closes rows, and leaves the same hole open for any other captain-held task (a captain-gated ship, not only a scout). (A) is deterministic, fails closed, covers every kind, and keeps exactly one durable row.

ACCEPTED DECISIONS, all deliberate and already ruled on by the supervisor - do not re-litigate them as if they were oversights:

  1. All captain-call mechanics stay in their existing single owner bin/fm-captain-hold.sh. It gained the read-only predicate 'open ' (exit 0 still an open captain call, 1 not, 2 cannot tell) and 'retain [--report|--pr|--note]' which records the finished work's deliverable in the task body and returns the row to Queued while keeping the captain hold.
  2. bin/fm-teardown.sh asks 'open' before its automatic backlog close. On 0 it defers ONLY the close; on 2 it refuses before any destructive step rather than treating 'cannot tell' as permission to close. Every other cleanup step runs exactly as before. --force does NOT lift the deferral: it authorizes discarding unlanded WORK, never the captain's question.
  3. The deferred row returns to Queued (still captain-held) rather than staying In flight. Queued-and-held is the shape every other captain call already has and is what fm-fleet-snapshot.sh's captain_actionable requires, so the retained call lands in Bearings' Captain's Call instead of reading as work still under way. This PRESERVES rather than excepts the meta/backlog invariant in bin/fm-backlog-transition-lib.sh.
  4. The deliverable is recorded in the task body rather than through 'tasks-axi update --report', because on a non-Done row that flag round-trips the path into the task title and corrupts it.
  5. A pending-CLOSE record is deliberately never staged on this path: replaying one at the next session start would close the captain's unanswered question, which is the loss the whole change exists to prevent. Bootstrap's established diagnostic for an unsafe pending close is deliberately preserved unchanged.
  6. retain and answer are serialized under the SAME existing per-task metadata lock captain-hold operations already use; teardown's ownership of that lock is passed through rather than reacquired, and retention never reopens a call a concurrent answer already closed.
  7. A durable pending-retention record (state/.backlog-retain) makes the retain-plus-record-removal transition replayable at the next session start. Its replay may only ever re-assert that the captain call is HELD; it must never encode or perform a close and never dispatch the row.
  8. The ordinary, non-captain-held teardown path stays behaviorally identical to before this change: a normal scout or ship still tears down and closes to Done with its report or PR link.
  9. The retention record is replayable from the moment it is staged, BEFORE destructive cleanup begins, until the transition completes and it is removed. The cleanup-complete flag decides only whether recovery warns about possible leftovers - never whether the record is replayable. A kill or a reported cleanup failure inside that window therefore recovers the captain call as HELD rather than leaving an unarmed record. This remedy was previously listed as remaining work; it is now IMPLEMENTED and its regression asserts the recovered call is queued and still captain-held with its deliverable.
  10. Recovery treats a captain answer recorded with 'answer --release' (resolution recorded, hold lifted, row not Done) as ANSWERED and completes the transition CLEANLY: it returns the now ownerless row to Queued while removing the worker metadata and the retention marker, and it does NOT restore the lifted hold. This is the supervisor's explicit ruling on the last review question ('confirm that released recovery should queue the row while removing metadata'), so it is a decided requirement, not an open choice. It keeps the meta/backlog invariant intact - recovery never leaves an In-flight row with no worker record - and the regression at tests/fm-captain-hold-lifecycle.test.sh asserts exactly that clean completion: metadata absent, marker absent, row queued, hold not restored, and the captain's recorded answer preserved. This remedy was previously listed as remaining work; it is now IMPLEMENTED.
  11. Stale retention replay must never mutate a replacement incarnation: recovery validates the current metadata spawn generation against the marker before touching the row and treats a mismatch as stale.

There is NO remaining accepted-but-unimplemented work in this area. Every remedy the supervisor authorized during earlier review rounds is implemented on this branch.

Regressions live in tests/fm-captain-hold-lifecycle.test.sh and exercise the real executables (fm-captain-hold.sh, fm-teardown.sh, fm-bootstrap.sh, fm-bearings-snapshot.sh) with observable behavior only, never source-text matching: a scout that holds its own work item survives cleanup open, queued, still captain-held, carrying its deliverable, visible in Bearings' Captain's Call, with no pending-close record and no leftover retention record; a --force cleanup leaves it open too; an ordinary non-held scout still closes to Done with its report link; only fm-captain-hold.sh answer closes the call; a concurrent captain answer during retention is neither lost nor reopened; a kill after the reopen recovers through session start to a durably held call; a reported cleanup failure recovers the call as held; a stale record does not mutate a replacement worker; and a released answer completes recovery cleanly without restoring the hold.

Constraints: bash 3.2 safe; bin/fm-lint.sh clean; one sentence per line and plain dashes in tracked Markdown, never an em dash; no agent co-author trailer on any commit; single-owner contracts with the affected script headers and docs/captain-hold-lifecycle.md updated rather than duplicated; bin/fm-lock-lib.sh and bin/fm-wake-lib.sh remain the owners of locking mechanics and bin/fm-timeout-lib.sh the owner of bounded execution.

This run is a re-validation of an already-implemented branch. The PR's automated attestation check is red only because the PR body attestation still names an older head than the current one; the push step re-binding the attestation to the current head is what clears it.

What Changed

  • Chose design A: teardown defers automatic closure of unanswered captain-held tasks, queues them with their deliverable recorded, and fails closed when hold state cannot be read.
  • Added locked, generation-validated retention markers and bootstrap replay that preserve concurrent or released answers without staging a pending close.
  • Updated lifecycle documentation and executable regressions for normal, forced, interrupted, stale, concurrent-answer, and non-held teardown paths.

Risk Assessment

✅ Low: The change is well-bounded, preserves the stated captain-call invariants across normal, concurrent, and recovery paths, and introduces no substantiated source defect.

Testing

The focused end-to-end lifecycle suite exercised teardown deferral, answer-only closure, pending-close refusal, concurrency, crash/bootstrap recovery, cleanup failures, stale generations, released answers, relocated backlogs, and unreadable state; all scenarios passed, and CLI verification confirmed recovery is the sole retention entry point. No visual artifact was needed because this is a CLI lifecycle change.

Evidence: Captain-hold lifecycle end-to-end transcript

Source: Captain-hold lifecycle end-to-end transcript

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 - cleanup leaves a captain-held work item open with its deliverable, and only an answer closes it
ok - cleanup retains captain calls in the configured backlog
ok - captain hold creation serializes with teardown
ok - a pending close blocks a new captain hold
ok - retention serializes with a concurrent captain answer
ok - bootstrap recovers a kill between reopen and record removal
ok - bootstrap recovers a kill during destructive cleanup
ok - stale retention recovery does not mutate a replacement incarnation
ok - reported cleanup failure preserves a recoverable captain call
ok - released answer completes retention recovery without restoring the hold
ok - cleanup fails closed when the captain hold cannot be read
Evidence: Public CLI help and rejected standalone retain command

Source: Public CLI help and rejected standalone retain command

$ bin/fm-captain-hold.sh --help
fm-captain-hold.sh - deterministic mechanics for tasks held for the captain.

The semantic policy is owned once by
.agents/skills/captain-hold-lifecycle/SKILL.md. This script never reads
report, visual-review, chat, or terminal prose to guess whether the captain
owes an answer. The invoking agent decides what is genuinely waiting on the
captain; this script supplies guarded creation, a durable record of what the
captain actually said, the investigation completion gate, and the one
keyed-answer intake every channel feeds.

There is no separate decision type. A captain call is an ordinary backlog
task held for the captain (`tasks-axi hold <id> --kind captain`), and its
identity is simply the task id. Older installs created derived
`<origin>-decision-<key>` identities through bin/fm-decision-hold.sh; those
rows are already plain task ids, so they keep working here unchanged, and
the legacy inputs noted below resolve them without a migration.
All backlog mutations run in the active FM_HOME, which keeps main-home and
secondmate-home ownership aligned with the work that discovered the call.

Usage:
  fm-captain-hold.sh hold <task-id> --reason <reason> \
    [--title <title>] [--repo <repo>] [--origin <origin-id>] [--until YYYY-MM-DD]
  fm-captain-hold.sh answer <task-id> --decision-file <path> [--release]
  fm-captain-hold.sh answers [<legacy-origin> | --any-origin] --source <provenance>   (keyed answers on stdin)
  fm-captain-hold.sh bind <source-id> [<legacy-origin> | --any-origin]
  fm-captain-hold.sh unbind <source-id>
  fm-captain-hold.sh binding <source-id>
  fm-captain-hold.sh complete <origin-id> (--none | <task-id>...)
  fm-captain-hold.sh verify <origin-id>
  fm-captain-hold.sh open <task-id>
  fm-captain-hold.sh recover-retain <state/task-id.backlog-retain>
  fm-captain-hold.sh diverged

`hold` places an existing task under an active captain hold, or creates the
task first when no work item exists to hold (--title required to create; the
optional --origin records provenance in the new task's body and supplies the
default repo from that origin's metadata). Prefer holding the work item the
question gates over minting a new row. Repeating `hold` with the same id is
idempotent; a task already closed is refused rather than reopened. `--until`
records the captain's own deferral date through `tasks-axi hold --until`, so
a "revisit later" answer is stored as a date instead of a live card.

`answer` records the captain's exact words and closes the call in the same
act. It requires a non-empty captain decision file of at most 8192 bytes,
writes a resolution block at the top of the task body (the previous body is
preserved below the block and archived through tasks-axi --archive-body),
then closes the task with `tasks-axi done` - or, with `--release`, lifts the
hold with `tasks-axi unhold` so a captain-gated WORK item resumes instead of
closing. An exact retry is idempotent only when its requested close mode
matches the newest record; a changed decision or a mode mismatch is rejected.
A re-held task may record a new answer on top. On a task already closed outside this script,
`answer` records the missing resolution block (the old `repair` path) only
when the task still carries the captain-hold provenance tasks-axi preserves
through a close, so an ordinary finished task cannot be dressed up as an
answered captain call. A hold that expired by date (`--until` in the past) is
still answerable: the surviving hold annotations, not tasks-axi's live
`held:` bit, prove the captain owned it.

ONE KEYED-ANSWER INTAKE, FED BY EVERY CHANNEL.
"A keyed answer closes its matching captain-held task" is a single
capability, owned here and nowhere else. `answers` reads
`<task-id>\t<answer>\t<label>[\t<mode>]` lines on stdin and closes each named
task through the very same `answer` path above, so every guard applies
identically no matter which channel the answer arrived on. The key IS the
task id - no identity arithmetic. The optional fourth field selects the close:
empty or `done` completes the task, `release` lifts the hold so held work
resumes; anything else is skipped. A key that names no task, a task that is
not held for the captain, or a task already closed is reported as `skipped:`
and feeds nothing. A replayed delivery whose answer digest and requested
close mode both match the newest record is reported `closed:` and is a no-op;
a mode mismatch is skipped. The command exits nonzero when any key was
skipped. `--source` is provenance text recorded in the
durable decision, never a behavior switch: this command has no per-channel
branch and no knowledge of chat, review decks, or any transport.
Legacy input: an optional positional origin (or a stored concrete-origin
binding) makes a key that names no task fall back to the old
`<origin>-decision-<key>` identity, so an in-flight pre-collapse channel
keeps closing its rows; `--any-origin` and the stored `(any)` marker mean
what an absent origin means and are accepted for the same reason.

A channel's ONLY job is to turn whatever it received into those keyed lines
and pipe them here. It must never map keys to tasks, build decision records,
choose a close mode beyond what its card declared, or close anything itself.

`bind`, `unbind`, and `binding` record that a captured-answer SOURCE feeds
this intake, for any channel whose answers arrive detached from their origin
(a process-event source id, for example). The binding is a private record
under `state/decision-bindings/`; a source with no binding feeds nothing, so
this whole path is opt-in per source and an unbound source behaves as if it
did not exist. `bind` deliberately does not require the source to exist yet,
so a channel can be bound BEFORE it is armed. The optional second argument
exists only for legacy pre-collapse records and callers: a concrete origin is
stored verbatim and used as the composition fallback above, and
`--any-origin` stores the same `(any)` marker a plain `bind <source-id>`
stores. `binding` prints the stored value verbatim and `answers` accepts it,
so the process-event runner's feed seam is unchanged.

`complete` is the shared investigation and visual-review completion gate.
It attests, in the origin task's metadata, the reviewed inventory of
captain-held tasks that carry the origin's unresolved captain calls.
`--none` is an explicit semantic attestation that the just-reviewed surface
has no unresolved captain call, and is refused while the origin still has an
open keyed status decision. With a non-empty inventory, every listed task is
verified durable (actively captain-held, or closed with a recorded answer),
the inventory is unioned idempotently into the metadata, and every still-open
keyed status decision is transferred to its durable owner with a
`captain-held [key=...]` status close naming the inventory. Later review
passes may add ids. A post-teardown visual review can complete against the
surviving report and tasks without recreating task state.
`verify` is read-only and is called by scout teardown, so teardown cannot
erase a source before this gate has succeeded: every recorded inventory
entry must still be durable and no keyed status decision may be open.
Metadata compatibility: the attestation keeps the historical
`decisions_reviewed=1` and `decision_keys=` keys, and an inventory entry that
names no existing task resolves through the legacy `<origin>-decision-<entry>`
identity, so pre-collapse metadata written by fm-decision-hold.sh verifies
unchanged. An entry that exists as a task id is always that task.

`open` is the read-only predicate a MECHANICAL closer asks before it may
retire a task's row: is this task still an open captain call? It exits 0 when
it is, 1 when it is not, and 2 when the answer cannot be established, so a
caller that must never close a live call can treat "cannot tell" as its own
case instead of as a no. It prints nothing on 0 or 1 and mutates nothing. The
policy prefers holding the very work item a question gates, so the row a
cleanup is about to close is routinely the captain's own call; this predicate
is how that cleanup finds out. It is not a second closing rule: `answe

... [4014 bytes truncated] ...

te; the
optional --origin records provenance in the new task's body and supplies the
default repo from that origin's metadata). Prefer holding the work item the
question gates over minting a new row. Repeating `hold` with the same id is
idempotent; a task already closed is refused rather than reopened. `--until`
records the captain's own deferral date through `tasks-axi hold --until`, so
a "revisit later" answer is stored as a date instead of a live card.

`answer` records the captain's exact words and closes the call in the same
act. It requires a non-empty captain decision file of at most 8192 bytes,
writes a resolution block at the top of the task body (the previous body is
preserved below the block and archived through tasks-axi --archive-body),
then closes the task with `tasks-axi done` - or, with `--release`, lifts the
hold with `tasks-axi unhold` so a captain-gated WORK item resumes instead of
closing. An exact retry is idempotent only when its requested close mode
matches the newest record; a changed decision or a mode mismatch is rejected.
A re-held task may record a new answer on top. On a task already closed outside this script,
`answer` records the missing resolution block (the old `repair` path) only
when the task still carries the captain-hold provenance tasks-axi preserves
through a close, so an ordinary finished task cannot be dressed up as an
answered captain call. A hold that expired by date (`--until` in the past) is
still answerable: the surviving hold annotations, not tasks-axi's live
`held:` bit, prove the captain owned it.

ONE KEYED-ANSWER INTAKE, FED BY EVERY CHANNEL.
"A keyed answer closes its matching captain-held task" is a single
capability, owned here and nowhere else. `answers` reads
`<task-id>\t<answer>\t<label>[\t<mode>]` lines on stdin and closes each named
task through the very same `answer` path above, so every guard applies
identically no matter which channel the answer arrived on. The key IS the
task id - no identity arithmetic. The optional fourth field selects the close:
empty or `done` completes the task, `release` lifts the hold so held work
resumes; anything else is skipped. A key that names no task, a task that is
not held for the captain, or a task already closed is reported as `skipped:`
and feeds nothing. A replayed delivery whose answer digest and requested
close mode both match the newest record is reported `closed:` and is a no-op;
a mode mismatch is skipped. The command exits nonzero when any key was
skipped. `--source` is provenance text recorded in the
durable decision, never a behavior switch: this command has no per-channel
branch and no knowledge of chat, review decks, or any transport.
Legacy input: an optional positional origin (or a stored concrete-origin
binding) makes a key that names no task fall back to the old
`<origin>-decision-<key>` identity, so an in-flight pre-collapse channel
keeps closing its rows; `--any-origin` and the stored `(any)` marker mean
what an absent origin means and are accepted for the same reason.

A channel's ONLY job is to turn whatever it received into those keyed lines
and pipe them here. It must never map keys to tasks, build decision records,
choose a close mode beyond what its card declared, or close anything itself.

`bind`, `unbind`, and `binding` record that a captured-answer SOURCE feeds
this intake, for any channel whose answers arrive detached from their origin
(a process-event source id, for example). The binding is a private record
under `state/decision-bindings/`; a source with no binding feeds nothing, so
this whole path is opt-in per source and an unbound source behaves as if it
did not exist. `bind` deliberately does not require the source to exist yet,
so a channel can be bound BEFORE it is armed. The optional second argument
exists only for legacy pre-collapse records and callers: a concrete origin is
stored verbatim and used as the composition fallback above, and
`--any-origin` stores the same `(any)` marker a plain `bind <source-id>`
stores. `binding` prints the stored value verbatim and `answers` accepts it,
so the process-event runner's feed seam is unchanged.

`complete` is the shared investigation and visual-review completion gate.
It attests, in the origin task's metadata, the reviewed inventory of
captain-held tasks that carry the origin's unresolved captain calls.
`--none` is an explicit semantic attestation that the just-reviewed surface
has no unresolved captain call, and is refused while the origin still has an
open keyed status decision. With a non-empty inventory, every listed task is
verified durable (actively captain-held, or closed with a recorded answer),
the inventory is unioned idempotently into the metadata, and every still-open
keyed status decision is transferred to its durable owner with a
`captain-held [key=...]` status close naming the inventory. Later review
passes may add ids. A post-teardown visual review can complete against the
surviving report and tasks without recreating task state.
`verify` is read-only and is called by scout teardown, so teardown cannot
erase a source before this gate has succeeded: every recorded inventory
entry must still be durable and no keyed status decision may be open.
Metadata compatibility: the attestation keeps the historical
`decisions_reviewed=1` and `decision_keys=` keys, and an inventory entry that
names no existing task resolves through the legacy `<origin>-decision-<entry>`
identity, so pre-collapse metadata written by fm-decision-hold.sh verifies
unchanged. An entry that exists as a task id is always that task.

`open` is the read-only predicate a MECHANICAL closer asks before it may
retire a task's row: is this task still an open captain call? It exits 0 when
it is, 1 when it is not, and 2 when the answer cannot be established, so a
caller that must never close a live call can treat "cannot tell" as its own
case instead of as a no. It prints nothing on 0 or 1 and mutates nothing. The
policy prefers holding the very work item a question gates, so the row a
cleanup is about to close is routinely the captain's own call; this predicate
is how that cleanup finds out. It is not a second closing rule: `answer`
remains the only act that closes a captain call.

`recover-retain` is what cleanup uses instead of closing. The work record that
discovered the call is going away, so recovery records the finished work's
deliverable in the task body and returns the row to Queued - nothing is
working on it any more, and a captain call reads as the captain's own only
while it is queued and held. The hold, and the requirement that only `answer`
closes it, are untouched. Recovery refuses an unanswered task that is not an
open captain call, and re-recording the same deliverable is a no-op. Every
refusal here is loud and leaves the call intact, because teardown reaches
recovery only on the path where its alternative would have been to close the
captain's own question. There is no standalone retention command: it had no
in-repo caller and could queue live work while leaving its worker metadata in
place, presenting it as an ownerless Captain's Call and violating the
meta/backlog invariant in bin/fm-backlog-transition-lib.sh.
All `hold`, `answer`, and `recover-retain` mutations serialize on the task
metadata lock. Teardown records replayable retention before cleanup, marks
cleanup complete afterward, releases its lock, and lets `recover-retain`
acquire that lock as the sole owner while it retains the current row and
removes the record. If `answer --release` won first, recovery preserves the
recorded answer and lifted hold but still returns the ownerless row to Queued
before removing the worker record.

`diverged` is the read-only guard over the seam between the two records of
one captain call. See "record divergence" beside command_diverged below.

Resolution records: the block written into the body names this script, the
decision digest, and a `Resolution mode:` of answered, released, or repaired.
Records written by the retired fm-decision-hold.sh (routed, declined,
answered, repaired) are recognized everywhere a record is read, so nothing
already closed needs rewriting.
[exit status: 2]

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • 🚨 bin/fm-captain-hold.sh:1033 - The required criterion says Queued retention preserves the meta/backlog invariant because teardown removes the worker record, but the advertised standalone retain command calls tasks_axi reopen and reports success without removing state/&lt;id&gt;.meta or requiring a retention marker. Concrete path: invoke retain on an active In-flight captain-held task; it becomes Queued and held while its live worker metadata remains, contradicting bin/fm-backlog-transition-lib.sh:7-16 and presenting active work as an ownerless Captain's Call. Guard command_retain at this earliest boundary so it can run only within an authorized retention transition, or authorize different standalone semantics; the remedy changes the accepted public command behavior and therefore needs user approval.

🔧 Fix: Remove unsafe standalone captain retention command
1 error still open:

  • 🚨 bin/fm-captain-hold.sh:463 - hold acquires the task lock but does not reject an existing &lt;id&gt;.backlog-close marker. Concrete sequence: ordinary teardown stages that marker, fails during cleanup and releases the lock; fm-captain-hold.sh hold &lt;id&gt; then succeeds on the still-open row; the next bootstrap replays the older close marker and closes the newly captain-held task without a recorded answer. Under the same lock, refuse creating/reasserting a hold while a pending close exists, before mutating the row, so an authorized close intent cannot survive underneath a new captain call.

🔧 Fix: Block captain holds over pending closes
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-captain-hold-lifecycle.test.sh
  • bin/fm-captain-hold.sh --help
  • bin/fm-captain-hold.sh retain sample-task --note finished (confirmed unsupported command exits 2)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (3): Last reviewed commit: "no-mistakes(document): Document pending-..." | Re-trigger Greptile

Comment thread bin/fm-captain-hold.sh
A scout that held its own work item for the captain - which is what
captain-hold-lifecycle prefers, "hold the work item the question gates" -
was closed by `bin/fm-teardown.sh`'s automatic backlog transition. The
completion gate passed, cleanup ran, and the captain's question moved to
Done with no recorded answer: the one thing the policy says must never
happen.

The contract was self-contradictory, so this fixes it at the design level
with one owner. `bin/fm-captain-hold.sh` gains the read-only `open`
predicate (0 still an open captain call, 1 not, 2 cannot tell) and the
`retain` command that carries a call across the removal of the work record
that discovered it: it records the finished work's deliverable in the task
body and returns the row to Queued, keeping the hold. Teardown asks `open`
before its close, and on a captain-held row defers only the close - every
other cleanup step runs as before, no pending-close record is staged (a
replay would close the call at the next session start), and "cannot tell"
refuses before any destructive step rather than reading as permission.
`--force` does not lift the deferral: it authorizes discarding unlanded
work, never the captain's question.

Queued-and-held is the shape every other captain call already has and what
`captain_actionable` requires, so the retained call lands in Bearings'
Captain's Call instead of reading as work still under way, and the paired
meta/backlog invariant is preserved rather than excepted.
`fm-captain-hold.sh answer` remains the only act that closes a captain
call.
…the established unsafe pending-close diagnostic, released-answer retention recovery queues the ownerless task without restoring its hold, and the lifecycle regression/docs reflect that invariant. Verified with fm-captain-hold-lifecycle, fm-backlog-atomicity, fm-lint.sh, and git diff --check
@kunchenguid
kunchenguid force-pushed the fm/fm-teardown-closes-captain-held-scout-r1 branch from e32f388 to 6a2abd7 Compare September 2, 2026 07:49
@kunchenguid kunchenguid changed the title fix: preserve captain-held tasks during teardown fix(bin): preserve captain calls during teardown Sep 2, 2026
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