From fbf0bb807bb5f23aa598f123b6b8bfef257243a6 Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 16:47:17 +0700 Subject: [PATCH 1/9] feat: automatic /stow via session-start re-emit and heartbeat staleness gates Adds the two triggers from data/fm-auto-stow/report.md so the captain no longer has to type /stow to keep memory current, without a new daemon, watcher, or cascade: - bin/fm-session-start.sh prepends a STOW DUE line to a compact/clear session-start re-emit when state/.last-stow is missing or older than FM_AUTO_STOW_INTERVAL_SECS (default ~24h), silent when current. - AGENTS.md section 8 rule 4 now also checks that same marker on a heartbeat wake, using the same larger-than-heartbeat interval, so a pass runs at most once per interval rather than on every wake. - The stow skill touches state/.last-stow only at the end of a pass it can call reset-safe, mirroring state/.last-heartbeat's bare-mtime marker. Away-mode heartbeats stay bash-only and unaffected: they never reach an LLM turn to run /stow in, per the existing away-daemon design. --- .agents/skills/stow/SKILL.md | 3 + AGENTS.md | 2 + bin/fm-session-start.sh | 37 +++++++++ docs/configuration.md | 1 + tests/fm-session-start.test.sh | 135 +++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+) diff --git a/.agents/skills/stow/SKILL.md b/.agents/skills/stow/SKILL.md index 348a997547..6ae167575a 100644 --- a/.agents/skills/stow/SKILL.md +++ b/.agents/skills/stow/SKILL.md @@ -300,6 +300,9 @@ Extend the completion receipt with one entry per secondmate alongside the primar Keep those entries in the same plain captain-facing language the rest of the receipt uses. The session is reset-safe only when every home is within its own budget with no unresolved exception. +When, and only when, the whole pass for this home - including the cascade above in a primary home - is reset-safe, touch `state/.last-stow` (`touch state/.last-stow`) as its true final step. +That bare-mtime marker mirrors `state/.last-heartbeat` (`bin/fm-watch.sh`) and is the single durable record the automatic `/stow` triggers in `AGENTS.md` read to decide whether a pass is due; never touch it when reset-safe cannot be claimed. + ## Scope exclusion: no skill storage by the pass The stow pass itself must never store, create, or edit a skill as a destination for any finding. diff --git a/AGENTS.md b/AGENTS.md index a50f6afe5c..69cb6bc9b8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,6 +134,7 @@ state/ runtime records and signals; gitignored .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .writing-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch .watch-triage.log watcher's absorbed-wake debug log (size-capped); never relied on, safe to delete .last-watcher-beat watcher liveness beacon, touched every poll (including while absorbing benign wakes); guard scripts read it + .last-stow bare-mtime marker touched only by the stow skill, only at the end of a reset-safe pass; read by fm-session-start.sh's compact/clear re-emit and by section 8 rule 4's heartbeat check to gate automatic /stow .subsuper-* .supervise-daemon.* sub-supervisor internals; never touch .no-mistakes/ local validation state and evidence; gitignored ``` @@ -409,6 +410,7 @@ Handle actionable wakes as follows: 2. For `stale:`, inspect the recorded endpoint and load `stuck-crewmate-recovery` for a stopped, looping, confused, or unresponsive worker; a deep-inspection reason also requires current-state and validation-log inspection. 3. For `check:`, act on the named poll result, including merges, Relay events, process-to-event source results, and captain inbox notes; a handled inbox note is also acknowledged with `bin/fm-inbox.sh drain --ack `, or it stays counted as still waiting for firstmate. 4. For `heartbeat:`, review the whole fleet from the structured fleet view, reconcile suspicious tasks and PR state, update the backlog, and never report an unchanged fleet as progress. + Also check `state/.last-stow`'s age against `FM_AUTO_STOW_INTERVAL_SECS` (default ~24h, a separate and larger clock than the heartbeat's own cadence); when due, run `/stow` first, before the rest of this review, so an automatic pass does not run on every heartbeat. When any wake reports a merged PR for a project cloned in this home, refresh that clone through the guarded fleet-sync path. When Relay-linked work reaches a milestone or terminal state, load `fmx-respond`; before terminal teardown, use its promised-final reconciliation when a typed public commitment exists, otherwise post the final completion follow-up so the link clears even if earlier follow-ups were spent. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 9eb50b4263..6202073738 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -335,6 +335,8 @@ PRIMARY_HARNESS=$("$SCRIPT_DIR/fm-harness.sh" 2>/dev/null || printf unknown) . "$SCRIPT_DIR/fm-wake-lib.sh" # shellcheck source=bin/fm-line-cap-lib.sh . "$SCRIPT_DIR/fm-line-cap-lib.sh" +# shellcheck source=bin/fm-supervision-lib.sh +. "$SCRIPT_DIR/fm-supervision-lib.sh" # One tasks-axi compatibility verdict per session start. The probe costs three # tasks-axi subprocesses and this digest needs the same answer twice - here for @@ -351,6 +353,40 @@ QUEUED_LIMIT=${FM_SESSION_START_QUEUED_LIMIT:-20} case "$QUEUED_LIMIT" in ''|*[!0-9]*|0) QUEUED_LIMIT=20 ;; esac BACKLOG_FIELDS=blocked_by,hold_kind,hold_reason +# Automatic /stow, trigger 1 (the compact/clear re-emit path below): a +# staleness gate on state/.last-stow, touched only by the stow skill itself at +# the end of a reset-safe pass (mirrors state/.last-heartbeat's bare-mtime +# marker, bin/fm-watch.sh). Read only here, never written by this script. +# Trigger 2 is the heartbeat-handling check in AGENTS.md section 8 rule 4, +# which reads the same marker against the same interval. +STOW_INTERVAL=${FM_AUTO_STOW_INTERVAL_SECS:-86400} +case "$STOW_INTERVAL" in ''|*[!0-9]*|0) STOW_INTERVAL=86400 ;; esac + +# stow_due_line: one "STOW DUE: ..." line when state/.last-stow is missing or +# at least STOW_INTERVAL seconds old, silent (prints nothing, exit 0) when +# current. Detect-only and cheap - a single mtime stat - matching the "always +# check, only speak up when it matters" idiom the bootstrap stage already uses. +stow_due_line() { + local marker="$STATE/.last-stow" m age + if [ -e "$marker" ]; then + m=$(fm_sup_stat_mtime "$marker" 2>/dev/null) + if [ -n "$m" ]; then + age=$(( $(date +%s) - m )) + else + age=999999 + fi + else + age=999999 + fi + [ "$age" -ge "$STOW_INTERVAL" ] || return 0 + if [ -e "$marker" ]; then + printf 'STOW DUE: last /stow pass was %ss ago (over the %ss interval, source=%s); run /stow before other work.\n' \ + "$age" "$STOW_INTERVAL" "${SESSION_SOURCE:-unknown}" + else + printf 'STOW DUE: no recorded /stow pass (source=%s); run /stow before other work.\n' "${SESSION_SOURCE:-unknown}" + fi +} + RULE='================================================================================' SUBRULE='--------------------------------------------------------------------------------' @@ -606,6 +642,7 @@ if [ "$REEMIT" -eq 0 ] && [ "$SESSION_SOURCE" = startup ]; then fi if [ "$REEMIT" -eq 1 ]; then + stow_due_line section "SESSION START (CONTEXT RE-EMIT) - $FM_HOME" printf 'This session already took the helm at its own startup and has only lost its\n' printf 'context. Lock ownership is re-verified and the durable records below are\n' diff --git a/docs/configuration.md b/docs/configuration.md index df27bcbda2..be49c5e9d3 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -649,6 +649,7 @@ FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops CMUX_SOCKET_PASSWORD= # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md) FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest; each line is capped by bin/fm-line-cap-lib.sh FM_SESSION_START_QUEUED_LIMIT=20 # plain queued backlog rows in the session-start digest; in-flight, held, and blocked rows are never bounded and done rows are never listed +FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow: gates the STOW DUE line on a compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow's mtime, touched only by the stow skill at the end of a reset-safe pass FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording FM_BOOTSTRAP_NETWORK=all # internal session-start phase split: all, skip (local steps only), or only (network steps only); see bin/fm-bootstrap.sh FM_STARTUP_NETWORK_TIMEOUT=120 # seconds bounding the whole deferred network stage; hitting it prints an actionable NETWORK_CHECKS line diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index e74eceb7ab..ea6e2ee457 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2016,6 +2016,135 @@ EOF pass "--reemit reprints the digest without repeating startup's mutating sweeps and still drains queued wakes" } +# --- automatic /stow trigger 1: STOW DUE on compact/clear re-emit ------------ +# A staleness gate on state/.last-stow that prepends one STOW DUE line to a +# compact/clear re-emit, silent when the marker is current. These exercise the +# real digest's public output only - never source bytes. + +run_reemit_for_stow() { # [source] + local home=$1 root=$2 path=$3 source=${4:-compact} + FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_FAKE_HARNESS_PID=$$ PATH="$path" \ + env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \ + "$SESSION_START" --reemit --source "$source" +} + +test_stow_due_prepended_when_marker_absent_on_reemit() { + local rec root home fakebin out first_line + rec=$(new_world stow-due-absent) + IFS='|' read -r root home fakebin < Date: Mon, 24 Aug 2026 17:05:07 +0700 Subject: [PATCH 2/9] no-mistakes(review): gate auto-stow on attempt marker, lock ownership --- .agents/skills/stow/SKILL.md | 6 +- AGENTS.md | 5 +- bin/fm-session-start.sh | 56 ++++++------ docs/configuration.md | 2 +- tests/fm-session-start.test.sh | 150 ++++++++++++++++++++++++++++----- 5 files changed, 163 insertions(+), 56 deletions(-) diff --git a/.agents/skills/stow/SKILL.md b/.agents/skills/stow/SKILL.md index 6ae167575a..d025df616f 100644 --- a/.agents/skills/stow/SKILL.md +++ b/.agents/skills/stow/SKILL.md @@ -300,8 +300,10 @@ Extend the completion receipt with one entry per secondmate alongside the primar Keep those entries in the same plain captain-facing language the rest of the receipt uses. The session is reset-safe only when every home is within its own budget with no unresolved exception. -When, and only when, the whole pass for this home - including the cascade above in a primary home - is reset-safe, touch `state/.last-stow` (`touch state/.last-stow`) as its true final step. -That bare-mtime marker mirrors `state/.last-heartbeat` (`bin/fm-watch.sh`) and is the single durable record the automatic `/stow` triggers in `AGENTS.md` read to decide whether a pass is due; never touch it when reset-safe cannot be claimed. +When, and only when, the whole pass for this home - including the cascade above in a primary home - is reset-safe, touch `state/.last-stow` (`touch state/.last-stow`); never touch it when reset-safe cannot be claimed. +Then touch `state/.last-stow-attempt` (`touch state/.last-stow-attempt`) as the pass's true final step, unconditionally, on every `/stow` invocation - reset-safe or not, and whatever exceptions stayed unresolved. +Both are bare-mtime markers mirroring `state/.last-heartbeat` (`bin/fm-watch.sh`): `state/.last-stow` records the last fully reset-safe pass, while `state/.last-stow-attempt` records that a pass ran at all and is the marker the automatic `/stow` triggers in `AGENTS.md` read to decide whether another pass is due. +A home carrying a sticky exception it cannot clear on its own - a `deferred` secondmate, an unresolved over-budget home, a shared preference still routing to the primary - therefore stays throttled to one automatic pass per interval instead of re-running on every heartbeat. ## Scope exclusion: no skill storage by the pass diff --git a/AGENTS.md b/AGENTS.md index 69cb6bc9b8..54cd520225 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,7 +134,8 @@ state/ runtime records and signals; gitignored .hash-* .count-* .stale-* .stale-since-* .paused-* .wedge-escalations-* .writing-* .seen-* .hb-surfaced-* .last-* .heartbeat-streak watcher internals; never touch .watch-triage.log watcher's absorbed-wake debug log (size-capped); never relied on, safe to delete .last-watcher-beat watcher liveness beacon, touched every poll (including while absorbing benign wakes); guard scripts read it - .last-stow bare-mtime marker touched only by the stow skill, only at the end of a reset-safe pass; read by fm-session-start.sh's compact/clear re-emit and by section 8 rule 4's heartbeat check to gate automatic /stow + .last-stow bare-mtime marker touched only by the stow skill, only at the end of a reset-safe pass; the durable record of the last clean /stow + .last-stow-attempt bare-mtime marker touched by the stow skill at the end of every /stow pass, reset-safe or not; read by fm-session-start.sh's compact/clear re-emit and by section 8 rule 4's heartbeat check to gate automatic /stow .subsuper-* .supervise-daemon.* sub-supervisor internals; never touch .no-mistakes/ local validation state and evidence; gitignored ``` @@ -410,7 +411,7 @@ Handle actionable wakes as follows: 2. For `stale:`, inspect the recorded endpoint and load `stuck-crewmate-recovery` for a stopped, looping, confused, or unresponsive worker; a deep-inspection reason also requires current-state and validation-log inspection. 3. For `check:`, act on the named poll result, including merges, Relay events, process-to-event source results, and captain inbox notes; a handled inbox note is also acknowledged with `bin/fm-inbox.sh drain --ack `, or it stays counted as still waiting for firstmate. 4. For `heartbeat:`, review the whole fleet from the structured fleet view, reconcile suspicious tasks and PR state, update the backlog, and never report an unchanged fleet as progress. - Also check `state/.last-stow`'s age against `FM_AUTO_STOW_INTERVAL_SECS` (default ~24h, a separate and larger clock than the heartbeat's own cadence); when due, run `/stow` first, before the rest of this review, so an automatic pass does not run on every heartbeat. + Also check `state/.last-stow-attempt`'s age against `FM_AUTO_STOW_INTERVAL_SECS` (default ~24h, a separate and larger clock than the heartbeat's own cadence); when due, run `/stow` first, before the rest of this review, so an automatic pass does not run on every heartbeat. That marker records an attempted pass rather than a reset-safe one, so a home holding an exception `/stow` cannot clear still waits out the full interval before the next automatic pass. When any wake reports a merged PR for a project cloned in this home, refresh that clone through the guarded fleet-sync path. When Relay-linked work reaches a milestone or terminal state, load `fmx-respond`; before terminal teardown, use its promised-final reconciliation when a typed public commitment exists, otherwise post the final completion follow-up so the link clears even if earlier follow-ups were spent. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 6202073738..24f30faea0 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -335,8 +335,6 @@ PRIMARY_HARNESS=$("$SCRIPT_DIR/fm-harness.sh" 2>/dev/null || printf unknown) . "$SCRIPT_DIR/fm-wake-lib.sh" # shellcheck source=bin/fm-line-cap-lib.sh . "$SCRIPT_DIR/fm-line-cap-lib.sh" -# shellcheck source=bin/fm-supervision-lib.sh -. "$SCRIPT_DIR/fm-supervision-lib.sh" # One tasks-axi compatibility verdict per session start. The probe costs three # tasks-axi subprocesses and this digest needs the same answer twice - here for @@ -354,37 +352,33 @@ case "$QUEUED_LIMIT" in ''|*[!0-9]*|0) QUEUED_LIMIT=20 ;; esac BACKLOG_FIELDS=blocked_by,hold_kind,hold_reason # Automatic /stow, trigger 1 (the compact/clear re-emit path below): a -# staleness gate on state/.last-stow, touched only by the stow skill itself at -# the end of a reset-safe pass (mirrors state/.last-heartbeat's bare-mtime -# marker, bin/fm-watch.sh). Read only here, never written by this script. -# Trigger 2 is the heartbeat-handling check in AGENTS.md section 8 rule 4, -# which reads the same marker against the same interval. +# staleness gate on state/.last-stow-attempt, touched by the stow skill itself +# at the end of every pass whether or not it reached reset-safe (mirrors +# state/.last-heartbeat's bare-mtime marker, bin/fm-watch.sh). Reading the +# attempt marker rather than its reset-safe-only sibling state/.last-stow is +# what holds the once-per-interval throttle in a home whose exceptions /stow +# cannot clear. Read only here, never written by this script. Trigger 2 is the +# heartbeat-handling check in AGENTS.md section 8 rule 4, which reads the same +# marker against the same interval. STOW_INTERVAL=${FM_AUTO_STOW_INTERVAL_SECS:-86400} case "$STOW_INTERVAL" in ''|*[!0-9]*|0) STOW_INTERVAL=86400 ;; esac -# stow_due_line: one "STOW DUE: ..." line when state/.last-stow is missing or -# at least STOW_INTERVAL seconds old, silent (prints nothing, exit 0) when -# current. Detect-only and cheap - a single mtime stat - matching the "always -# check, only speak up when it matters" idiom the bootstrap stage already uses. +# stow_due_line: one "STOW DUE: ..." line when state/.last-stow-attempt is +# missing, unreadable, or at least STOW_INTERVAL seconds old, silent (prints +# nothing, exit 0) when current. Detect-only and cheap - a single mtime stat - +# matching the "always check, only speak up when it matters" idiom the +# bootstrap stage already uses. stow_due_line() { - local marker="$STATE/.last-stow" m age - if [ -e "$marker" ]; then - m=$(fm_sup_stat_mtime "$marker" 2>/dev/null) - if [ -n "$m" ]; then - age=$(( $(date +%s) - m )) - else - age=999999 - fi - else - age=999999 - fi - [ "$age" -ge "$STOW_INTERVAL" ] || return 0 - if [ -e "$marker" ]; then - printf 'STOW DUE: last /stow pass was %ss ago (over the %ss interval, source=%s); run /stow before other work.\n' \ - "$age" "$STOW_INTERVAL" "${SESSION_SOURCE:-unknown}" - else + local marker="$STATE/.last-stow-attempt" m age + m=$(fm_path_mtime "$marker") + if [ -z "$m" ]; then printf 'STOW DUE: no recorded /stow pass (source=%s); run /stow before other work.\n' "${SESSION_SOURCE:-unknown}" + return 0 fi + age=$(( $(date +%s) - m )) + [ "$age" -ge "$STOW_INTERVAL" ] || return 0 + printf 'STOW DUE: last /stow pass was %ss ago (over the %ss interval, source=%s); run /stow before other work.\n' \ + "$age" "$STOW_INTERVAL" "${SESSION_SOURCE:-unknown}" } RULE='================================================================================' @@ -642,7 +636,6 @@ if [ "$REEMIT" -eq 0 ] && [ "$SESSION_SOURCE" = startup ]; then fi if [ "$REEMIT" -eq 1 ]; then - stow_due_line section "SESSION START (CONTEXT RE-EMIT) - $FM_HOME" printf 'This session already took the helm at its own startup and has only lost its\n' printf 'context. Lock ownership is re-verified and the durable records below are\n' @@ -676,6 +669,13 @@ if [ "$LOCK_RC" -ne 0 ]; then printf '%s\n' "$BAR" } fi +# Automatic /stow, trigger 1. Held until the lock verdict above: /stow mutates +# this home's memory files, so a re-emit that could not verify fleet-lock +# ownership must stay silent about it and leave the still-due marker to the +# next session start or re-emit that does own the lock. +if [ "$REEMIT" -eq 1 ] && [ "$READ_ONLY" -eq 0 ]; then + stow_due_line +fi REBUILDING_SESSION_PID=$(fm_harness_ancestry_pid 2>/dev/null || true) print_agents_refresh_if_required "$REBUILDING_SESSION_PID" diff --git a/docs/configuration.md b/docs/configuration.md index be49c5e9d3..c8f5c1b63c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -649,7 +649,7 @@ FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops CMUX_SOCKET_PASSWORD= # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md) FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest; each line is capped by bin/fm-line-cap-lib.sh FM_SESSION_START_QUEUED_LIMIT=20 # plain queued backlog rows in the session-start digest; in-flight, held, and blocked rows are never bounded and done rows are never listed -FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow: gates the STOW DUE line on a compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow's mtime, touched only by the stow skill at the end of a reset-safe pass +FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow: gates the STOW DUE line on a lock-owning compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow-attempt's mtime, touched by the stow skill at the end of every pass whether or not it reached reset-safe (state/.last-stow, its reset-safe-only sibling, is not what these triggers read) FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording FM_BOOTSTRAP_NETWORK=all # internal session-start phase split: all, skip (local steps only), or only (network steps only); see bin/fm-bootstrap.sh FM_STARTUP_NETWORK_TIMEOUT=120 # seconds bounding the whole deferred network stage; hitting it prints an actionable NETWORK_CHECKS line diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index ea6e2ee457..7dc025ebf5 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2017,9 +2017,24 @@ EOF } # --- automatic /stow trigger 1: STOW DUE on compact/clear re-emit ------------ -# A staleness gate on state/.last-stow that prepends one STOW DUE line to a -# compact/clear re-emit, silent when the marker is current. These exercise the -# real digest's public output only - never source bytes. +# A staleness gate on state/.last-stow-attempt that surfaces one STOW DUE line +# in a lock-owning compact/clear re-emit, silent when the marker is current and +# silent when the re-emit could not verify fleet-lock ownership. These exercise +# the real digest's public output only - never source bytes. + +# Set 's mtime to exactly seconds (touch -t takes a local-time +# stamp, not an epoch, on both platforms, so convert via BSD `date -r` or GNU +# `date -d @`). +set_stow_marker_mtime() { # + local epoch=$1 f=$2 stamp + touch "$f" + if stamp=$(date -r "$epoch" +%Y%m%d%H%M.%S 2>/dev/null); then + touch -t "$stamp" "$f" + else + stamp=$(date -d "@$epoch" +%Y%m%d%H%M.%S) + touch -t "$stamp" "$f" + fi +} run_reemit_for_stow() { # [source] local home=$1 root=$2 path=$3 source=${4:-compact} @@ -2028,8 +2043,13 @@ run_reemit_for_stow() { # [source] "$SESSION_START" --reemit --source "$source" } -test_stow_due_prepended_when_marker_absent_on_reemit() { - local rec root home fakebin out first_line +# Line number of the first line matching , or empty when absent. +stow_line_no() { # + printf '%s\n' "$1" | grep -n -F -- "$2" | head -1 | cut -d: -f1 +} + +test_stow_due_surfaced_when_marker_absent_on_reemit() { + local rec root home fakebin out due_at bootstrap_at rec=$(new_world stow-due-absent) IFS='|' read -r root home fakebin < "$home/state/.lock" + cat > "$fakebin/ps" <<'SH' +#!/usr/bin/env bash +set -u +case "$*" in + *"-p 999999"*) printf 'claude\n'; exit 0 ;; + *"comm="*|*"args="*) printf 'bash\n'; exit 0 ;; +esac +exit 0 +SH + chmod +x "$fakebin/ps" + + out=$(run_reemit_for_stow "$home" "$root" "$fakebin:$BASE_PATH") + + assert_contains "$out" "READ-ONLY SESSION" \ + "the read-only re-emit fixture did not actually refuse the lock" + assert_not_contains "$out" "STOW DUE:" \ + "a re-emit without verified fleet-lock ownership was told to run the mutating /stow pass" + + pass "a re-emit that lacks verified fleet-lock ownership stays silent about /stow" +} + test_stow_due_never_appears_on_ordinary_startup() { local rec root home fakebin out rec=$(new_world stow-due-startup) @@ -2134,7 +2235,7 @@ $rec EOF make_fake_toolchain "$fakebin" make_fake_ps_claude "$fakebin" - # No state/.last-stow marker at all - trigger 1 is scoped to the + # No state/.last-stow-attempt marker at all - trigger 1 is scoped to the # compact/clear re-emit path only, never the ordinary full-digest startup. out=$(run_session_start "$home" "$root" "$fakebin:$BASE_PATH") @@ -2629,11 +2730,14 @@ test_portable_timeout_escalates_term_resistant_process test_runtime_bound_leaves_a_healthy_digest_untouched test_runtime_bound_leaves_harness_ancestry_headroom test_reemit_skips_startup_sweeps_but_keeps_the_wake_drain -test_stow_due_prepended_when_marker_absent_on_reemit +test_stow_due_surfaced_when_marker_absent_on_reemit test_stow_due_silent_when_marker_is_fresh test_stow_due_default_interval_keeps_a_recent_marker_silent test_stow_due_when_marker_older_than_interval +test_stow_due_throttles_on_the_attempt_marker_not_the_reset_safe_one test_stow_due_respects_custom_interval_env_var +test_stow_due_missing_marker_is_due_under_any_interval +test_stow_due_silent_on_a_read_only_reemit test_stow_due_never_appears_on_ordinary_startup test_agents_baseline_stays_at_true_start_and_reemits_on_every_drifted_pi_compact test_read_only_pi_compact_refreshes_against_its_own_session_identity From 8c6c3d2ffda22339f78e83f0361cf7077943ea73 Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 17:32:57 +0700 Subject: [PATCH 3/9] fix: document Pi heartbeat auto-stow gap and harden stow-due tests Promote the last-stow marker contract out of the cascade heading so a secondmate home still throttles automatic /stow. Pin the stale re-emit age assertion to a 9xxxx band instead of the prefix 900. Document that default Pi branch supervision does not run heartbeat /stow, and leave that wiring as follow-up (kunchenguid/firstmate#2944) rather than editing fm-branch-prompt.sh. --- .agents/skills/stow/SKILL.md | 5 +++++ docs/architecture.md | 1 + docs/configuration.md | 14 +++++++++++++- docs/pi-supervision-branch.md | 5 +++++ tests/fm-session-start.test.sh | 8 ++++++-- 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.agents/skills/stow/SKILL.md b/.agents/skills/stow/SKILL.md index d025df616f..abce3830b4 100644 --- a/.agents/skills/stow/SKILL.md +++ b/.agents/skills/stow/SKILL.md @@ -300,6 +300,11 @@ Extend the completion receipt with one entry per secondmate alongside the primar Keep those entries in the same plain captain-facing language the rest of the receipt uses. The session is reset-safe only when every home is within its own budget with no unresolved exception. +## Automatic /stow markers + +Every `/stow` invocation in every home - primary or secondmate - updates the staleness markers below after that home's own pass (and, in a primary home, after the cascade above). +A secondmate home still performs this step even though it never cascades further. + When, and only when, the whole pass for this home - including the cascade above in a primary home - is reset-safe, touch `state/.last-stow` (`touch state/.last-stow`); never touch it when reset-safe cannot be claimed. Then touch `state/.last-stow-attempt` (`touch state/.last-stow-attempt`) as the pass's true final step, unconditionally, on every `/stow` invocation - reset-safe or not, and whatever exceptions stayed unresolved. Both are bare-mtime markers mirroring `state/.last-heartbeat` (`bin/fm-watch.sh`): `state/.last-stow` records the last fully reset-safe pass, while `state/.last-stow-attempt` records that a pass ran at all and is the marker the automatic `/stow` triggers in `AGENTS.md` read to decide whether another pass is due. diff --git a/docs/architecture.md b/docs/architecture.md index a25bb20430..4d37f8e722 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -330,6 +330,7 @@ It is deliberately not a reconciliation of durable records against repository or Task-scoped notes use `tasks-axi show --full` followed by `tasks-axi update --body-file `, adding `--archive-body` when the prior body should remain recoverable. The stow pass never writes a skill, but a separately executed, captain-approved migration may move conditional knowledge into a user-owned local skill excluded from the Firstmate clone; changes to Firstmate's tracked skills remain deliberate repository work through the normal PR pipeline. Invoked in a primary home, `/stow` then cascades the same sweep to every registered secondmate, enumerated through `bin/fm-stow-cascade.sh`: each home is accounted and curated against its own startup-memory allowance, a live secondmate sweeps its own session, and a slow or unreachable home is reported as an exception rather than blocking the primary. +Automatic triggers for the same skill - a lock-owning compact/clear re-emit and a staleness-gated heartbeat check - are owned by [configuration.md](configuration.md#automatic-stow). ## Local clones stay fresh diff --git a/docs/configuration.md b/docs/configuration.md index c8f5c1b63c..0eece261cc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -187,6 +187,18 @@ The flag is per home and is not inherited by secondmate homes, because stow cade Only the file's presence is read, so its contents are ignored; remove it to return to the default contract on the next pass. The skill text owns the marker spelling, the tick order, and the reinforcement rule. +## Automatic /stow + +Two existing turns already reach the agent, and both now decide whether to run the internal [`/stow` skill](../.agents/skills/stow/SKILL.md) instead of waiting for the captain to type it. + +1. A lock-owning compact/clear session-start re-emit prepends one `STOW DUE:` line when `state/.last-stow-attempt` is missing or older than `FM_AUTO_STOW_INTERVAL_SECS` (default 86400), and stays silent when the marker is current or the session could not verify fleet-lock ownership (`bin/fm-session-start.sh`). +2. Heartbeat handling in `AGENTS.md` section 8 rule 4 runs `/stow` first when the same marker is due, using that same larger interval so a pass does not run on every heartbeat wake. + +The stow skill touches `state/.last-stow-attempt` at the end of every pass, reset-safe or not, and touches `state/.last-stow` only when the pass is reset-safe. +The automatic triggers read the attempt marker, so a home holding an exception `/stow` cannot clear still waits out the full interval. +Away-mode heartbeats stay bash-only and never run `/stow`. +On a default Pi primary, heartbeat wakes go to the supervision branch, which does not currently run the `AGENTS.md` check; compact/clear re-emit remains the automatic path there ([Pi supervision branch](pi-supervision-branch.md#heartbeat-routing)). + ## Secondmate routes (data/secondmates.md) Persistent secondmate routes live locally in `data/secondmates.md`. @@ -649,7 +661,7 @@ FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops CMUX_SOCKET_PASSWORD= # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md) FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest; each line is capped by bin/fm-line-cap-lib.sh FM_SESSION_START_QUEUED_LIMIT=20 # plain queued backlog rows in the session-start digest; in-flight, held, and blocked rows are never bounded and done rows are never listed -FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow: gates the STOW DUE line on a lock-owning compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow-attempt's mtime, touched by the stow skill at the end of every pass whether or not it reached reset-safe (state/.last-stow, its reset-safe-only sibling, is not what these triggers read) +FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow (see "Automatic /stow" above): gates the STOW DUE line on a lock-owning compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow-attempt's mtime FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording FM_BOOTSTRAP_NETWORK=all # internal session-start phase split: all, skip (local steps only), or only (network steps only); see bin/fm-bootstrap.sh FM_STARTUP_NETWORK_TIMEOUT=120 # seconds bounding the whole deferred network stage; hitting it prints an actionable NETWORK_CHECKS line diff --git a/docs/pi-supervision-branch.md b/docs/pi-supervision-branch.md index 599da22bd0..4354c680ef 100644 --- a/docs/pi-supervision-branch.md +++ b/docs/pi-supervision-branch.md @@ -58,6 +58,11 @@ A review that found literally nothing worth reporting uses verdict `routine`, `t Only a captain-worthy finding reports verdict `captain` and opens a main turn. Every other fleet-wide or unresolvable wake - including watcher-failure alarms, which are never offered to the branch - keeps today's wake-to-main path. +The branch's heartbeat review does not currently run automatic `/stow`. +`AGENTS.md` section 8 rule 4's staleness-gated `/stow` instruction lives on main, and default-on branch supervision routes heartbeat wakes away from main. +A lock-owning compact/clear session-start re-emit remains the automatic `/stow` path on a default Pi primary (`bin/fm-session-start.sh`; `FM_AUTO_STOW_INTERVAL_SECS` in [configuration.md](configuration.md)). +Wiring heartbeat `/stow` into the branch is follow-up work: `bin/fm-branch-prompt.sh`'s byte-stable-prefix contract forbids per-wake state, so that change has to preserve cache identity rather than appending a live marker age. + ## Cost model and the byte-stable prefix The captain accepted the normal provider prompt-caching strategy: a byte-identical branch prefix generated once per firstmate version, the same tool set in the same order on every request, and one shared `prompt_cache_key` per home for all branch sessions (set in a `before_provider_request` hook, and only for providers whose requests already carry that field); main keeps its own per-session key. diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 7dc025ebf5..764d1ed648 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2120,8 +2120,12 @@ EOF out=$(run_reemit_for_stow "$home" "$root" "$fakebin:$BASE_PATH") - assert_contains "$out" "STOW DUE: last /stow pass was 900" \ - "a state/.last-stow-attempt marker older than the interval did not surface a STOW DUE line naming its measured age" + # Age is measured at digest time, so a 90000s-old marker can print 90000 + # through ~90120 on a loaded host (SESSION_START_BUDGET defaults to 120s). + # Pin the five-digit 9xxxx band rather than the prefix "900", which flips + # once the age leaves 90000-90099. + printf '%s\n' "$out" | grep -Eq 'STOW DUE: last /stow pass was 9[0-9]{4}s ago' || \ + fail "a state/.last-stow-attempt marker older than the interval did not surface a STOW DUE line naming a measured five-digit age in the 9xxxx range"$'\n'"--- output ---"$'\n'"$out" assert_contains "$out" "ago (over the 86400s interval" \ "the STOW DUE line did not disclose the interval it compared against" From 576888dffda11952ae7eb413c2e44fdfb83c352a Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 17:39:01 +0700 Subject: [PATCH 4/9] no-mistakes(review): Cap automatic stow interval safely --- bin/fm-session-start.sh | 9 ++++++++- docs/configuration.md | 3 ++- tests/fm-session-start.test.sh | 21 +++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 24f30faea0..afe1ef61f1 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -361,7 +361,14 @@ BACKLOG_FIELDS=blocked_by,hold_kind,hold_reason # heartbeat-handling check in AGENTS.md section 8 rule 4, which reads the same # marker against the same interval. STOW_INTERVAL=${FM_AUTO_STOW_INTERVAL_SECS:-86400} -case "$STOW_INTERVAL" in ''|*[!0-9]*|0) STOW_INTERVAL=86400 ;; esac +if [[ "$STOW_INTERVAL" =~ ^0*([1-9][0-9]{0,7})$ ]]; then + STOW_INTERVAL=${BASH_REMATCH[1]} +else + STOW_INTERVAL=86400 +fi +if [ "${#STOW_INTERVAL}" -eq 8 ] && [[ "$STOW_INTERVAL" > 31536000 ]]; then + STOW_INTERVAL=86400 +fi # stow_due_line: one "STOW DUE: ..." line when state/.last-stow-attempt is # missing, unreadable, or at least STOW_INTERVAL seconds old, silent (prints diff --git a/docs/configuration.md b/docs/configuration.md index 0eece261cc..493eeb576a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -194,6 +194,7 @@ Two existing turns already reach the agent, and both now decide whether to run t 1. A lock-owning compact/clear session-start re-emit prepends one `STOW DUE:` line when `state/.last-stow-attempt` is missing or older than `FM_AUTO_STOW_INTERVAL_SECS` (default 86400), and stays silent when the marker is current or the session could not verify fleet-lock ownership (`bin/fm-session-start.sh`). 2. Heartbeat handling in `AGENTS.md` section 8 rule 4 runs `/stow` first when the same marker is due, using that same larger interval so a pass does not run on every heartbeat wake. +`FM_AUTO_STOW_INTERVAL_SECS` accepts whole seconds from 1 through 31536000 (365 days); empty, non-numeric, zero, or larger values fall back to 86400. The stow skill touches `state/.last-stow-attempt` at the end of every pass, reset-safe or not, and touches `state/.last-stow` only when the pass is reset-safe. The automatic triggers read the attempt marker, so a home holding an exception `/stow` cannot clear still waits out the full interval. Away-mode heartbeats stay bash-only and never run `/stow`. @@ -661,7 +662,7 @@ FM_ZELLIJ_SESSION=firstmate # zellij-only: named session for normal backend ops CMUX_SOCKET_PASSWORD= # cmux-only: socket password fallback when config/cmux-socket-password is absent (docs/cmux-backend.md) FM_SESSION_START_STATUS_TAIL=5 # state/*.status lines printed per task in the session-start digest; each line is capped by bin/fm-line-cap-lib.sh FM_SESSION_START_QUEUED_LIMIT=20 # plain queued backlog rows in the session-start digest; in-flight, held, and blocked rows are never bounded and done rows are never listed -FM_AUTO_STOW_INTERVAL_SECS=86400 # staleness interval for automatic /stow (see "Automatic /stow" above): gates the STOW DUE line on a lock-owning compact/clear session-start re-emit and the heartbeat-handling stow check in AGENTS.md section 8; measured against state/.last-stow-attempt's mtime +FM_AUTO_STOW_INTERVAL_SECS=86400 # 1..31536000 seconds; invalid or larger values fall back to 86400; gates automatic /stow against state/.last-stow-attempt's mtime (see "Automatic /stow" above) FM_BOOTSTRAP_DETECT_ONLY=0 # internal/read-only session-start mode: skip bootstrap's mutating sweeps and print advisory TANGLE wording FM_BOOTSTRAP_NETWORK=all # internal session-start phase split: all, skip (local steps only), or only (network steps only); see bin/fm-bootstrap.sh FM_STARTUP_NETWORK_TIMEOUT=120 # seconds bounding the whole deferred network stage; hitting it prints an actionable NETWORK_CHECKS line diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 764d1ed648..1e0d8c68d2 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2197,6 +2197,26 @@ EOF pass "a missing marker is due under any configured interval" } +test_stow_due_huge_interval_falls_back_to_default() { + local rec root home fakebin out + rec=$(new_world stow-due-huge-interval) + IFS='|' read -r root home fakebin < Date: Mon, 24 Aug 2026 17:46:14 +0700 Subject: [PATCH 5/9] no-mistakes(document): Document automatic stow triggers and Pi follow-up --- AGENTS.md | 2 +- docs/pi-supervision-branch.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 54cd520225..bd2b5577db 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -259,7 +259,7 @@ Route durable knowledge to its most specific owner: Firstmate never writes a project's `AGENTS.md` directly. A crewmate creates or updates it lazily through the project's selected delivery path, using `bin/fm-ensure-agents-md.sh` and preferring pointers to authoritative sources over copied detail. Keep fleet delivery posture and captain-private strategy out of project memory. -When the captain invokes `/stow`, load the `stow` skill for its memory curation, knowledge routing, and persistence of the open work records this session is holding; it files and corrects only the open work that session is holding, and never reconciles the backlog against repository or PR reality. +When the captain invokes `/stow` or an automatic trigger says it is due, load the `stow` skill for its memory curation, knowledge routing, and persistence of the open work records this session is holding; it files and corrects only the open work that session is holding, and never reconciles the backlog against repository or PR reality. ## 7. Task lifecycle diff --git a/docs/pi-supervision-branch.md b/docs/pi-supervision-branch.md index 4354c680ef..d01a72836a 100644 --- a/docs/pi-supervision-branch.md +++ b/docs/pi-supervision-branch.md @@ -61,7 +61,7 @@ Every other fleet-wide or unresolvable wake - including watcher-failure alarms, The branch's heartbeat review does not currently run automatic `/stow`. `AGENTS.md` section 8 rule 4's staleness-gated `/stow` instruction lives on main, and default-on branch supervision routes heartbeat wakes away from main. A lock-owning compact/clear session-start re-emit remains the automatic `/stow` path on a default Pi primary (`bin/fm-session-start.sh`; `FM_AUTO_STOW_INTERVAL_SECS` in [configuration.md](configuration.md)). -Wiring heartbeat `/stow` into the branch is follow-up work: `bin/fm-branch-prompt.sh`'s byte-stable-prefix contract forbids per-wake state, so that change has to preserve cache identity rather than appending a live marker age. +Wiring heartbeat `/stow` into the branch is tracked in [issue #2944](https://github.com/kunchenguid/firstmate/issues/2944): `bin/fm-branch-prompt.sh`'s byte-stable-prefix contract forbids per-wake state, so that change has to preserve cache identity rather than appending a live marker age. ## Cost model and the byte-stable prefix From 49fb7f302282f0014551f176cbe12029dfd66f36 Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 17:47:11 +0700 Subject: [PATCH 6/9] no-mistakes(lint): Fix numeric stow interval lint comparison --- bin/fm-session-start.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index afe1ef61f1..0f021256a6 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -366,7 +366,7 @@ if [[ "$STOW_INTERVAL" =~ ^0*([1-9][0-9]{0,7})$ ]]; then else STOW_INTERVAL=86400 fi -if [ "${#STOW_INTERVAL}" -eq 8 ] && [[ "$STOW_INTERVAL" > 31536000 ]]; then +if [ "${#STOW_INTERVAL}" -eq 8 ] && [ "$STOW_INTERVAL" -gt 31536000 ]; then STOW_INTERVAL=86400 fi From 39bbea4834c5021de5c760cb2ebc3965ec50ed4c Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 17:55:04 +0700 Subject: [PATCH 7/9] no-mistakes: apply CI fixes --- bin/fm-session-start.sh | 5 +++++ tests/fm-session-start.test.sh | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index 0f021256a6..b6700740a2 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -383,6 +383,11 @@ stow_due_line() { return 0 fi age=$(( $(date +%s) - m )) + if [ "$age" -lt 0 ]; then + printf 'STOW DUE: last /stow attempt marker is future-dated (source=%s); run /stow before other work.\n' \ + "${SESSION_SOURCE:-unknown}" + return 0 + fi [ "$age" -ge "$STOW_INTERVAL" ] || return 0 printf 'STOW DUE: last /stow pass was %ss ago (over the %ss interval, source=%s); run /stow before other work.\n' \ "$age" "$STOW_INTERVAL" "${SESSION_SOURCE:-unknown}" diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 1e0d8c68d2..0ced3f1421 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2132,6 +2132,24 @@ EOF pass "a state/.last-stow-attempt marker older than the default interval surfaces a STOW DUE line on re-emit" } +test_stow_due_when_marker_is_future_dated() { + local rec root home fakebin out + rec=$(new_world stow-due-future) + IFS='|' read -r root home fakebin < Date: Mon, 24 Aug 2026 18:00:53 +0700 Subject: [PATCH 8/9] no-mistakes: apply CI fixes --- bin/fm-session-start.sh | 6 +----- tests/fm-session-start.test.sh | 4 +++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index b6700740a2..bcb62ce2dd 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -383,11 +383,7 @@ stow_due_line() { return 0 fi age=$(( $(date +%s) - m )) - if [ "$age" -lt 0 ]; then - printf 'STOW DUE: last /stow attempt marker is future-dated (source=%s); run /stow before other work.\n' \ - "${SESSION_SOURCE:-unknown}" - return 0 - fi + [ "$age" -ge 0 ] || age=$STOW_INTERVAL [ "$age" -ge "$STOW_INTERVAL" ] || return 0 printf 'STOW DUE: last /stow pass was %ss ago (over the %ss interval, source=%s); run /stow before other work.\n' \ "$age" "$STOW_INTERVAL" "${SESSION_SOURCE:-unknown}" diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index 0ced3f1421..f2626b9926 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2144,8 +2144,10 @@ EOF out=$(run_reemit_for_stow "$home" "$root" "$fakebin:$BASE_PATH") - assert_contains "$out" "STOW DUE: last /stow attempt marker is future-dated" \ + assert_contains "$out" "STOW DUE: last /stow pass was ${FM_AUTO_STOW_INTERVAL_SECS:-86400}s ago" \ "a future-dated state/.last-stow-attempt marker suppressed automatic /stow" + assert_contains "$out" "over the ${FM_AUTO_STOW_INTERVAL_SECS:-86400}s interval" \ + "a future-dated marker did not use the configured staleness threshold" pass "a future-dated attempt marker fails open and surfaces a STOW DUE line" } From 184018d971bc288b1e7d9ce81ced1f711cd9e702 Mon Sep 17 00:00:00 2001 From: bingb0t5 Date: Mon, 24 Aug 2026 18:07:54 +0700 Subject: [PATCH 9/9] no-mistakes: apply CI fixes --- AGENTS.md | 2 +- bin/fm-session-start.sh | 7 ++++--- docs/configuration.md | 7 ++++--- docs/pi-supervision-branch.md | 2 +- tests/fm-session-start.test.sh | 30 ++++++++++++++++++++++++++---- 5 files changed, 36 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bd2b5577db..5fbb187616 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -411,7 +411,7 @@ Handle actionable wakes as follows: 2. For `stale:`, inspect the recorded endpoint and load `stuck-crewmate-recovery` for a stopped, looping, confused, or unresponsive worker; a deep-inspection reason also requires current-state and validation-log inspection. 3. For `check:`, act on the named poll result, including merges, Relay events, process-to-event source results, and captain inbox notes; a handled inbox note is also acknowledged with `bin/fm-inbox.sh drain --ack `, or it stays counted as still waiting for firstmate. 4. For `heartbeat:`, review the whole fleet from the structured fleet view, reconcile suspicious tasks and PR state, update the backlog, and never report an unchanged fleet as progress. - Also check `state/.last-stow-attempt`'s age against `FM_AUTO_STOW_INTERVAL_SECS` (default ~24h, a separate and larger clock than the heartbeat's own cadence); when due, run `/stow` first, before the rest of this review, so an automatic pass does not run on every heartbeat. That marker records an attempted pass rather than a reset-safe one, so a home holding an exception `/stow` cannot clear still waits out the full interval before the next automatic pass. + When the captain has explicitly enabled automatic stow for this home with `config/auto-stow`, also check `state/.last-stow-attempt`'s age against `FM_AUTO_STOW_INTERVAL_SECS` (default ~24h, a separate and larger clock than the heartbeat's own cadence); when due, run `/stow` first, before the rest of this review, so an automatic pass does not run on every heartbeat. That marker records an attempted pass rather than a reset-safe one, so a home holding an exception `/stow` cannot clear still waits out the full interval before the next automatic pass. When any wake reports a merged PR for a project cloned in this home, refresh that clone through the guarded fleet-sync path. When Relay-linked work reaches a milestone or terminal state, load `fmx-respond`; before terminal teardown, use its promised-final reconciliation when a typed public commitment exists, otherwise post the final completion follow-up so the link clears even if earlier follow-ups were spent. diff --git a/bin/fm-session-start.sh b/bin/fm-session-start.sh index bcb62ce2dd..923eb87bfc 100755 --- a/bin/fm-session-start.sh +++ b/bin/fm-session-start.sh @@ -351,8 +351,9 @@ QUEUED_LIMIT=${FM_SESSION_START_QUEUED_LIMIT:-20} case "$QUEUED_LIMIT" in ''|*[!0-9]*|0) QUEUED_LIMIT=20 ;; esac BACKLOG_FIELDS=blocked_by,hold_kind,hold_reason -# Automatic /stow, trigger 1 (the compact/clear re-emit path below): a -# staleness gate on state/.last-stow-attempt, touched by the stow skill itself +# Automatic /stow, trigger 1 (the compact/clear re-emit path below): an +# explicit home-local config/auto-stow grant plus a staleness gate on +# state/.last-stow-attempt, touched by the stow skill itself # at the end of every pass whether or not it reached reset-safe (mirrors # state/.last-heartbeat's bare-mtime marker, bin/fm-watch.sh). Reading the # attempt marker rather than its reset-safe-only sibling state/.last-stow is @@ -681,7 +682,7 @@ fi # this home's memory files, so a re-emit that could not verify fleet-lock # ownership must stay silent about it and leave the still-due marker to the # next session start or re-emit that does own the lock. -if [ "$REEMIT" -eq 1 ] && [ "$READ_ONLY" -eq 0 ]; then +if [ "$REEMIT" -eq 1 ] && [ "$READ_ONLY" -eq 0 ] && [ -e "$CONFIG/auto-stow" ]; then stow_due_line fi REBUILDING_SESSION_PID=$(fm_harness_ancestry_pid 2>/dev/null || true) diff --git a/docs/configuration.md b/docs/configuration.md index 493eeb576a..d76487d587 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -189,10 +189,11 @@ The skill text owns the marker spelling, the tick order, and the reinforcement r ## Automatic /stow -Two existing turns already reach the agent, and both now decide whether to run the internal [`/stow` skill](../.agents/skills/stow/SKILL.md) instead of waiting for the captain to type it. +Create the home-local, gitignored `config/auto-stow` file to grant automatic memory curation for that home; its contents are ignored, and removing it disables both triggers. +This explicit captain opt-in satisfies `VISION.md`'s authority boundary while letting two existing turns decide whether to run the internal [`/stow` skill](../.agents/skills/stow/SKILL.md) instead of waiting for the captain to type it. -1. A lock-owning compact/clear session-start re-emit prepends one `STOW DUE:` line when `state/.last-stow-attempt` is missing or older than `FM_AUTO_STOW_INTERVAL_SECS` (default 86400), and stays silent when the marker is current or the session could not verify fleet-lock ownership (`bin/fm-session-start.sh`). -2. Heartbeat handling in `AGENTS.md` section 8 rule 4 runs `/stow` first when the same marker is due, using that same larger interval so a pass does not run on every heartbeat wake. +1. An enabled, lock-owning compact/clear session-start re-emit prepends one `STOW DUE:` line when `state/.last-stow-attempt` is missing or older than `FM_AUTO_STOW_INTERVAL_SECS` (default 86400), and stays silent when the marker is current, automatic stow is disabled, or the session could not verify fleet-lock ownership (`bin/fm-session-start.sh`). +2. Enabled heartbeat handling in `AGENTS.md` section 8 rule 4 runs `/stow` first when the same marker is due, using that same larger interval so a pass does not run on every heartbeat wake. `FM_AUTO_STOW_INTERVAL_SECS` accepts whole seconds from 1 through 31536000 (365 days); empty, non-numeric, zero, or larger values fall back to 86400. The stow skill touches `state/.last-stow-attempt` at the end of every pass, reset-safe or not, and touches `state/.last-stow` only when the pass is reset-safe. diff --git a/docs/pi-supervision-branch.md b/docs/pi-supervision-branch.md index d01a72836a..1aadab5559 100644 --- a/docs/pi-supervision-branch.md +++ b/docs/pi-supervision-branch.md @@ -60,7 +60,7 @@ Every other fleet-wide or unresolvable wake - including watcher-failure alarms, The branch's heartbeat review does not currently run automatic `/stow`. `AGENTS.md` section 8 rule 4's staleness-gated `/stow` instruction lives on main, and default-on branch supervision routes heartbeat wakes away from main. -A lock-owning compact/clear session-start re-emit remains the automatic `/stow` path on a default Pi primary (`bin/fm-session-start.sh`; `FM_AUTO_STOW_INTERVAL_SECS` in [configuration.md](configuration.md)). +When the captain has enabled `config/auto-stow`, a lock-owning compact/clear session-start re-emit remains the automatic `/stow` path on a default Pi primary (`bin/fm-session-start.sh`; `FM_AUTO_STOW_INTERVAL_SECS` in [configuration.md](configuration.md)). Wiring heartbeat `/stow` into the branch is tracked in [issue #2944](https://github.com/kunchenguid/firstmate/issues/2944): `bin/fm-branch-prompt.sh`'s byte-stable-prefix contract forbids per-wake state, so that change has to preserve cache identity rather than appending a live marker age. ## Cost model and the byte-stable prefix diff --git a/tests/fm-session-start.test.sh b/tests/fm-session-start.test.sh index f2626b9926..e3d83301f9 100755 --- a/tests/fm-session-start.test.sh +++ b/tests/fm-session-start.test.sh @@ -2017,10 +2017,11 @@ EOF } # --- automatic /stow trigger 1: STOW DUE on compact/clear re-emit ------------ -# A staleness gate on state/.last-stow-attempt that surfaces one STOW DUE line -# in a lock-owning compact/clear re-emit, silent when the marker is current and -# silent when the re-emit could not verify fleet-lock ownership. These exercise -# the real digest's public output only - never source bytes. +# An explicit config/auto-stow grant and staleness gate on +# state/.last-stow-attempt that surface one STOW DUE line in a lock-owning +# compact/clear re-emit, silent when disabled, when the marker is current, and +# when the re-emit could not verify fleet-lock ownership. These exercise the +# real digest's public output only - never source bytes. # Set 's mtime to exactly seconds (touch -t takes a local-time # stamp, not an epoch, on both platforms, so convert via BSD `date -r` or GNU @@ -2038,11 +2039,31 @@ set_stow_marker_mtime() { # run_reemit_for_stow() { # [source] local home=$1 root=$2 path=$3 source=${4:-compact} + touch "$home/config/auto-stow" FM_HOME="$home" FM_ROOT_OVERRIDE="$root" FM_FAKE_HARNESS_PID=$$ PATH="$path" \ env -u CLAUDECODE -u PI_CODING_AGENT -u FM_PI_HARNESS -u GROK_AGENT \ "$SESSION_START" --reemit --source "$source" } +test_stow_due_silent_without_captain_opt_in() { + local rec root home fakebin out + rec=$(new_world stow-due-disabled) + IFS='|' read -r root home fakebin <, or empty when absent. stow_line_no() { # printf '%s\n' "$1" | grep -n -F -- "$2" | head -1 | cut -d: -f1 @@ -2774,6 +2795,7 @@ test_portable_timeout_escalates_term_resistant_process test_runtime_bound_leaves_a_healthy_digest_untouched test_runtime_bound_leaves_harness_ancestry_headroom test_reemit_skips_startup_sweeps_but_keeps_the_wake_drain +test_stow_due_silent_without_captain_opt_in test_stow_due_surfaced_when_marker_absent_on_reemit test_stow_due_silent_when_marker_is_fresh test_stow_due_default_interval_keeps_a_recent_marker_silent