Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cf92d6d
docs(backlog): document retention-archived rows retire a pending close
rub-a-dub-dub Sep 19, 2026
e40915d
no-mistakes(review): treat a retention-archived row as the close tear…
rub-a-dub-dub Sep 19, 2026
17932d0
no-mistakes(review): report an absent backlog row distinctly from a l…
rub-a-dub-dub Sep 19, 2026
b4f6904
no-mistakes(review): keep the orphaned-cleanup warning on a retired a…
rub-a-dub-dub Sep 19, 2026
d52c3d0
no-mistakes(document): document accepted backlog-row absence on close…
rub-a-dub-dub Sep 20, 2026
187849a
no-mistakes(review): label a replayed close that found no row absent
rub-a-dub-dub Sep 20, 2026
3031be3
no-mistakes(document): align backlog-contract and bootstrap sweep doc…
rub-a-dub-dub Sep 20, 2026
4c800e4
no-mistakes(review): remove archive-proof component from backlog clos…
rub-a-dub-dub Sep 26, 2026
c71ba28
no-mistakes(review): deduplicate teardown absent backlog reminder wor…
rub-a-dub-dub Sep 26, 2026
1c5d276
no-mistakes(review): pin absent replay label in test; name both ROW_A…
rub-a-dub-dub Sep 26, 2026
0cbf5e7
no-mistakes(review): report retired absent close under BACKLOG_RECONC…
rub-a-dub-dub Sep 26, 2026
2a47d04
no-mistakes(review): soften replay wording to unconfirmed completion …
rub-a-dub-dub Sep 26, 2026
af66e58
no-mistakes(review): name the failed backlog read in unconfirmed-abse…
rub-a-dub-dub Sep 26, 2026
0db6260
no-mistakes(review): claim no absence when close reported none
rub-a-dub-dub Sep 26, 2026
0a28998
no-mistakes(review): align teardown link wording with session start
rub-a-dub-dub Sep 26, 2026
1889a5f
no-mistakes(document): align retired-close doc wording with softened …
rub-a-dub-dub Sep 27, 2026
5977a32
no-mistakes(document): correct refused-lookup set in retired-close ba…
rub-a-dub-dub Sep 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ When any diagnostic needs captain attention, report the plain consequence and re
Verify process reaping, the local-copy return, and endpoint closure without closing or lifting the captain's call, then reconcile any surviving resource.
- `BOOTSTRAP_INFO: the captain had already answered the call for <id> before cleanup finished; its endpoint or local copy may remain and should be reconciled` - the captain answered the held call before replay ran, so replay only finished the record side; the durable transition says physical cleanup was interrupted.
The answer stands - do not reopen or re-ask the call; verify process reaping, the local-copy return, and endpoint closure, then reconcile any surviving resource.
- `BACKLOG_RECONCILE: <id>: the recorded backlog close was retired because its backlog row had already left this backlog, so no close was left to land; its recorded completion link (<link>) could not be confirmed as applied and should be checked` - the row was already gone, so replay retired the record without landing any close.
The row left the backlog before this replay could read it, so whether an earlier close had already recorded the named link is unknowable here: the line reports it unconfirmed rather than lost. Check where the closed row would have carried it, file it only if it is missing, and do not treat the retirement as the end of it; the line ends `it recorded no completion link to reconcile` when the record carried none.
The line may also warn that the endpoint or local copy may remain after interrupted cleanup: verify process reaping, the local-copy return, and endpoint closure, then reconcile any surviving resource; do not re-close the row, and never read its absence as evidence that the physical cleanup finished.
- `BACKLOG_RECONCILE: <id>: recorded backlog close could not be replayed: <reason>` - this session start found a pending-close record carrying a close or retention transition but could not land it.
A valid teardown record proves the transition was authorized and recorded, but physical cleanup may be partial: verify process reaping, the local-copy return, and endpoint closure before assuming those resources are gone.
A validation error means the record cannot be trusted, so do not assume cleanup completed or follow any path or argument stored in it.
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ state/ runtime records and signals; gitignored
<id>.muse-session muse busy-source binding (sessions root plus task worktree) written by fm-spawn; removed by teardown
<id>.cursor-session cursor busy-source binding (projects root, task worktree, prior conversations) written by fm-spawn; removed by teardown
<id>.reconcile-nudged epoch second of the last inventory-reconcile nudge sent to this secondmate; bin/fm-secondmate-reconcile.sh owns its per-home cooldown window
<id>.backlog-close the exact backlog transition a teardown recorded before removing the task's record, so an interrupted cleanup can still be finished at the next session start; bin/fm-backlog-transition-lib.sh owns its format and replay, and a landed transition removes it, retiring a captain-held retain whose row is answered nowhere into <id>.backlog-reconcile instead of deleting it
<id>.backlog-close the exact backlog transition a teardown recorded before removing the task's record, so an interrupted cleanup can still be finished at the next session start; bin/fm-backlog-transition-lib.sh owns its format and replay, and the record is removed once its transition lands or its row has left the backlog entirely, retiring a captain-held retain whose row is answered nowhere into <id>.backlog-reconcile instead of deleting it
<id>.backlog-reconcile a retired retain's durable reconcile record, naming the deliverable a captain-held call's row left unclaimed when it was answered nowhere, live or archived; bin/fm-backlog-transition-lib.sh's fm_backlog_reconcile_marker_write creates it and bin/fm-bootstrap.sh re-reports it every session start, never deleting it on report; only bin/fm-backlog-reconcile.sh ack retires it
<id>.inbox/ durable steering inbox: sequenced firstmate instruction records the worker acknowledges by moving them into its handled/ subdirectory; written by fm-send, with ordinary records re-rung and escalated by the watcher while explicit fire-and-forget records are excluded from that ladder, and removed by teardown (bin/fm-task-inbox-lib.sh)
<id>.meta task metadata; each producer script's header owns its exact fields and mutation contract, with docs/configuration.md routing operator-facing backend and trace-context details
Expand Down Expand Up @@ -522,7 +522,7 @@ Work routed to a secondmate is recorded in that secondmate home's own backlog, n
A decision is simply a task held for the captain: create the task with `bin/fm-tasks-axi.sh add` when needed, then always hold it through `bin/fm-captain-hold.sh hold <id> --reason "<reason>"`, with `--until <date>` when the captain defers it.
When a main-side thread such as a pending captain decision or relay reminder is worth durable tracking, file it as its own work item and hold it through that wrapper.
Captain calls discovered by investigations or visual reviews follow `captain-hold-lifecycle`, which owns their completion gate and recorded-answer rules.
When the automatic transition gate applies, dispatch and completion move the item themselves - `bin/fm-spawn.sh` and `bin/fm-teardown.sh` own those transitions and refuse rather than report success without them - so what remains yours is filing the item before dispatch, recording decisions, and keeping notes current; `docs/configuration.md` owns gate applicability and the manual-backend exception.
When the automatic transition gate applies, dispatch and completion move the item themselves - `bin/fm-spawn.sh` and `bin/fm-teardown.sh` own those transitions and refuse rather than report a move they did not make - so what remains yours is filing the item before dispatch, recording decisions, and keeping notes current; `docs/configuration.md` owns gate applicability and every exception, including the manual backend and a row confirmed to have left the backlog entirely.
Re-evaluate queued work after every teardown and heartbeat, dispatching items only when dependencies and time gates have cleared.

`.tasks.toml`, `docs/configuration.md`, and current `tasks-axi --help` own the backlog schema, compatibility, retention, and routine command syntax.
Expand Down
94 changes: 81 additions & 13 deletions bin/fm-backlog-transition-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,19 @@
# replay would reject. The validator pins the data path to this home's configured
# root before any recovery mutation, then re-runs exactly that close.
# `tasks-axi done` on an already-closed task backfills links
# without moving the close date, so replay is idempotent. Spawn needs no marker:
# without moving the close date, so replay is idempotent. A row that has left
# the active backlog entirely - closed and aged out of done_keep retention, or
# removed outright - can never be closed again, so a confirmed NOT_FOUND from
# the row probe retires the record instead of recording a retry that can never
# land. That path never pretends a close landed: it reports the absence through
# FM_BACKLOG_CLOSE_ROW_ABSENT and names the completion link the retirement
# could not apply, so a merged PR or report is never discarded silently.
# Absence is whatever this home's configured backend answers: a NOT_FOUND from
# it is trusted, and nothing distinguishes a row that aged out from one this
# backlog never carried. Only a lookup ERROR - an unreadable backlog, an
# unresolvable or incompatible backend, any non-NOT_FOUND failure - is preserved
# as an error for a later retry.
# Spawn needs no marker:
# it publishes the meta first, so a crash
# leaves the meta itself as the evidence that the row is owed a start.
# A captain-held row uses the same record with a `mode=retain` line: replay then
Expand Down Expand Up @@ -95,9 +107,24 @@ FM_BACKLOG_ARCHIVE_ROW_RESULT=
FM_BACKLOG_ARCHIVE_ROW_ERROR=
# Set by fm_backlog_close_marker_replay: closed | closed_incomplete | retained |
# retained_incomplete | answered | answered_incomplete | retain_unresolved |
# stale | noop.
# absent | absent_incomplete | stale | noop. `absent` is a row that has left
# this backlog; `stale` is a record a newer incarnation superseded, which still
# owes its own close. The `_incomplete` twins carry the same outcome for a
# cleanup that never finished removing the endpoint or local copy.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
FM_BACKLOG_CLOSE_REPLAY_RESULT=
# Set by fm_backlog_close_transition, and by fm_backlog_close_marker_replay when
# it reaches an already-absent row: 1 when the row had already left the backlog,
# so the close it was asked for was accepted without one landing.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
FM_BACKLOG_CLOSE_ROW_ABSENT=0
# Set by fm_backlog_close_marker_replay with an absent result: the completion
# link the retired record carried, named by fm_backlog_retain_deliverable so the
# operator that result asks to reconcile is told which artifact the retirement
# could not apply, and empty when the record carried none.
# shellcheck disable=SC2034 # Output global, read by the sourcing caller.
FM_BACKLOG_CLOSE_REPLAY_DELIVERABLE=

# Bounded execution is fm-timeout-lib.sh's alone; source it rather than
# re-deriving a deadline here. It is stateless, so the memoisation reason this
# library does not source fm-tasks-axi-lib.sh does not apply.
Expand Down Expand Up @@ -1031,11 +1058,31 @@ fm_backlog_dispatch_rollback() {
return 0
}

# A close whose row the probe positively reports as gone from the backlog can
# never land, so the record retires rather than promising a retry. Only that
# confirmed NOT_FOUND is accepted, and the pending record survives every other
# answer: a probe that still finds the row keeps the original close failure
# alone, while a probe that cannot read the row reports that failure together
# with the read error, so the refusal says the item's existence was never
# settled instead of asserting an absence the close may never have reported.
fm_backlog_close_transition() {
local meta=$1 marker=$2 data=$3 id=$4 state=$5
local meta=$1 marker=$2 data=$3 id=$4 state=$5 close_error
shift 5
FM_BACKLOG_CLOSE_ROW_ABSENT=0
[ -z "$meta" ] || fm_backlog_record_remove "$meta" "task record" "$state" || return 1
fm_backlog_done "$data" "$id" "$@" || return 1
if ! fm_backlog_done "$data" "$id" "$@"; then
close_error=$FM_BACKLOG_TRANSITION_ERROR
if fm_backlog_row_probe "$data" "$id" \
|| [ "$FM_BACKLOG_ROW_RESULT" != not_found ]; then
if [ "$FM_BACKLOG_ROW_RESULT" = error ]; then
FM_BACKLOG_TRANSITION_ERROR="$close_error; this home's backlog row could not be read to confirm whether the item still exists ($FM_BACKLOG_ROW_ERROR), so the next session start retries this close"
else
FM_BACKLOG_TRANSITION_ERROR=$close_error
fi
return 1
fi
FM_BACKLOG_CLOSE_ROW_ABSENT=1
fi
fm_backlog_record_remove "$marker" "pending-close record" "$state"
}

Expand Down Expand Up @@ -1393,6 +1440,25 @@ fm_backlog_reconcile_marker_ack() { # <state-dir> <id>
fm_backlog_close_marker_remove "$marker" "$1"
}

# A close transition that reached a row already gone from the backlog retires
# the record without one landing, so label the replay by what it reached rather
# than by the transition having returned 0. Retiring that record discards the
# only durable copy of the completion link it carried, so name that link too:
# the caller cannot ask for a reconciliation it can no longer identify.
fm_backlog_close_replay_result() { # <cleanup-incomplete> [flag value]...
local incomplete=$1 outcome=closed
shift
if [ "$FM_BACKLOG_CLOSE_ROW_ABSENT" = 1 ]; then
outcome=absent
FM_BACKLOG_CLOSE_REPLAY_DELIVERABLE=$(fm_backlog_retain_deliverable "$@")
fi
if [ "$incomplete" = 1 ]; then
FM_BACKLOG_CLOSE_REPLAY_RESULT=${outcome}_incomplete
else
FM_BACKLOG_CLOSE_REPLAY_RESULT=$outcome
fi
}

# Replay one recorded close or retention. Returns 0 when the row is closed (or
# retained), the marker is stale, an answer already closed a retained row
# (live or discovered through the archive), or a retain marker's row is
Expand All @@ -1406,6 +1472,8 @@ fm_backlog_close_marker_replay() { # <state-dir> <marker-path> <authorized-data
local recorded_utc
local args=() mode_flags=()
FM_BACKLOG_CLOSE_REPLAY_RESULT=noop
FM_BACKLOG_CLOSE_REPLAY_DELIVERABLE=
FM_BACKLOG_CLOSE_ROW_ABSENT=0
fm_backlog_directory_present "$state" "state directory" || return 1
[ -e "$marker" ] || [ -L "$marker" ] || return 0
marker_name=${marker##*/}
Expand Down Expand Up @@ -1472,11 +1540,8 @@ fm_backlog_close_marker_replay() { # <state-dir> <marker-path> <authorized-data
fi
if fm_backlog_atomic_transition close '' "$marker" "$data" "$id" "$state" \
"${args[@]+"${args[@]}"}"; then
if [ "$cleanup_incomplete" = 1 ]; then
FM_BACKLOG_CLOSE_REPLAY_RESULT=closed_incomplete
else
FM_BACKLOG_CLOSE_REPLAY_RESULT=closed
fi
fm_backlog_close_replay_result "$cleanup_incomplete" \
"${args[@]+"${args[@]}"}"
return 0
fi
return 1
Expand Down Expand Up @@ -1513,8 +1578,12 @@ fm_backlog_close_marker_replay() { # <state-dir> <marker-path> <authorized-data
FM_BACKLOG_CLOSE_REPLAY_RESULT=retain_unresolved
return 0
fi
# The row this recorded close was for has left the backlog entirely, so
# nothing can ever land it; retiring the record is the only safe move.
FM_BACKLOG_CLOSE_ROW_ABSENT=1
fm_backlog_close_marker_remove "$marker" "$state" || return 1
FM_BACKLOG_CLOSE_REPLAY_RESULT=stale
fm_backlog_close_replay_result "$cleanup_incomplete" \
"${args[@]+"${args[@]}"}"
return 0
;;
esac
Expand All @@ -1526,10 +1595,9 @@ fm_backlog_close_marker_replay() { # <state-dir> <marker-path> <authorized-data
else
FM_BACKLOG_CLOSE_REPLAY_RESULT=retained
fi
elif [ "$cleanup_incomplete" = 1 ]; then
FM_BACKLOG_CLOSE_REPLAY_RESULT=closed_incomplete
else
FM_BACKLOG_CLOSE_REPLAY_RESULT=closed
fm_backlog_close_replay_result "$cleanup_incomplete" \
"${args[@]+"${args[@]}"}"
fi
return 0
fi
Expand Down
26 changes: 22 additions & 4 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,12 +92,16 @@
# (bin/fm-backlog-transition-lib.sh), so this sweep exists for the
# crash window inside those scripts and for drift a home was already
# carrying: it finishes the authoritative close or captain-call
# retention an interrupted cleanup recorded, and marks In flight any
# item this home already owns a worker for. The worker-record sweep
# retention an interrupted cleanup recorded, retires a close without
# landing anything when its backlog row has left the backlog entirely,
# and marks In flight any item this home already owns a worker for.
# bin/fm-backlog-transition-lib.sh owns those outcomes. The worker-record sweep
# never starts a captain-held or closed item, and reconciliation never
# reads or writes another home; the fleet snapshot's classifier and
# bin/fm-secondmate-reconcile.sh's nudge stay as backstops. Replayed
# transitions and restored In-flight rows print BOOTSTRAP_INFO facts.
# transitions and restored In-flight rows print BOOTSTRAP_INFO facts;
# a record retired without landing its close leaves an unapplied
# completion link, so it reports through BACKLOG_RECONCILE instead.
# The `code-root <file>` variant is a detect-only local check that runs
# even in a read-only session; detect_code_root_backlog_fork owns what
# it reports.
Expand Down Expand Up @@ -1261,7 +1265,7 @@ backlog_reconcile_record_report() {
# snapshot's classifier and bin/fm-secondmate-reconcile.sh's nudge stay as
# backstops for what this cannot see. Never reads or writes another home.
backlog_record_reconcile() {
local marker meta control_lock meta_lock id row label has_record=0 gate_status
local marker meta control_lock meta_lock id row label has_record=0 gate_status disposition
# A fresh home with no state directory has no physical task records to pair.
# Keep bootstrap diagnostics working without creating state just for a no-op.
[ -e "$STATE" ] || [ -L "$STATE" ] || return 0
Expand Down Expand Up @@ -1325,6 +1329,20 @@ backlog_record_reconcile() {
# landed) and every session start after it reaches too, not just this
# one.
;;
absent|absent_incomplete)
if [ -n "$FM_BACKLOG_CLOSE_REPLAY_DELIVERABLE" ]; then
disposition="its recorded completion link ($FM_BACKLOG_CLOSE_REPLAY_DELIVERABLE) could not be confirmed as applied and should be checked"
else
disposition="it recorded no completion link to reconcile"
fi
if [ "$FM_BACKLOG_CLOSE_REPLAY_RESULT" = absent_incomplete ]; then
disposition="$disposition, and its endpoint or local copy may remain and should be reconciled"
fi
echo "BACKLOG_RECONCILE: $label: the recorded backlog close was retired because its backlog row had already left this backlog, so no close was left to land; $disposition"
;;
stale)
echo "BOOTSTRAP_INFO: discarded a pending close for $label recorded by a superseded incarnation; the incarnation now on record still owes its own close"
;;
esac
else
echo "BACKLOG_RECONCILE: $label: recorded backlog close could not be replayed: $FM_BACKLOG_TRANSITION_ERROR"
Expand Down
Loading
Loading