Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
1031676
fix(dod): drive no-mistakes with one foreground call, not a backgroun…
tiago-peixoto Sep 12, 2026
16cd059
fix(bin): let a waiting worker spend no turns until it is answered
tiago-peixoto Sep 11, 2026
25e6f5d
no-mistakes(review): Exempt captain holds, quiet deferred reconcile, …
tiago-peixoto Sep 18, 2026
b6e58be
no-mistakes(document): Document deferred automatic nudges, rereads, a…
tiago-peixoto Sep 18, 2026
e4fb45a
no-mistakes(document): Ring unlanded fire-and-forget steers exactly o…
tiago-peixoto Sep 18, 2026
d2bffb4
no-mistakes(ci): The failing check, "PR must be raised via no-mistake…
tiago-peixoto Sep 18, 2026
86ba891
no-mistakes(review): Hold automatic wakes until a mate's own decision…
tiago-peixoto Sep 18, 2026
232c42b
no-mistakes(document): Document watcher delivery of deferred remote r…
tiago-peixoto Sep 18, 2026
4251b64
Merge remote-tracking branch 'origin/main' into fm/firstmate-issue422…
tiago-peixoto Sep 23, 2026
533521b
no-mistakes(review): Merge duplicate elapsed-wait reattach instructio…
tiago-peixoto Sep 23, 2026
578f680
no-mistakes(test): Resolve merged default decision in remote-reply re…
tiago-peixoto Sep 24, 2026
4434e60
no-mistakes(test): Source classify lib so config-push retry-deferred …
tiago-peixoto Sep 24, 2026
4ee79ce
Merge remote-tracking branch 'origin/main' into fm/firstmate-issue422…
tiago-peixoto Sep 27, 2026
c719928
no-mistakes(ci): Fixed a flaky test that also fails on main. Neither …
tiago-peixoto Sep 27, 2026
1fa3c7d
Revert "no-mistakes(ci): Fixed a flaky test that also fails on main. …
tiago-peixoto Sep 28, 2026
5e57c93
no-mistakes(review): Retry deferred local instruction nudges via the …
tiago-peixoto Sep 28, 2026
f05362a
no-mistakes(review): Document watcher retry for deferred local instru…
tiago-peixoto Sep 28, 2026
26bdb64
no-mistakes(ci): I fixed both review findings you selected (ci-1 and …
tiago-peixoto Sep 28, 2026
46feaaf
Pin autoarm supervision model in secondmate restart T3b
tiago-peixoto Sep 28, 2026
14cdc9c
Keep deferred secondmate nudges retryable under the inheritance lock.
tiago-peixoto Sep 28, 2026
4c8f1e8
no-mistakes(document): Document watcher retry of deferred restart re-…
tiago-peixoto Sep 28, 2026
2228cd3
Send secondmate reread and restart nudges immediately again.
tiago-peixoto Sep 28, 2026
4cf178a
Make the no-turn wait opt-in behind config/wait-no-turns.
tiago-peixoto Sep 29, 2026
964913d
no-mistakes(document): Document wait-no-turns inbox wording change in…
tiago-peixoto Sep 29, 2026
fdb2351
no-mistakes(review): Keep checkpoint inbox check; forbid only polling…
tiago-peixoto Sep 29, 2026
fc99272
no-mistakes(ci): Fixed ci-2 (Greptile: a concurrent retry marker gets…
tiago-peixoto Sep 29, 2026
d022c71
Merge origin/main, keeping the opt-in wait behind config/wait-no-turns.
tiago-peixoto Sep 29, 2026
40456be
Merge remote-tracking branch 'origin/main' into fm/firstmate-issue422…
tiago-peixoto Sep 30, 2026
ec19e07
no-mistakes(document): Fix stale wait-no-turns inbox wording in inbox…
tiago-peixoto Sep 30, 2026
e75716d
Merge origin/main into fm/firstmate-issue4228-wait-turn-spend
cursoragent Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .agents/skills/bootstrap-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,8 @@ When any diagnostic needs captain attention, report the plain consequence and re
An unsafe-outbox variant requires path and file-type inspection before any retry.
- `NUDGE_SECONDMATES: secondmate <id>: send failed: <reason>` - secondmate convergence changed a running home's loaded instructions or inherited config, but the deterministic `fm-send.sh fm-<id>` re-read nudge failed.
Inspect the reason, keep the pending marker under `state/.secondmate-nudge-pending/` intact, and rerun session start after the endpoint or metadata issue is fixed so bootstrap can retry the exact same marked send on the same local or remote route.
- `NUDGE_SECONDMATES: secondmate <id>: deferred: ...` - the same nudge was not sent because that secondmate is waiting on its own open decision or blocker, and automatic nudges never wake a waiting worker.
Keep the pending marker.
The watcher sends the deferred nudge within one poll of that decision closing (`bin/fm-config-push.sh --retry-deferred`), for local and remote secondmates alike.
- `FMX: X mode on ...` / `FMX: X mode off ...` - bootstrap confirmed or removed the local Relay poll artifacts (`docs/configuration.md` "Relay (.env)"); the emitted line still carries Relay's former `X mode` wording.
Only when a running watcher needs the cadence transition applied immediately, restart the home-scoped watcher through the emitted harness supervision protocol; bootstrap deliberately never restarts the watcher itself.
4 changes: 3 additions & 1 deletion .agents/skills/secondmate-provisioning/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Keep each home's `data/captain.md` domain-local.
After first propagation to an existing home, trim that home's local `data/captain.md` by hand to domain-specific content plus pointers to `data/captain-shared.md`; do not automate or silently delete private content.
Keep every `data/learnings.md` fully local by captain decision; route fleet-general machinery facts into tracked documentation through the normal firstmate repo path rather than inventing shared learnings propagation.
No AGENTS.md reread nudge is needed at spawn or respawn because the agent reads instructions fresh on launch; only the bootstrap sweep's running-home instruction-surface advance needs that AGENTS.md re-read.
Bootstrap reports successful AGENTS.md re-read sends as `BOOTSTRAP_INFO:` and only emits `NUDGE_SECONDMATES:` when that send fails and needs retry.
Bootstrap reports successful AGENTS.md re-read sends as `BOOTSTRAP_INFO:` and only emits `NUDGE_SECONDMATES:` when that send fails or is deferred and needs retry.
A separate, literal-content config reread is required whenever inherited `config/*` material changes under an already-running secondmate.
For a local home, after each successful allowlisted config write, both the locked bootstrap convergence path and mid-session `bin/fm-config-push.sh` use the shared propagation report to build one per-home generation-specific private instruction file from the validated destination post-write bytes for only the declared config items that actually changed for that home, in declaration order.
Each changed path is printed with clear begin/end delimiters and the destination file's full exact new bytes unparsed, or the explicit token `ABSENT` when propagation removed the destination copy.
Expand All @@ -138,6 +138,8 @@ The instruction uses only minimal framing that these are defaults/rules and do n
Homes whose allowlisted config files were all unchanged receive no config-reread message when no retry is pending.
Different homes may receive different changed-file sets based on their pre-push destination bytes.
Delivery uses the existing routed secondmate path (`fm-send`) with only a single-line `CONFIG_REREAD: <absolute generation-specific instruction path>` pointer; a failed instruction publication retains the generated exact bytes in a bounded private retry queue when possible, legacy retry reports remain recoverable, a failed publication or retry-marker write retains the exact generation until it can be delivered, a failed send records a per-generation durable retry marker when possible, and all failures surface a concrete `CONFIG_REREAD:` diagnostic without claiming the live agent already re-read the values.
A reread is an automatic send (`bin/fm-send.sh` header, `--automatic`), so while the secondmate waits on its own open decision or blocker it is deferred rather than sent: the retry marker stays, the diagnostic reads `CONFIG_REREAD: secondmate <id>: deferred: ...`, and the deferral is not a failure.
The deferral is flagged, and the watcher delivers the retained reread within one poll of that decision closing (`bin/fm-config-push.sh --retry-deferred`), for local and remote secondmates alike.
The propagation, generation publication, and pointer-delivery sequence holds one per-home inheritance lock, so concurrent mid-session pushes cannot deliver an older generation after a newer one.
A newly launched or relaunched secondmate already reads its files at launch, so its pending config-reread generations are discarded or quarantined after cleanup failure and it needs no redundant live-agent config nudge unless propagation changes files after launch.
Quarantined pre-relaunch generations are retained in bounded private history, and cleanup skips creating an empty quarantine generation.
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ A persistent secondmate is recorded in the secondmate registry and runtime state
Steer a worker with ordinary text through fail-closed `fm-send`: the message becomes a durable record in the task's steering inbox (multi-line text is legal, local and remote alike) and the worker's terminal receives only a constant doorbell line, with the watcher re-ringing an unacknowledged local message and escalating a stuck one (`bin/fm-task-inbox-lib.sh`; `bin/fm-send.sh` owns the typed-plane carve-outs).
A remote secondmate steer rides the same durable-inbox model through the remote transport; after an unconfirmed delivery, only the exact `FM_PENDING_REPLY_EXISTING_CORR=<id>` resend command printed by `fm-send` is safe because it preserves the request body for remote enqueue deduplication (`bin/fm-send.sh` header).
When a steer answers an open keyed decision or blocker, pass `fm-send`'s `--resolve-key` so the answer itself closes that decision record at answer time, identically for local and remote workers (contract: `bin/fm-send.sh` header).
While a worker's decision or blocker is open, send it only the answer: a holding note or a nudge wakes a worker whose wait should cost nothing.
`fm-send` is the data plane for text the worker should read; never use its key or text paths for interrupt, exit, or other lifecycle control, because routing-marked lifecycle text becomes chat the worker reasons about instead of executing.
Drive a worker's lifecycle through `bin/fm-control.sh <task-id> interrupt|exit|relaunch`, which owns the per-runtime mechanics, verifies each action, and never tears down or discards anything ([`docs/agent-control.md`](docs/agent-control.md)).
A secondmate's routed reply returns through status or a document pointer, not by firstmate peeking into its chat.
Expand Down
56 changes: 38 additions & 18 deletions bin/fm-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -408,8 +408,25 @@ secondmate_sync() {
fm_secondmate_nudge_write "$STATE" "$id" "$home" "$commit" "$instr" "$message" "$remote"
}

# fm-send exits 4 with a "deferred:" line while the mate waits on its own open
# decision; the retained marker keeps the nudge, and the deferral is flagged
# for a local home and a remote route alike so the watcher sends it once that
# decision closes (fm_secondmate_reread_mark_deferred).
# The exit status is what classifies the result, not the shape of the output:
# anything the send prints ahead of that line must not read as a failure.
secondmate_nudge_unsent() { # <id> <fm-send-output> <fm-send-status>
local detail
if [ "${3:-1}" -eq 4 ]; then
fm_secondmate_reread_mark_deferred "$STATE" "$1" || true
detail=$(printf '%s\n' "$2" | grep -m1 '^deferred:') || detail=$(first_line "$2")
echo "NUDGE_SECONDMATES: secondmate $1: $detail"
else
echo "NUDGE_SECONDMATES: secondmate $1: send failed: $(first_line "$2")"
fi
}

secondmate_send_nudge() {
local id=$1 home=$2 commit=$3 instr=$4 selector marker out
local id=$1 home=$2 commit=$3 instr=$4 selector marker out send_rc
selector="fm-$id"
marker=$(secondmate_nudge_marker_path "$id") || {
echo "NUDGE_SECONDMATES: secondmate $id: send failed: unsafe id"
Expand All @@ -419,11 +436,12 @@ secondmate_sync() {
echo "NUDGE_SECONDMATES: secondmate $id: send failed: cannot record retry marker"
return 0
fi
if out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" "$SCRIPT_DIR/fm-send.sh" "$selector" "$SECOND_MATE_NUDGE_MESSAGE" 2>&1); then
out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" "$SCRIPT_DIR/fm-send.sh" "$selector" --automatic "$SECOND_MATE_NUDGE_MESSAGE" 2>&1) && send_rc=0 || send_rc=$?
if [ "$send_rc" -eq 0 ]; then
rm -f "$marker"
echo "BOOTSTRAP_INFO: nudged $selector with '$SECOND_MATE_NUDGE_MESSAGE'"
else
echo "NUDGE_SECONDMATES: secondmate $id: send failed: $(first_line "$out")"
secondmate_nudge_unsent "$id" "$out" "$send_rc"
fi
}

Expand All @@ -433,7 +451,7 @@ secondmate_sync() {
}

secondmate_retry_pending_nudges() {
local marker id selector home commit message remote expected_marker meta meta_home home_real head out
local marker id selector home commit message remote expected_marker meta meta_home home_real head out send_rc
[ -d "$SECOND_MATE_NUDGE_PENDING_DIR" ] || return 0
for marker in "$SECOND_MATE_NUDGE_PENDING_DIR"/*.pending; do
[ -f "$marker" ] || continue
Expand Down Expand Up @@ -492,11 +510,12 @@ secondmate_sync() {
echo "NUDGE_SECONDMATES: secondmate $id: send failed: retry target is not at recorded instruction commit"
continue
}
if out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" "$SCRIPT_DIR/fm-send.sh" "$selector" "$SECOND_MATE_NUDGE_MESSAGE" 2>&1); then
out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" "$SCRIPT_DIR/fm-send.sh" "$selector" --automatic "$SECOND_MATE_NUDGE_MESSAGE" 2>&1) && send_rc=0 || send_rc=$?
if [ "$send_rc" -eq 0 ]; then
rm -f "$marker"
echo "BOOTSTRAP_INFO: nudged $selector with '$SECOND_MATE_NUDGE_MESSAGE'"
else
echo "NUDGE_SECONDMATES: secondmate $id: send failed: $(first_line "$out")"
secondmate_nudge_unsent "$id" "$out" "$send_rc"
fi
done
}
Expand All @@ -522,7 +541,7 @@ secondmate_sync() {
# running home, send its literal-content reread instruction pointer so the
# live agent does not keep applying stale defaults. Spawn/respawn already
# re-reads at launch and needs no redundant nudge unless files changed after launch.
local id home home_real home_lock propagated_homes report reread_out reread_skip_pending
local id home home_real home_lock propagated_homes report reread_out reread_rc reread_skip_pending
propagated_homes=""
SECONDMATE_RESPAWNED_IDS=${SECONDMATE_RESPAWNED_IDS:-}
while IFS='|' read -r id home _window _meta; do
Expand Down Expand Up @@ -572,17 +591,16 @@ secondmate_sync() {
else
echo "SECONDMATE_SYNC: secondmate $id: skipped: inheritance failed"
fi
if ! reread_out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" \
reread_rc=0
reread_out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" \
FM_STATE_OVERRIDE="$STATE" \
FM_CONFIG_REREAD_SKIP_PENDING="$reread_skip_pending" \
fm_config_send_reread_nudge "$id" "$home_real" "$report" 2>&1); then
if [ -n "$reread_out" ]; then
printf '%s\n' "$reread_out"
else
echo "CONFIG_REREAD: secondmate $id: send failed: unknown error"
fi
elif [ -n "$reread_out" ]; then
fm_config_send_reread_nudge "$id" "$home_real" "$report" 2>&1) || reread_rc=$?
[ "$reread_rc" -ne 4 ] || fm_secondmate_reread_mark_deferred "$STATE" "$id" || true
if [ -n "$reread_out" ]; then
printf '%s\n' "$reread_out"
elif [ "$reread_rc" -ne 0 ]; then
echo "CONFIG_REREAD: secondmate $id: send failed: unknown error"
fi
rm -f "$report"
fm_lock_release "$home_lock" || true
Expand Down Expand Up @@ -636,12 +654,14 @@ secondmate_sync() {
fi
[ "$remote_pending" -eq 0 ] || nudge_needed=1
if [ "$converged" -eq 1 ] && [ "$nudge_needed" -eq 1 ]; then
if out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" \
"$SCRIPT_DIR/fm-send.sh" "fm-$id" "$REMOTE_SECOND_MATE_NUDGE_MESSAGE" 2>&1); then
local send_rc=0
out=$(FM_HOME="$FM_HOME" FM_ROOT_OVERRIDE="$FM_ROOT" FM_STATE_OVERRIDE="$STATE" \
"$SCRIPT_DIR/fm-send.sh" "fm-$id" --automatic "$REMOTE_SECOND_MATE_NUDGE_MESSAGE" 2>&1) && send_rc=0 || send_rc=$?
if [ "$send_rc" -eq 0 ]; then
rm -f "$remote_marker"
[ "${FM_BOOTSTRAP_VERBOSE_FACTS:-0}" != 1 ] || echo "BOOTSTRAP_INFO: nudged remote fm-$id after convergence"
else
echo "NUDGE_SECONDMATES: secondmate $id: send failed: $(first_line "$out")"
secondmate_nudge_unsent "$id" "$out" "$send_rc"
fi
elif [ "$converged" -eq 1 ]; then
rm -f "$remote_marker"
Expand Down
29 changes: 26 additions & 3 deletions bin/fm-brief.sh
Original file line number Diff line number Diff line change
Expand Up @@ -349,16 +349,35 @@ INBOX_DIR=$(shell_quote "$STATE/$ID.inbox")
# The receive-and-ack half of the steering-inbox contract, included in every
# scaffold kind. The record format, doorbell line, and re-ring ladder are
# owned by bin/fm-task-inbox-lib.sh; the doorbell itself is self-describing,
# so this section is reinforcement for the natural-checkpoint habit, not the
# only carrier of the instruction.
# so this section is reinforcement, not the only carrier of the instruction.
# Every waiting record rings, so the worker never lists the inbox unprompted.
IFS= read -r -d '' INBOX_SECTION <<EOF || true
# Firstmate instruction inbox
Firstmate steers you through durable message files in $INBOX_DIR.
When a terminal message says an instruction is waiting there - and at any natural checkpoint when you are unsure - list $INBOX_DIR/*.msg, read and act on each message in numeric order, then acknowledge each handled message by moving it: \`mv $INBOX_DIR/NNN.msg $INBOX_DIR/handled/\`.
When a terminal message says an instruction is waiting there, list $INBOX_DIR/*.msg, read and act on each message in numeric order, then acknowledge each handled message by moving it: \`mv $INBOX_DIR/NNN.msg $INBOX_DIR/handled/\`.
The move IS the acknowledgement: without it firstmate rings again and eventually treats you as stuck. An empty or absent inbox needs no action.
Every waiting instruction rings, so never list the inbox on your own.
EOF
INBOX_SECTION=${INBOX_SECTION%$'\n'}

# How a crewmate or scout waits. Every model turn resends the whole context, so
# a wait must cost no turns: a decision wait ends the turn, and an external
# wait sleeps in one bounded blocking shell command sized to the harness.
IFS= read -r -d '' WAIT_SECTION <<'EOF' || true
# Waiting
Every turn you take resends your whole context, so a wait must cost no turns.
After you append `needs-decision:` or `blocked:`, end your turn at once: do not check the inbox, the status file, or anything else, because the answer arrives as a terminal message that starts your next turn.
Wait on anything external - a pipeline gate, PR checks, a heavy-test slot - with ONE blocking shell command that returns when the state changes: `no-mistakes axi run` or `respond` with `--wait`, `gh pr checks <pr> --watch`, or `until <condition>; do sleep 30; done` for anything else.
Never spend turns on `sleep` followed by a status check, and never background a command in order to poll it.
In Claude Code that `until` loop in a single Bash call is the sanctioned foreground wait: when the harness refuses a sleep-then-check command and points you at backgrounding instead, reissue the wait as the loop rather than accepting the background.
Bound that command by what your harness lets one command run: in Pi pass the bash tool a `timeout` of at most 2700 seconds, because Pi sets none by default; in Claude Code pass the Bash tool its maximum `timeout` of 600000 ms, because its default is 2 minutes; in Codex keep waiting on a still-running command with empty `write_stdin` polls of up to 300000 ms; elsewhere pass your shell tool its largest timeout and assume at most 10 minutes.
Give any `--wait` a duration a little under that bound.
When the bound passes with nothing changed, run the same blocking command again, with no status check in between.
The one exception is `respond`: it sent its answer before it began waiting, so reattach with `no-mistakes axi run --wait` instead, and never send the same `respond` again, because it would answer whichever gate parks next without you reading it.
A wait your shell can watch this way needs no `paused:` line, except your own pipeline run, a long foreground command, or your own validation round, which you declare once just before its blocking hold: append `paused:` once just before its first blocking command, then stay in the command, and never append it again as you reissue that command.
EOF
WAIT_SECTION=${WAIT_SECTION%$'\n'}

if [ "$KIND" = secondmate ]; then
SECONDMATE_PROJECTS=""
idx=1
Expand Down Expand Up @@ -572,6 +591,8 @@ $CREWMATE_PAUSE_INSTRUCTIONS
Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append \`resolved [at=<epoch>]: {how it cleared}\` yourself (same \`[key=<slug>]\` if you opened it with one) as you resume.
$SHARED_INFRA_RULE

$WAIT_SECTION

$INBOX_SECTION

# Definition of done
Expand Down Expand Up @@ -650,6 +671,8 @@ $ASK_USER_BLOCK
Firstmate's reply normally writes that closing line at answer time; when a blocker or wait clears WITHOUT a firstmate reply, append \`resolved [at=<epoch>]: {how it cleared}\` yourself (same \`[key=<slug>]\` if you opened it with one) as you resume.
$SHARED_INFRA_RULE

$WAIT_SECTION

$INBOX_SECTION

# Project memory
Expand Down
18 changes: 18 additions & 0 deletions bin/fm-classify-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -916,6 +916,24 @@ EOF
printf '%s\n' "$current"
}

# The subset of status_open_decisions the task raised about its own work: a
# reserved-namespace key is raised by a supervisor library about the task (a
# pending-reply escalation), a `remote-reply-continuity-` key is the parent's
# own blocker about a broken remote reply mirror
# (bin/fm-procevent-remote-reply.sh), and a `captain-hold-` key relays a child
# decision a secondmate escalated to the captain (bin/fm-captain-hold.sh) while
# it keeps working, so the task is not waiting on any of them. Automatic senders
# consult this set and leave a task alone while it is non-empty.
status_own_open_decisions() { # <status-file>
local line prefix
status_open_decisions "$1" | while IFS= read -r line || [ -n "$line" ]; do
for prefix in ${FM_CLASSIFY_RESERVED_KEY_PREFIXES:-$FM_CLASSIFY_RESERVED_KEY_PREFIXES_DEFAULT} remote-reply-continuity- captain-hold-; do
case "$line" in "$prefix"*) continue 2 ;; esac
done
printf '%s\n' "$line"
done
}

# 0 when the fold above still holds at least one decision OPENED by
# `needs-decision` - the status side's own record that a human was asked
# something and has not answered. A `blocked` record is deliberately not this: a
Expand Down
Loading
Loading