Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ state/
data/
scratchpad*
.no-mistakes/
.omc/
.lavish/
.fm-secondmate-home
.fm-secondmate-parent
Expand Down
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Hard rules, in priority order:
You may maintain this repo's private operational state directly.
Shared tracked material is `AGENTS.md`, `README.md`, `CONTRIBUTING.md`, `.tasks.toml`, `.github/workflows/`, `bin/`, `.agents/skills/`, and public `skills/`.
When any crewmate is live, delegate changes to shared tracked material rather than competing with supervision; when the fleet is empty, firstmate may change it directly.
This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, and `.no-mistakes/` are captain-private and gitignored.
This repo is a shared template, while `.env`, `data/`, `state/`, `config/`, `projects/`, `.no-mistakes/`, and `.omc/` are captain-private and gitignored.
Ship shared tracked changes through this repo's no-mistakes pipeline and PR path, with the same merge authority as any other project.
Never add an agent name as a commit co-author.

Expand Down Expand Up @@ -158,6 +158,7 @@ state/ runtime records and signals; gitignored
.last-watcher-beat watcher liveness beacon, touched every poll (including while absorbing benign wakes); guard scripts read it
.subsuper-* .supervise-daemon.* sub-supervisor internals; never touch
.no-mistakes/ local validation state and evidence; gitignored
.omc/ local session and operator-console state; gitignored
```

A `state/<id>.status` line is a wake event, not current-state truth; `bin/fm-crew-state.sh` owns current-state reconciliation.
Expand Down
12 changes: 12 additions & 0 deletions bin/fm-fleet-snapshot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@
#
# --contribution-input prints only the canonical backlog/tasks ownership pair,
# without worker observations or cross-home collection, for the home-local poll.
# --backlog-json prints only the canonical backlog projection, without task
# metadata or merge-authority resolution, for home-local consumers that read
# backlog rows alone.
# Compatibility: JSON is the primary machine-readable surface.
# Human views must render this output instead of parsing state files again.
set -u
Expand Down Expand Up @@ -232,6 +235,9 @@ Print a structured snapshot of the firstmate fleet.
JSON is the stable machine-readable output contract. The default snapshot
refreshes only its parent-side remote-summary cache as an observational side effect.

--backlog-json emits the canonical local backlog projection only, without task
metadata or cross-home collection.

--contribution-input emits the canonical local backlog/tasks ownership pair only,
without worker observations or cross-home collection.

Expand Down Expand Up @@ -283,6 +289,7 @@ OUTPUT_MODE=json
case "${1:---json}" in
--json) ;;
--secondmate-home-summary) OUTPUT_MODE=secondmate-home-summary ;;
--backlog-json) OUTPUT_MODE=backlog_json ;;
--contribution-input) OUTPUT_MODE=contribution-input ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; exit 2 ;;
Expand Down Expand Up @@ -1972,6 +1979,11 @@ contribution_tasks_json() {
done | jq -s .
}

if [ "$OUTPUT_MODE" = backlog_json ]; then
printf '%s\n' "$BACKLOG_JSON"
exit 0
fi

if [ "$OUTPUT_MODE" = contribution-input ]; then
# Reuse the canonical backlog parser, without observing workers or other homes.
contribution_tasks=$(contribution_tasks_json) || { echo "fm-fleet-snapshot: contribution task read failed" >&2; exit 1; }
Expand Down
77 changes: 55 additions & 22 deletions bin/fm-hold-reverify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#
# Usage:
# fm-hold-reverify.sh [check] run one bounded re-verification sweep
# fm-hold-reverify.sh classify <facts.json> print the verdict for one hold's facts
# fm-hold-reverify.sh arm write and register the standing check
# fm-hold-reverify.sh disarm remove the standing check
# fm-hold-reverify.sh --help print this help
Expand All @@ -16,7 +15,8 @@
# remaining work was wrong. The rot concentrates in age.
#
# This script re-checks each aged captain hold against shipped reality and reports
# it in the SAME reconciliation vocabulary captain-hold-lifecycle already owns:
# it with one of four verdicts, each a proposal for the reconciliation seam
# captain-hold-lifecycle owns rather than a closure:
# dead / still_live / not_a_decision / unestablishable. It reports only. It never
# calls `answer` and never calls `reconcile close`/`reconcile note`, so it can
# never close a captain call; only the captain's own words or an explicit
Expand All @@ -37,8 +37,10 @@
#
# THE REPORT IS THE DELIVERABLE
# A sweep writes state/hold-reverify/docket.json (schema fm-hold-reverify-docket.v1)
# listing every examined hold with its verdict, structured evidence, and a short
# reason, and prints ONE line (the wake) only when the finding set changes.
# listing every examined hold with its verdict and the structured fields that
# decided it - the row's state and recorded hold reason, its recorded pull request
# and that request's state, and whether the row records a merged completion - and
# prints ONE line (the wake) only when the finding set changes.
# state/.hold-reverify stores the last sweep's epoch and a digest of the
# {id:verdict} set, mirroring state/.tool-updates, so a new or changed finding
# wakes once while an unchanged sweep stays silent. A sweep killed by the
Expand All @@ -62,9 +64,9 @@
#
# WHAT IT READS
# Aged holds come from the canonical local backlog projection rather than a second
# parser: `fm-fleet-snapshot.sh --contribution-input` reuses the canonical backlog
# parser WITHOUT observing workers or other homes, so the sweep stays local and
# bounded. A hold's recorded pull request is read through bin/fm-pr-lib.sh, which
# parser: `fm-fleet-snapshot.sh --backlog-json` reuses the canonical backlog
# parser WITHOUT task metadata, worker observations, or other homes, so the
# sweep stays local and bounded. A hold's recorded pull request is read through bin/fm-pr-lib.sh, which
# is the same gh-then-gh-axi path every other surface uses. A redundant local
# origin/main fetch is deliberately NOT performed: the forge merge state and the
# row's own recorded completion are the authoritative landing signals, and a clone
Expand Down Expand Up @@ -113,7 +115,6 @@ usage() {
cat <<'EOF'
Usage:
fm-hold-reverify.sh [check] run one bounded re-verification sweep
fm-hold-reverify.sh classify <facts.json> print the verdict for one hold's facts
fm-hold-reverify.sh arm write and register state/hold-reverify.check.sh
fm-hold-reverify.sh disarm remove the standing check, its trust binding, and the record
fm-hold-reverify.sh --help print this help
Expand Down Expand Up @@ -183,10 +184,17 @@ if [ "$BUDGET_SECS" -gt "$BUDGET_MAX" ]; then
BUDGET_CUT_FROM=$BUDGET_SECS
BUDGET_SECS=$BUDGET_MAX
fi
# The local projection is a fast bounded child of the same sweep budget, so it
# can never consume more than the sweep has left.
SNAPSHOT_BOUND=5
[ "$SNAPSHOT_BOUND" -le "$BUDGET_SECS" ] || SNAPSHOT_BOUND=$BUDGET_SECS
# The backlog-only projection is a bounded child of the same sweep budget. At
# large fleet sizes the contribution-input pair can spend most of its time on
# per-task merge-authority resolution the sweep never reads; backlog-json avoids
# that work (sub-second on the home that timed out at five seconds before).
# FM_HOLD_REVERIFY_BUDGET_SECS governs the projection bound; reserve probe time
# inside the sweep budget the same way BUDGET_MAX reserves it for FM_CHECK_TIMEOUT.
SNAPSHOT_BOUND=$BUDGET_SECS
if [ "$SNAPSHOT_BOUND" -gt "$((BUDGET_SECS - PROBE_MIN_SECS))" ]; then
SNAPSHOT_BOUND=$((BUDGET_SECS - PROBE_MIN_SECS))
fi
[ "$SNAPSHOT_BOUND" -ge 1 ] || SNAPSHOT_BOUND=1

# --- small helpers ----------------------------------------------------------

Expand Down Expand Up @@ -291,13 +299,15 @@ gather_facts() {
reason=$(printf '%s\n' "$hold" | jq -r '.hold_reason // ""')
pr_url=$(printf '%s\n' "$hold" | jq -r '.pr_url // ""')
merged=$(printf '%s\n' "$hold" | jq -r 'if .completion_merged == true then "true" else "false" end')
if [ -n "$pr_url" ]; then
if [ "$state" = "done" ] || [ -z "$reason" ]; then
pr_state=none
elif [ -n "$pr_url" ]; then
if fm_pr_url_parse "$pr_url" && [ "$FM_PR_PROVIDER" = github ] \
&& [ -n "$FM_PR_OWNER" ] && [ -n "$FM_PR_REPO" ] && [ -n "$FM_PR_NUMBER" ]; then
owner=$FM_PR_OWNER
repo=$FM_PR_REPO
number=$FM_PR_NUMBER
if out=$(read_record_bounded "$owner" "$repo" "$number" "$PROBE_SECS"); then
if out=$(read_record_bounded "$owner" "$repo" "$number" "$(probe_bound)"); then
record_state=${out%% *}
case "$record_state" in
MERGED) pr_state=merged ;;
Expand Down Expand Up @@ -333,6 +343,32 @@ SNAPSHOT_ERROR=

budget_exhausted() { [ "$(real_epoch)" -ge "$DEADLINE" ]; }

# probe_bound: clamp each forge read to the sweep budget remaining, so no probe
# can run past the end of the sweep (bin/fm-tool-update-check.sh probe_bound).
probe_bound() {
local left
left=$((DEADLINE - $(real_epoch)))
if [ "$left" -lt "$PROBE_MIN_SECS" ]; then
printf '%s\n' "$PROBE_MIN_SECS"
elif [ "$left" -lt "$PROBE_SECS" ]; then
printf '%s\n' "$left"
else
printf '%s\n' "$PROBE_SECS"
fi
}

snapshot_bound() {
local left bound=$SNAPSHOT_BOUND
if [ "$DEADLINE" -gt 0 ]; then
left=$((DEADLINE - $(real_epoch)))
if [ "$left" -lt "$bound" ]; then
bound=$left
fi
fi
[ "$bound" -ge 1 ] || bound=1
printf '%s\n' "$bound"
}

sweep_cleanup() {
[ -z "$FINDINGS_FILE" ] || rm -f -- "$FINDINGS_FILE"
FINDINGS_FILE=
Expand All @@ -343,15 +379,17 @@ snapshot_holds() {
local snapshot
snapshot=$(FM_HOME="$FM_HOME" FM_STATE_OVERRIDE="$STATE" FM_DATA_OVERRIDE="$DATA" \
FM_CONFIG_OVERRIDE="$CONFIG" \
fm_run_timed "$SNAPSHOT_BOUND" "$SNAPSHOT_BIN" --contribution-input 2>/dev/null) || return 1
fm_run_timed "$(snapshot_bound)" "$SNAPSHOT_BIN" --backlog-json 2>/dev/null) || return 1
[ -n "$snapshot" ] || return 1
printf '%s\n' "$snapshot" | jq -c --argjson age "$AGE_DAYS" '
(.backlog.records // [])[]
[(.records // [])[]
| select(.structured == true)
| select(.hold_kind == "captain")
| select(.hold_age_days != null and .hold_age_days >= $age)
| {id, title, state, hold_reason, hold_age_days, pr_url,
completion_merged: (.completion.verb == "merged")}' || return 1
completion_merged: (.completion.verb == "merged")}]
| sort_by(-.hold_age_days)
| .[]' || return 1
}

action_check() {
Expand Down Expand Up @@ -600,11 +638,6 @@ case "${1:-check}" in
[ "$#" -le 1 ] || die_usage "check takes no arguments"
action_check
;;
classify)
[ "$#" -eq 2 ] || die_usage "classify requires one facts JSON file"
[ -f "$2" ] && [ ! -L "$2" ] || die_usage "classify facts file is unavailable: $2"
classify_facts "$(cat "$2")"
;;
arm)
[ "$#" -eq 1 ] || die_usage "arm takes no arguments"
action_arm
Expand Down
5 changes: 3 additions & 2 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,8 @@ family_for_basename() {
printf '%s\n' afk
;;
fm-bearings-board-render.test.sh|fm-bearings-snapshot.test.sh|fm-contributions.test.sh|\
fm-fleet-snapshot-view.test.sh|fm-home-summary-refresh.test.sh)
fm-fleet-snapshot-view.test.sh|fm-home-summary-refresh.test.sh|\
fm-hold-reverify.test.sh)
printf '%s\n' snapshot-bearings
;;
fm-backend-cmux.test.sh|fm-backend-cmux-smoke.test.sh)
Expand Down Expand Up @@ -1563,7 +1564,7 @@ families_for_changed_path() {
printf '%s\n' live-harness-optin
;;
bin/fm-bearings-snapshot.sh|bin/fm-fleet-snapshot.sh|bin/fm-fleet-view.sh|bin/fm-contributions.sh|bin/fm-contributions.jq|\
bin/fm-home-summary-refresh.sh)
bin/fm-home-summary-refresh.sh|bin/fm-hold-reverify.sh)
printf '%s\n' snapshot-bearings
;;
bin/fm-install-herdr.sh|bin/fm-install-treehouse.sh|bin/fm-herdr-ci-cleanup.sh)
Expand Down
8 changes: 4 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -626,18 +626,18 @@ A budget that is not a whole number from 1 to 120 is still refused outright.
## Captain-hold re-verification

A captain call is an ordinary backlog task held for the captain, and its list rots with age: hundreds of holds had never been re-checked, so the captain's list was mostly ghosts and every count of remaining work was wrong.
`bin/fm-hold-reverify.sh` re-checks each aged hold against shipped reality and reports it in the reconciliation vocabulary `captain-hold-lifecycle` already owns: `dead`, `still_live`, `not_a_decision`, or `unestablishable`.
`bin/fm-hold-reverify.sh` re-checks each aged hold against shipped reality and reports it with one of four verdicts: `dead`, `still_live`, `not_a_decision`, or `unestablishable`.
It reports only.
It never calls `answer` and never closes or annotates a call, so only the captain's own words or an explicit evidence-backed reconciliation can resolve one.
It never calls `answer` and never closes or annotates a call, so only the captain's own words or an explicit evidence-backed reconciliation - the seam the `captain-hold-lifecycle` skill owns - can resolve one.
A hold is `dead` when shipped reality resolves the subject - its recorded pull request is merged, or its row records a merged completion.
It is `still_live` when the recorded pull request is open, `not_a_decision` when the row carries no live captain question (already Done, or no hold reason), and `unestablishable` otherwise.
`dead` is never inferred from absence or from an unreadable source, and a closed-unmerged pull request stays `unestablishable` rather than reading as dead.
Aged holds come from the canonical local backlog projection (`fm-fleet-snapshot.sh --contribution-input`), and a recorded pull request is read through `bin/fm-pr-lib.sh`; no second backlog parser and no redundant `origin/main` clone fetch are involved.
Aged holds come from the canonical local backlog projection (`fm-fleet-snapshot.sh --backlog-json`, which omits task metadata and merge-authority resolution), and a recorded pull request is read through `bin/fm-pr-lib.sh`; no second backlog parser and no redundant `origin/main` clone fetch are involved.

`check` is a plain custom watcher check, so it stays in the check-fires-then-firstmate-decides flow that the process-event `when` adapter explicitly excludes for an action whose right form depends on what the condition finds.
Arm it once per home with `bin/fm-hold-reverify.sh arm`, which writes `state/hold-reverify.check.sh` and binds its bytes with `bin/fm-check-register.sh` so the watcher dispatches it on its normal cadence and turns its one line into a `check:` wake.
`disarm` removes the shim, its trust binding, and the report record.
Each sweep writes `state/hold-reverify/docket.json` (schema `fm-hold-reverify-docket.v1`) with every examined hold's verdict, evidence, and reason, and prints one line only when the finding set changes.
Each sweep writes `state/hold-reverify/docket.json` (schema `fm-hold-reverify-docket.v1`) with every examined hold's verdict and the structured evidence it was decided from, and prints one line only when the finding set changes.
`state/.hold-reverify` records the sweep epoch and a digest of the `{id: verdict}` set, so a new or changed finding is reported once while an unchanged sweep stays silent.
A sweep the watcher kills writes no record and is retried.

Expand Down
1 change: 1 addition & 0 deletions docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-backlog-receive.sh` | Idempotently ingest one confined remote handoff outbox through tasks-axi |
| `fm-captain-hold.sh` | Hold tasks for the captain, record the captain's answers, gate investigation completion, and report record divergence between the status log and the backlog |
| `fm-decision-hold.sh` | One-release compatibility shim mapping the retired decision commands onto fm-captain-hold.sh |
| `fm-hold-reverify.sh` | Standing re-verification of aged captain holds: `arm` registers a watcher check that re-checks them against shipped reality and reports each as dead, still_live, not_a_decision, or unestablishable without ever closing a call, `disarm` removes it |
| `fm-brief.sh` | Scaffold ship (explicit `--mode`), scout, secondmate-charter, and Herdr-lab briefs, with Captain's intent and Firstmate spec subsections on ship/scout |
| [`fm-dod-lib.sh`](../bin/fm-dod-lib.sh) | Own ship/scout worker role scope, ship definitions of done, and the no-mistakes `--intent` contract |
| `fm-herdr-lab.sh` | Provision and guardedly operate an isolated, never-default Herdr lab session |
Expand Down
Loading
Loading