Skip to content
Open
Show file tree
Hide file tree
Changes from 43 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1097082
fix(teardown): never re-return a pool slot a newer task now occupies
Kallas95 Aug 14, 2026
7213b9b
no-mistakes(review): ignore returned predecessors when detecting pool…
Kallas95 Aug 14, 2026
9c2a519
no-mistakes(document): qualify teardown fail-closed claim with rerun …
Kallas95 Aug 14, 2026
be2b20d
watcher: apply long pause cadence to a live declared-pause agent
Kallas95 Aug 14, 2026
47e708b
herdr: adopt upstream PR 2373 locally to stop pane scroll-on-poll
Kallas95 Aug 15, 2026
0ed22f0
Merge branch 'main' into fm/fm-herdr-scroll-local
Kallas95 Aug 15, 2026
d8236d1
fix(bin): let a Claude Stop hook prove it belongs to the lock-owning …
Kallas95 Aug 15, 2026
7aef183
no-mistakes(review): dedupe Stop hook identity disjunction, document …
Kallas95 Aug 15, 2026
b29db81
watcher: adopt upstream PR 2419 locally, reconciled with local main
Kallas95 Aug 15, 2026
630eb13
no-mistakes(review): guard the detached Stop path, rename autoarm tra…
Kallas95 Aug 15, 2026
2a016d8
no-mistakes(review): record the measured live-guard evidence and vend…
Kallas95 Aug 15, 2026
885bfa5
no-mistakes(document): document the Stop identity proof's regression …
Kallas95 Aug 15, 2026
9cc0532
Merge branch 'main' into fm/fm-autoarm-local
Kallas95 Aug 15, 2026
7527e4f
fix(lock): let a session prove it owns its own fleet lock
Kallas95 Aug 15, 2026
4ccfcf7
no-mistakes(review): bound pool premise, share ancestry helper, wire …
Kallas95 Aug 15, 2026
d2f6c1f
no-mistakes(document): document session-identity binding in lock owne…
Kallas95 Aug 15, 2026
1ea93b2
Merge branch 'main' into fm/fm-lock-local
Kallas95 Aug 16, 2026
9c94e91
feat(bin): guard local landings against dropping local-only commits
Kallas95 Aug 16, 2026
d9d6d3a
no-mistakes(review): pin dropped commits under a durable rescue ref
Kallas95 Aug 16, 2026
ee59db7
fix(calm): sync the follow-up E2E on Pi's paint and name the skipped …
Kallas95 Aug 16, 2026
adce327
no-mistakes(review): name the unpainted pane and share the wait helper
Kallas95 Aug 16, 2026
38bb5cd
no-mistakes(review): key rescue refs per drop and set test exec bit
Kallas95 Aug 16, 2026
e179c9c
no-mistakes(document): align fm-merge-local header with code, dedupe …
Kallas95 Aug 16, 2026
a96c163
Merge remote-tracking branch 'origin/main' into fm/fm-adoption-lot-0816
Kallas95 Aug 16, 2026
b4c7755
Merge remote-tracking branch 'no-mistakes/fm/fm-worktree-base-locale'…
Kallas95 Aug 16, 2026
de1ede2
Merge remote-tracking branch 'no-mistakes/fm/fm-calm-bash5-regression…
Kallas95 Aug 16, 2026
f04fab1
Merge branch 'main' into fm/fm-main-rebase-amont
Kallas95 Aug 19, 2026
3224be9
Merge branch 'main' into fm/fm-reintegration-amont-0820
Kallas95 Aug 20, 2026
45d6fdc
Merge remote-tracking branch 'origin/main' into fm/fm-reintegration-a…
Kallas95 Aug 26, 2026
ec61bb1
Merge branch 'main' into fm/fm-autoarm-bloque-apres-afk
Kallas95 Aug 27, 2026
5f24aa4
fix(bin): restore Stop-owned supervision after an away-mode return
Kallas95 Aug 27, 2026
d731017
no-mistakes(document): name the lock's session binding as the hook's …
Kallas95 Aug 27, 2026
992c2e8
feat(bin): give Pi scouts web search without giving them the key
Kallas95 Sep 1, 2026
c858cad
docs(verification): record the measured payload reduction exactly
Kallas95 Sep 1, 2026
31ae94a
merge: reconcile local Firstmate with upstream
Kallas95 Sep 2, 2026
78b0a68
no-mistakes(review): Coordinate teardown reissue and preempt Claude s…
Kallas95 Sep 2, 2026
8614568
no-mistakes(review): Fix lifecycle lock ordering, stall clamps, and b…
Kallas95 Sep 2, 2026
92df5e5
no-mistakes(review): Serialize binding backfills and reset recovered …
Kallas95 Sep 2, 2026
d22a434
no-mistakes(review): Reset stall episodes and preserve concurrent loc…
Kallas95 Sep 2, 2026
cbc3f75
no-mistakes(review): Commit stall resets atomically and preserve conc…
Kallas95 Sep 2, 2026
6415a0f
no-mistakes(review): Preserve stall progression when alarm publicatio…
Kallas95 Sep 2, 2026
5489caf
no-mistakes(document): Reconcile safety and supervision documentation
Kallas95 Sep 2, 2026
4936159
no-mistakes(lint): Use explicit race fixture paths across subshells
Kallas95 Sep 2, 2026
ca389e5
no-mistakes(ci): Fixed Greptile’s valid finding: a failed compare-and…
Kallas95 Sep 2, 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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ state/ runtime records and signals; gitignored
<id>.status appended by crewmates: "<state>: <note>" wake-event lines, not current-state truth
<id>.turn-ended touched by turn-end hooks
<id>.grok-turnend-token firstmate-owned grok hook registry token for the task; removed by teardown
<id>.worktree-returned best-effort pool-return marker in the rerun tenant-safety protocol owned by bin/fm-teardown.sh's header; removed by teardown
<id>.kimi-turnend-token firstmate-owned Kimi hook registry token for the task; removed by teardown
<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
Expand Down
12 changes: 10 additions & 2 deletions bin/backends/herdr.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2594,14 +2594,22 @@ fm_backend_herdr_send_key() { # <target> <key>
# the composer-state guard/fallback reads around submit and injection). Workaround:
# always request a generous fetch far above any realistic viewport height, then
# trim to the caller's requested bound ourselves with `tail`.
#
# Fetches --format ansi and strips locally instead of asking for text: on herdr
# 0.8.0 a text-format `pane read --source recent` against an idle alt-screen
# agent triggers the wheel-event history harvest (src/server/alt_screen_read.rs)
# - the pane VISIBLY scrolls up for seconds and snaps back on every poll
# (herdrdev/herdr#2669, closed as dup of #2387). The ansi path skips the
# harvest entirely and returns herdr's retained rows instantly, which matches
# the pre-0.8.0 text behavior this capture was written against.
fm_backend_herdr_capture() { # <target> <lines>
fm_backend_herdr_target_ready "$1" || return 1
local lines=${2:-200} fetch out
case "$lines" in ''|*[!0-9]*) lines=200 ;; esac
fetch=$lines
case "$fetch" in ''|*[!0-9]*) fetch=200 ;; *) [ "$fetch" -ge 200 ] || fetch=200 ;; esac
out=$(fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source recent --lines "$fetch" 2>/dev/null) || return 1
printf '%s' "$out" | tail -n "$lines"
out=$(fm_backend_herdr_cli "$FM_BACKEND_HERDR_SESSION" pane read "$FM_BACKEND_HERDR_PANE" --source recent --lines "$fetch" --format ansi 2>/dev/null) || return 1
printf '%s' "$out" | tail -n "$lines" | fm_composer_strip_ansi
}

fm_backend_herdr_capture_ansi() { # <target> <lines>
Expand Down
93 changes: 77 additions & 16 deletions bin/fm-claude-stop-autoarm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@
# - Scope: only a genuine primary checkout (plain checkout or validly marked
# secondmate home) with AGENTS.md, bin/, and the effective state dir - the
# exact fm-turnend-guard.sh scope. Child crew/scout worktrees stay inert.
# - Identity: only when THIS session's harness ancestor holds state/.lock.
# When an existing numeric owner fails the shared harness-liveness predicate,
# the hook delegates guarded recovery to bin/fm-lock.sh and then re-verifies
# ownership. A live owner, missing lock, malformed lock, or unresolved
# ancestry remains inert, so a competing session never arms or rewakes.
# - Identity: only when THIS session holds state/.lock, proven by harness
# ancestry, by the delivering Claude session's own pid, or by the session the
# lock's durable binding records. The extra proofs are required because
# Claude Code serves hook commands from a shared worker pool whose top
# process is reparented to init, which can leave a hook with no ancestry path
# back to its live session at all, and because the lock records the
# OUTERMOST pid of the run that acquired it, which is not always the pid the
# harness exports for the same session; all three proofs and the disjunction
# over them are owned by bin/fm-session-lock-lib.sh, so the admission check
# and the post-recovery re-check cannot drift apart.
# When an existing numeric owner fails the shared harness-liveness
# predicate, the hook delegates guarded recovery to bin/fm-lock.sh and then
# re-verifies ownership through that same disjunction. A live owner, a
# missing or malformed lock, and a lock naming another session all remain
# inert, so a competing session never arms or rewakes.
# - AFK: while state/.afk exists the away daemon owns the watcher and triage;
# this hook exits 0 and NEVER rewakes the primary (checked again at
# translation time so a mid-cycle AFK transition is honored).
Expand Down Expand Up @@ -62,7 +72,13 @@
# suppresses any later automatic continuation in that unresolved episode.
#
# This hook never blocks the Stop decision itself and never prints to stdout:
# exit 0 is always silent, and exit 2 carries the rewake banner on stderr.
# exit 0 is always silent, and exit 2 carries the rewake banner on stderr. The
# opt-in FM_CLAUDE_AUTOARM_TRACE diagnostic is the one exception: when it is
# set to a non-empty value, every gate that ends the run inert BEFORE it claims
# the cycle names itself on stderr, which is what diagnoses a hook that never
# claims the home under real hook conditions. Exits after that claim stay silent
# and are read from the epoch ledger instead. It changes no decision and is
# never set in normal operation.
# On any uncertainty such as unresolvable ancestry, malformed lock state, or
# lock contention, it exits 0 and leaves continuity to the synchronous guard and
# the model.
Expand All @@ -83,6 +99,13 @@ case "$AUTOARM_ATTEMPTS" in
*) AUTOARM_ATTEMPTS=2 ;;
esac

# Opt-in gate diagnostic. Silent unless FM_CLAUDE_AUTOARM_TRACE is set to a
# non-empty value, so the production contract above is unchanged.
trace() { # <message>
[ -n "${FM_CLAUDE_AUTOARM_TRACE:-}" ] || return 0
printf 'autoarm-trace: %s\n' "$*" >&2
}

# shellcheck source=bin/fm-primary-scope-lib.sh
. "$SCRIPT_DIR/fm-primary-scope-lib.sh"
# shellcheck source=bin/fm-supervision-lib.sh
Expand All @@ -105,10 +128,16 @@ PAYLOAD=$(cat 2>/dev/null || true)
# the declared multi-hour timeout - the exact wedge grok 1.0.0 produced
# (docs/turnend-guard.md "Harness integrations"). Cursor's own park adapter owns
# its turn boundary, so stand down on a Cursor-delivered payload.
fm_hook_payload_is_foreign_host "$PAYLOAD" && exit 0
if fm_hook_payload_is_foreign_host "$PAYLOAD"; then
trace 'inert: payload delivered by a foreign host'
exit 0
fi

# --- scope: genuine primary checkout only -----------------------------------
fm_primary_scope_matches "$FM_ROOT" "$STATE" || exit 0
if ! fm_primary_scope_matches "$FM_ROOT" "$STATE"; then
trace "inert: $FM_ROOT is not a primary home with state dir $STATE"
exit 0
fi

# --- identity: only the lock-owning session's hooks may arm ------------------
# A prior session may have died after leaving its numeric harness pid in .lock.
Expand All @@ -117,31 +146,63 @@ fm_primary_scope_matches "$FM_ROOT" "$STATE" || exit 0
# idle or away home remains byte-for-byte inert. Missing or malformed locks are
# uncertainty rather than stale-owner evidence and remain inert.
RECOVER_SESSION_LOCK=0
if ! fm_session_lock_owned_by_self "$STATE"; then
if fm_session_lock_owned_by_this_claude_session "$STATE" "$PAYLOAD"; then
case "$FM_SESSION_LOCK_PROOF" in
ancestry) trace 'identity: proven by harness ancestry' ;;
claude-session)
trace "identity: proven by the delivering Claude session (pid ${CLAUDE_PID:-?})"
;;
claude-session-binding)
trace "identity: proven by the lock's recorded session (${CLAUDE_CODE_SESSION_ID:-?})"
;;
esac
else
LOCK_PID=$(cat "$STATE/.lock" 2>/dev/null || true)
case "$LOCK_PID" in
''|*[!0-9]*) exit 0 ;;
''|*[!0-9]*)
trace 'inert: no usable session lock, and neither identity proof applies'
exit 0
;;
esac
fm_harness_pid_alive "$LOCK_PID" && exit 0
if fm_harness_pid_alive "$LOCK_PID"; then
trace "inert: live session $LOCK_PID owns this home and neither identity proof applies (CLAUDE_PID=${CLAUDE_PID:-unset})"
exit 0
fi
trace "identity: recorded owner $LOCK_PID is dead, recovery pending"
RECOVER_SESSION_LOCK=1
fi

# --- AFK: the away daemon owns the watcher and triage; never rewake ----------
[ -e "$STATE/.afk" ] && exit 0
if [ -e "$STATE/.afk" ]; then
trace 'inert: away mode owns supervision'
exit 0
fi

# --- need: in-flight work or an X-mode relay poll ----------------------------
need_supervision() {
fm_supervision_needed "$STATE" "$GRACE"
}
need_supervision || exit 0
if ! need_supervision; then
trace 'inert: nothing in flight and no relay poll to run'
exit 0
fi

# --- stale session-lock recovery ---------------------------------------------
# Delegate the claim to fm-lock.sh so its live-owner refusal and write semantics
# remain the single acquisition owner, then re-verify current-session identity
# before touching any auto-arm state.
# before touching any auto-arm state. The re-check uses the same three-proof
# predicate as the admission gate above on purpose: if fm-lock.sh ever elects a
# different pid than it does today, this verification must not silently narrow
# to ancestry alone.
if [ "$RECOVER_SESSION_LOCK" -eq 1 ]; then
"$SCRIPT_DIR/fm-lock.sh" >/dev/null 2>&1 || exit 0
fm_session_lock_owned_by_self "$STATE" || exit 0
if ! "$SCRIPT_DIR/fm-lock.sh" >/dev/null 2>&1; then
trace 'inert: guarded session-lock recovery refused'
exit 0
fi
if ! fm_session_lock_owned_by_this_claude_session "$STATE" "$PAYLOAD"; then
trace 'inert: ownership unproven after session-lock recovery'
exit 0
fi
fi

# --- single-flight generation claim --------------------------------------------
Expand Down
38 changes: 27 additions & 11 deletions bin/fm-lock.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
# Writes the harness (agent) process PID found by walking the shell's ancestry,
# which lives as long as the firstmate session - unlike the transient subshell
# PID of any one tool call, which is dead moments after it is written.
# It also records, in the adjacent state/.lock.session binding, which harness
# session acquired that pid, because ancestry is not a stable session identity:
# a call served by a reparented worker pool never reaches its own session. That
# binding is what lets the acquiring session keep proving ownership; a lock that
# carries no binding behaves exactly as it did before it existed.
# Usage: fm-lock.sh acquire; exit 1 unless ownership is verified
# fm-lock.sh status print holder and liveness; always exits 0
set -u
Expand Down Expand Up @@ -55,17 +60,12 @@ release_claim_lock() {
trap release_claim_lock EXIT
trap 'exit 1' HUP INT TERM

if [ -f "$LOCK" ] && [ ! -L "$LOCK" ]; then
old=$(cat "$LOCK" 2>/dev/null || true)
if [ "$old" = "$me" ]; then
echo "lock acquired: harness pid $me"
exit 0
fi
if fm_harness_pid_alive "$old"; then
echo "error: another live firstmate session holds the lock (pid $old); operate read-only until resolved" >&2
exit 1
fi
fi
backfill_session_binding() {
local id
id=$(fm_harness_session_identity) || return 0
fm_session_lock_binding_names_session "$STATE" "$id" && return 0
fm_session_lock_publish_identity "$STATE" "$old" || true
}

if ! fm_lock_try_acquire "$CLAIM_LOCK"; then
sweep_pid=$(sed -n 's/^pid=//p' "$STATE/.startup-network.status" 2>/dev/null | tail -1)
Expand All @@ -86,6 +86,17 @@ if [ -e "$LOCK" ] || [ -L "$LOCK" ]; then
echo "error: session lock is unreadable; operate read-only until resolved" >&2
exit 1
}
if fm_session_lock_owned_by_self "$STATE"; then
backfill_session_binding
release_claim_lock
echo "lock acquired: harness pid $old"
exit 0
fi
if [ "$old" != "$me" ] && fm_session_lock_owned_by_session_identity "$STATE"; then
release_claim_lock
echo "lock acquired: harness pid $old"
exit 0
fi
if [ "$old" != "$me" ] && fm_harness_pid_alive "$old"; then
echo "error: another live firstmate session holds the lock (pid $old); operate read-only until resolved" >&2
exit 1
Expand All @@ -103,5 +114,10 @@ if [ ! -f "$LOCK" ] || [ -L "$LOCK" ] || [ "$written" != "$me" ]; then
echo "error: session lock ownership verification failed; operate read-only until resolved" >&2
exit 1
fi
# Bind the verified lock to this session's own identity, so this home stays
# provable from a worker pool that cannot reach the session through ancestry.
# A failure here is deliberately not fatal: the home simply keeps the
# ancestry-only behavior it had before this binding existed.
fm_session_lock_publish_identity "$STATE" "$me" || true
release_claim_lock
echo "lock acquired: harness pid $me"
Loading