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
2 changes: 1 addition & 1 deletion .agents/skills/agent-skill-trigger-index/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ metadata:

These skills are not captain-invocable; load them only at their precise triggers.

- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `PRESENTATION_UNAVAILABLE:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `HOME_SUMMARY:`, `BACKLOG_RECONCILE:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`), or when `BOOTSTRAP_INFO:` says an interrupted backlog cleanup may have left an endpoint or local copy; silence and other `BOOTSTRAP_INFO:` facts need no load.
- `bootstrap-diagnostics` - load whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line (`MISSING:`, `MISSING_MANUAL:`, `PRESENTATION_UNAVAILABLE:`, `BACKEND_INVALID:`, `NEEDS_GH_AUTH`, `TANGLE:`, `STARTUP_MEMORY_BUDGET:`, `CREW_DISPATCH: invalid`, `FLEET_SYNC:`, `NETWORK_CHECKS:`, `HOME_SUMMARY:`, `BACKLOG_RECONCILE:`, `SECONDMATE_SYNC:`, `SECONDMATE_LIVENESS:`, `SECONDMATE_HANDOFF:`, `NUDGE_SECONDMATES:`, or `FMX:`), or when `BOOTSTRAP_INFO:` says an interrupted backlog cleanup may have left an endpoint or local copy, or on a `check: home-summary-refresh` wake; silence and other `BOOTSTRAP_INFO:` facts need no load.
- `diagnostic-reasoning` - load before scoping a reported bug and before acting on a diagnostic report, when the captain asks what agents keep getting wrong or for a repeat-mistake sweep, and when a second instance of an already-seen mistake class surfaces.
- `ask-user-authority` - load before deciding any ask-user finding.
- `quota-array-dispatch` - load before choosing among a matched crew-dispatch profile array from current quota-axi default TOON.
Expand Down
6 changes: 3 additions & 3 deletions .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: bootstrap-diagnostics
description: >-
Agent-only handling playbook for session-start bootstrap diagnostics.
Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, PRESENTATION_UNAVAILABLE, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, STARTUP_MEMORY_BUDGET, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, HOME_SUMMARY, BACKLOG_RECONCILE, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or reports that an interrupted backlog cleanup may have left an endpoint or local copy, or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines.
Use whenever the session-start digest's bootstrap or network-checks section prints an actionable diagnostic line - MISSING, MISSING_MANUAL, PRESENTATION_UNAVAILABLE, BACKEND_INVALID, NEEDS_GH_AUTH, TANGLE, STARTUP_MEMORY_BUDGET, CREW_DISPATCH invalid, FLEET_SYNC, NETWORK_CHECKS, HOME_SUMMARY, BACKLOG_RECONCILE, SECONDMATE_SYNC, SECONDMATE_LIVENESS, SECONDMATE_HANDOFF, NUDGE_SECONDMATES, or FMX - or reports that an interrupted backlog cleanup may have left an endpoint or local copy, or when a standalone bin/fm-bootstrap.sh or bin/fm-startup-network.sh run prints one of those lines, or on a `check: home-summary-refresh` wake.
A silent bootstrap section, or any other BOOTSTRAP_INFO fact, means no skill load.
user-invocable: false
metadata:
Expand Down Expand Up @@ -45,9 +45,9 @@ When any diagnostic needs captain attention, report the plain consequence and re
- `FLEET_SYNC: <repo>: STUCK: on <state>, N commits behind <base> - needs attention` - the clone is dirty, on a non-default branch, detached with unique commits, or diverged, so the sync left it untouched (never forcing or discarding); it will keep falling behind until you look.
A loud STUCK, especially a growing N across bootstraps, means that clone needs hands-on attention; dispatch a crewmate or resolve it before it strands work.
- `HOME_SUMMARY: this home has never published state/home-summary.json` or `... has not been republished since <stamp>` - this home's structured summary publication has failed repeatedly, and the line carries the failure count and the newest recorded reason from `state/.home-summary-refresh.log`.
Publication is deliberately best-effort, so it cannot change another session-start, spawn, teardown, or watcher-poll result, and the watcher runs it detached so a slow attempt cannot delay the liveness beacon.
Handle a `check: home-summary-refresh` wake through this same diagnostic procedure; [`bin/fm-home-summary-refresh.sh`](../../../bin/fm-home-summary-refresh.sh)'s header owns publication, failure accounting, and escalation.
Read the named record for the recorded reasons, then reproduce with a direct `bin/fm-home-summary-refresh.sh` (no `--best-effort`, which is what keeps the failure quiet) so the refresh error reaches you.
A recorded deadline means the complete refresh did not finish inside `FM_HOME_SUMMARY_TIMEOUT`, so inspect lock acquisition and producer completion before validation or publication, and fix the blocked phase rather than raising this load-bearing bound.
A recorded deadline means initialization or the refresh did not finish inside its configured bound, so inspect state-directory access, lock acquisition, and producer completion before validation or publication, and fix the blocked phase rather than raising this load-bearing bound.

- `BOOTSTRAP_INFO: closed the backlog item for <id> after interrupted cleanup; its endpoint or local copy may remain and should be reconciled` - replay closed the item, but the durable transition says physical cleanup was interrupted.
Verify process reaping, the local-copy return, and endpoint closure, then reconcile any surviving resource.
Expand Down
2 changes: 1 addition & 1 deletion .agents/skills/operational-home-layout/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ state/ runtime records and signals; gitignored
.startup-network.* status, report, per-step elapsed timings, inline-print claim, and lock for the deferred startup stage that runs network checks and the inactive-outcome scan off the digest's blocking path; bin/fm-startup-network.sh
.wake-queue durable queued wakes retained until post-handling acknowledgement: epoch<TAB>seq<TAB>kind<TAB>key<TAB>payload
.watcher-down private generation-bound recovery state coupling watcher downtime, durable wake presentation, and post-handling acknowledgement; never touch
.<id>.open-decisions-cursor per-task byte cursor and folded open-decision set bounding the OPEN DECISIONS scan's cost to new status-log appends; written only by fm-classify-lib.sh's status_open_decisions_incremental, removed by teardown, safe to delete (forces one full re-fold)
.<id>.open-decisions-cursor per-task fold checkpoint; format and reuse contract: bin/fm-status-decision-lib.sh; live persistence and scratch-copy carry: bin/fm-classify-lib.sh; removed by teardown, safe to delete (forces one full re-fold)
.<id>.home-appends per-task ledger of byte ranges this home itself appended as bookkeeping closes, so a wake scan can tell its own growth from a foreign write instead of waking on it; presentation is unaffected, so both the signal annotation and UNREAD STATUS still print those lines; written only by fm-status-wake-lib.sh's status_home_appends_record; its sibling .<id>.home-appends.lock serializes that ledger's read-merge-write; both removed by teardown, safe to delete
.status-presentation-cursor .status-presentation-lock fleet-wide per-task status identity plus independent annotation and outcome-backstop byte offsets, with a serialization lock preventing already-presented lines from replaying while preserving delayed signal annotations; owned by fm-classify-lib.sh, with each task's row retired by teardown
.afk-contract the away-posture record: the captain's verbatim away words, expected return, reach profile, and spend cap; written only by bin/fm-afk-contract.sh in the same turn as /afk, archived under afk-contracts/ at return; its presence IS the away posture in every harness; its sibling .afk-contract.lock serializes actions authorized by the live record (contract: bin/fm-afk-contract.sh)
Expand Down
19 changes: 15 additions & 4 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -607,11 +607,22 @@ fm_backend_herdr_projection_journal_create() { # <state-dir> <task-id>
printf '%s' "$token"
}

# One read of the journal with no child process: the snapshot below asks for a
# dozen fields of each journal, and a locked session start walks every journal,
# so a grep-and-cut pair per field was hundreds of process creations.
fm_backend_herdr_projection_journal_field() { # <journal> <key>
local journal=$1 key=$2 count
count=$(grep -c "^${key}=" "$journal" 2>/dev/null || true)
[ "$count" = 1 ] || return 1
grep "^${key}=" "$journal" 2>/dev/null | cut -d= -f2-
local journal=$1 key=$2 line count=0 value=
[ -f "$journal" ] && [ -r "$journal" ] || return 1
while IFS= read -r line || [ -n "$line" ]; do
case "$line" in
"$key="*)
count=$((count + 1))
value=${line#"$key="}
;;
esac
done < "$journal"
[ "$count" -eq 1 ] || return 1
printf '%s\n' "$value"
}

# fm_backend_herdr_projection_journal_snapshot: validate a version 1 attempt
Expand Down
97 changes: 92 additions & 5 deletions bin/fm-backlog-transition-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ fm_backlog_bytes_of_file() { # <path>
perl -e 'open(my $f, "<", $ARGV[0]) or exit 1; binmode $f; local $/; my $c = <$f>; $c = "" unless defined $c; print join(" ", unpack("C*", $c)), "\n"' -- "$1"
}

# 0 when <string> holds no control byte (below 0x20, or 0x7f), newline included.
# The byte-by-byte checks above need a child process each; this answers the same
# question for a string already in the shell, in the C locale so that multibyte
# characters are judged by their bytes exactly as unpack("C*") judges them.
fm_backlog_string_control_free() { # <string>
local LC_ALL=C
case "$1" in
*[[:cntrl:]]*) return 1 ;;
*) return 0 ;;
esac
}

fm_backlog_control_bytes_valid() { # <allow-newline: 0|1> <od-bytes>
printf '%s\n' "$2" | awk -v allow_newline="$1" '
{ for (i = 1; i <= NF; i++) if (($i < 32 && !(allow_newline && $i == 10)) || $i == 127) exit 1 }
Expand Down Expand Up @@ -143,9 +155,8 @@ fm_backlog_directory_present() {
}

fm_backlog_data_absolute() {
local data=$1 raw_bytes check
raw_bytes=$(fm_backlog_bytes_of_string "$data") || return 1
if ! fm_backlog_control_bytes_valid 0 "$raw_bytes"; then
local data=$1 check
if ! fm_backlog_string_control_free "$data"; then
printf 'error: data directory contains an invalid control byte\n' >&2
return 2
fi
Expand Down Expand Up @@ -443,6 +454,60 @@ fm_backlog_row_list() { # <resolved-data-dir> [flag...]
fi
}

# One bounded read of every backlog row's id and state, printed as one
# "<id>:<state>" line per row, for a caller that must decide which of many task
# records can need a closer look. A per-record `tasks-axi show` is a node process
# each, so asking once replaces a start-time cost that grew with the number of
# workers. Fails (nothing printed) whenever it cannot vouch for a complete
# answer - addressing, the bound (status 124, so the caller can latch a wedged
# backend), the backend, or a row count that does not match what was parsed - and
# the caller then probes each record exactly as it did before.
fm_backlog_row_states() { # <data-dir>
local data=$1 out secs=${FM_BACKLOG_ROW_TIMEOUT_SECS:-10} line expected='' declared='' in_tasks=0 parsed=0 id rest rows='' status
local -a args=()
case "$secs" in ''|*[!0-9]*) secs=10 ;; esac
[ "$secs" -gt 0 ] 2>/dev/null || secs=10
data=$(fm_backlog_data_absolute "$data") || return 1
fm_backlog_tasks_axi_addressing "$data" || return 1
[ "$FM_BACKLOG_ROW_SHOW_WEDGED" != 1 ] || return 1
[ -z "$FM_BACKLOG_AXI_FILE" ] || args+=(--file "$FM_BACKLOG_AXI_FILE")
# shellcheck disable=SC2016 # Expansion is deliberately deferred to the child shell.
out=$(fm_run_timed "$secs" bash -c 'cd "$1" 2>/dev/null || exit 1; shift; exec tasks-axi list "$@"' \
_ "$FM_BACKLOG_AXI_ROOT" ${args[@]+"${args[@]}"} 2>/dev/null)
status=$?
[ "$status" -eq 0 ] || return "$status"
while IFS= read -r line; do
case "$line" in
'count: '*) expected=${line#count: } ;;
'tasks['*']{id,state,'*'}:')
declared=${line#'tasks['}
declared=${declared%%']'*}
case "$declared" in ''|*[!0-9]*) return 1 ;; esac
in_tasks=1
;;
' '*,*,*)
[ "$in_tasks" = 1 ] || continue
id=${line# }
rest=${id#*,}
id=${id%%,*}
case "$id" in \"*\") id=${id#\"}; id=${id%\"} ;; esac
case "$id" in ''|.*|*[!A-Za-z0-9._-]*) return 1 ;; esac
rest=${rest%%,*}
[ -n "$rest" ] || return 1
rows="${rows}${id}:${rest}"$'\n'
parsed=$((parsed + 1))
;;
*) in_tasks=0 ;;
esac
done <<EOF
$out
EOF
case "$expected" in ''|*[!0-9]*) return 1 ;; esac
[ "$expected" = "$declared" ] || return 1
[ "$expected" -eq "$parsed" ] || return 1
printf '%s' "$rows"
}

fm_backlog_row_probe() { # <data-dir> <id>
local data authorized_data=$1 id=$2 out state held blocked hold_kind command_status source_status
if ! data=$(fm_backlog_data_absolute "$1"); then
Expand Down Expand Up @@ -727,12 +792,34 @@ fm_backlog_retain() { # <data-dir> <id> [flag...]
fm_backlog_mutate "$authorized_data" reopen "$id"
}

fm_backlog_canonical_existing() {
# The canonical path of an existing path. Bootstrap and every lifecycle script
# call this several times per task record, and a perl process per call dominated
# a locked session start. A directory, and a regular file that is not itself a
# symlink, resolve in the shell with the same answer; anything else (a symlink
# or a path the shell cannot classify) still goes to realpath.
fm_backlog_canonical_existing() { # <path>
local path=$1 dir base resolved
if [ -d "$path" ]; then
resolved=$(CDPATH='' cd -P -- "$path" 2>/dev/null && pwd -P) && [ -n "$resolved" ] && {
printf '%s' "$resolved"
return 0
}
elif [ -e "$path" ] && [ ! -L "$path" ]; then
dir=${path%/*}
base=${path##*/}
if [ "$dir" != "$path" ] && [ -n "$base" ]; then
[ -n "$dir" ] || dir=/
resolved=$(CDPATH='' cd -P -- "$dir" 2>/dev/null && pwd -P) && [ -n "$resolved" ] && {
printf '%s/%s' "${resolved%/}" "$base"
return 0
}
fi
fi
LC_ALL=C perl -MCwd=realpath -e '
my $resolved = realpath($ARGV[0]);
exit 1 unless defined $resolved;
print $resolved;
' "$1" 2>/dev/null
' "$path" 2>/dev/null
}

fm_backlog_record_parent_authorized() { # <path> <label> <root> [parent-only]
Expand Down
28 changes: 27 additions & 1 deletion bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,7 @@ crew_dispatch_validate() {
# 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 row_states have_row_states
# 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 @@ -1274,6 +1274,22 @@ backlog_record_reconcile() {
break
done
[ "$has_record" = 1 ] || return 0
# Only a queued row is ever healed below, so one read of every row's state lets
# a record whose item exists and is not queued skip its own backlog read. A
# record with no row in the answer, or a queued one, is still probed exactly as
# before, and a read that cannot be trusted probes every record.
row_states=
have_row_states=0
if row_states=$(fm_backlog_row_states "$DATA"); then
have_row_states=1
# Command substitution drops the final newline; restore it so the last row
# is delimited on both sides like every other.
row_states=$'\n'"$row_states"$'\n'
else
# A bound hit here means the backend is wedged. Latch it, as every other
# bounded read does, so the per-record reads below do not wait it out again.
[ "$?" -ne 124 ] || FM_BACKLOG_ROW_SHOW_WEDGED=1
fi
for meta in "$STATE"/*.meta; do
[ -e "$meta" ] || [ -L "$meta" ] || continue
if ! fm_backlog_record_present "$meta" "task record" "$STATE"; then
Expand Down Expand Up @@ -1302,6 +1318,16 @@ backlog_record_reconcile() {
if [ "$(fm_meta_get "$meta" kind)" != secondmate ] \
&& [ "$(fm_meta_get "$meta" cleanup_recovery)" != orca ]; then
row=
if [ "$have_row_states" = 1 ]; then
case "$row_states" in
*$'\n'"$id:queued"$'\n'*) ;;
*$'\n'"$id:"*)
fm_lock_release "$meta_lock"
fm_lock_release "$control_lock"
continue
;;
esac
fi
if fm_backlog_row_probe "$DATA" "$id"; then
row=$FM_BACKLOG_ROW_STATE
elif [ "$FM_BACKLOG_ROW_RESULT" != not_found ]; then
Expand Down
Loading
Loading