Conversation
state/captain-outbox.jsonl row 17 had two JSON objects glued without a newline separator. Root cause: concurrent appends from drain + LLM agent + manual retry interleave because no writer held a lock. Fix: new bin/fm-captain-outbox-append.sh helper with flock; all writers migrate to it (drain.sh ack append; wake.md LLM agent path). review-submit.sh keeps its own flock but references the same lock path. Regression test: tests/fm-outbox-concurrency.test.sh spawns 30 concurrent writers and asserts every line is valid single-line JSON. Test passes after fix.
Restore the hash-bound submission helper lost in the cleanup of the outbox-writer-lock worktree. The helper emits a kind=report-submission done row to state/captain-outbox.jsonl on the shared flock and is the only writer surface the controller's find_submission_events filter recognises for enrolled review jobs. Also restore bin/fm-captain-inbox-append.sh from the same canonical staging source; the colocated test depends on it for the inbox --json assertion and cannot run without it. Both scripts gain the canonical `# shellcheck source=bin/fm-wake-lib.sh` directive above their fm-wake-lib.sh source line, matching the convention carried across bin/ (bin/fm-guard.sh:37). ShellCheck cannot resolve the runtime $SCRIPT_DIR without it, so --external-sources alone leaves SC1091. The test's ten `cond && ok || ko` assertions are rewritten as `if cond; then ok; else ko; fi` to clear ShellCheck SC2015, matching the single-line if/then/else assertion style used by sibling tests. Trigger conditions are unchanged.
…over-firing
Closes the 2026-08-03 asyncRewake gap (data/handoff-2026-08-03-wake-and-lint.md
item 1; data/learnings.md 2026-08-03): when firstmate runs under the
daemon+argv topology, Claude Code Stop-hook asyncRewake does not re-invoke
the idle primary. The backstop is a second line of defense: the already-
armed watcher monitors state/.claude-autoarm-epoch, and after
FM_WATCH_BACKSTOP_GRACE (default 60s, which clears the synchronous
FM_CLAUDE_AUTOARM_EPOCH_FRESH 15s plus typical background-task notification
latency) it spawns a detached bash task that re-checks the epoch, consumes
the unconsumed rewake row, and injects the message into the primary pane
via the same channel the away-mode daemon uses (bin/fm-operational-input.sh
+ bin/fm-backend.sh fm_backend_send_text_submit). Background Bash task
completions re-invoke the idle session (observed 2/2); asyncRewake is
retained as the opportunistic fast path.
The inject path is bounded by timeout, not by positive confirmation: a
delivered rewake can only signal consumption by running the next turn, so
in the failure case the observation never happens. Idempotency is owned
by the bash task (not the watcher) so the re-check happens as close to
injection as possible: epoch is re-checked once after locating the row
and again immediately before consuming it; the consume step is the LAST
step so any earlier guard failure leaves the row in the queue. A
FM_WATCH_BACKSTOP_CONFIRM_INJECT=1 gate fail-closes the send so a probe
against a non-live state with an unresolved supervisor target cannot
deliver a real wake to the captain (the 2026-08-03 incident that
motivated the brief).
The secondary defect (item 1, "Secondary defect found in the same area")
folded into the same change: surface_nonterminal_stale was emitting a
bare "stale: <win>" wake every ~2 minutes for live agents on a declared
pause, bypassing handle_paused_stale. The fix routes that path through
handle_paused_stale with a first-sight fire flag so the LIVE-agent first
sight surfaces an annotated wake once (not hidden behind the long
PAUSE_RESURFACE_SECS cadence), while the DEAD-agent captain-held path
still absorbs on first sight. A successful surface in either branch
writes .paused-resurfaced-<key> so the long cadence governs every
subsequent recheck, not every pane change.
New files:
bin/fm-watch-backstop.sh the delivery helper (should-fire /
fire / inject subcommands, with
FM_WATCH_BACKSTOP_CONFIRM_INJECT
test-isolation gate)
tests/fm-watch-backstop.test.sh 15 hermetic cases covering each
trigger gate, the confirm gate, the
race-window idempotency, the
watcher's per-cycle tick, and the
paused-defect routing
Modified files:
bin/fm-watch.sh the per-cycle fm_backstop_tick,
the surface_nonterminal_stale
paused-routing fix, and the
handle_paused_stale first-sight
fire flag
docs/turnend-guard.md the wake-backstop design contract,
with the constraint self-check
carried from the brief
tests/fm-watch-triage.test.sh the live-decision-gate assertion
updated to expect the new
first-sight annotated reason
Constraint self-check:
- One supervision cycle ............. satisfied: the watcher is the
cycle; the bash task is
delivery plumbing, not an arm
- Stop-hook ownership not duplicated satisfied: fm-turnend-guard.sh
keeps sole ownership of the
Stop emit; the backstop only
adds a delivery attempt after
the preferred one
demonstrably failed
- AGENTS.md section 1 ............... satisfied: this is firstmate
shared tracked material; the
change ships through the
repo's own delivery path (a
local-only fast-forward
merge), never by firstmate
directly
Acceptance criteria demonstration:
1. Backstop fires and delivers
Demonstration: tests/fm-watch-backstop.test.sh
test_fire_spawns_detached_inject_with_confirm_flag sets up an
aged rewake in a scratch state, runs the backstop's fire
subcommand, waits for the detached inject child to consume the
row, write the consumed marker, and rewrite the epoch outcome
to consumed. A fake tmux in PATH shadows the only subprocess
the real inject touches, so the test is hermetic.
2. Idempotency holds
Demonstration: tests/fm-watch-backstop.test.sh
test_inject_idempotency_epoch_outcome_changed_aborts_cleanly
advances the epoch outcome to consumed between the should-fire
pre-check and the inject's second re-check, then asserts the row
is preserved (consume happens after the second re-check) and
the consumed marker is NOT written.
3. Paused defect fixed
Demonstration: tests/fm-watch-backstop.test.sh
test_surface_nonterminal_stale_paused_uses_long_cadence asserts
the LIVE-agent first sight fires exactly one annotated wake
(the bare text would be the bug this fix removes), and the
existing test_nonterminal_stale_paused_absorbed_then_resurfaced
in tests/fm-watch-triage.test.sh continues to verify the
DEAD-agent captain-held path still absorbs on first sight.
Co-Authored-By: Claude <noreply@anthropic.com>
fm-research-worker-spawn.sh + test (P3, 2026-08-04): - bypass treehouse/fm-spawn (treehouse get hung on git fetch origin, 60s timeout in this env); spawn fresh worker via direct tmux new-window + claude -p in the spec temporary project_dir (firstmate home). - preallocate UUID session_id, pass to claude --session-id, wait for the exact session .jsonl (removes concurrent-worker shared-directory race). - stdin-based brief (claude -p ... < brief) not shell interpolation. - fail-closed on tmux-create / send / session-capture failure: no .meta written, inbox retry boundary preserved. - --permission-mode bypassPermissions per P3 decision source (not --dangerously-skip-permissions). Verified: test 12/12 pass; bash -n OK; live smoke 2 concurrent harmless report_research rows -> 2 distinct session_ids (50a5aace / 83553418); P2 drain tests intact; old fm-fresh-context-executor test (treehouse path) recorded incompatible, not a P3 defect. Codex repaired the two files; puti reviewed + verified + committed. Per CLAUDE.md commit-attribution Option A.
The watcher's hash-stable stale detection fires on firstmate:0 every cycle when claude -p processes a long tool call (the rendered pane footer does not change), the auto-arm treats that as actionable, rewakes the owner, and a fresh watcher cycle sees the same primary again — an endless self-triggering Stop-hook loop. This fix compares each window against the canonical target recorded in state/firstmate.meta and skips stale/wedge classification for that one pane only. Worker panes retain normal supervision. The Stop/SessionEnd hooks remain the authoritative liveness channel for the primary owner. Added a focused regression (tests/fm-watch-triage.test.sh) that proves a stable primary neither emits a wake nor enqueues a record while a stable child pane still surfaces, plus a docs/verification/supervision.md entry dated 2026-08-05 with the live-fix evidence. bin/fm-watch.sh was already dirty before this fix (P2 inbox-drain hunks that remain uncommitted). This commit includes only the new primary-exclusion hunk; the P2 hunks stay in the working tree for a separate commit.
…ests Sprint WIP 0805: - P2 live-drain: when captain-inbox surfaces a signal wake, drain inbox immediately so the agent sees the message in this turn rather than next. Hunks in fm-captain-inbox-drain.sh (P2 hunks), fm-watch.sh, fm-research-worker-spawn.sh, fm-claude-stop-autoarm.sh. - Wake-drain helper: bin/fm-captain-wake-drain.sh surfaces FIRSTMATE CAPTAIN INPUT v1 blocks as parsed captain instructions with corr/kind/seq. - Proxy env helper: bin/fm-proxy-env.sh (WIP, unverified in this commit). - Outbox append hardening: bin/fm-captain-inbox-append.sh flock already in; session-start / watch-arm small follow-ups. - Colocated tests: tests/fm-captain-wake-drain.test.sh, tests/fm-fresh-context-executor.test.sh, tests/fm-mid-session-drain.test.sh, tests/fm-p2-live-drain.test.sh. Scope: 7 modified bin/*.sh (141 insertions) + 6 untracked (2 bin + 4 tests). Origin push deferred (proxy+凭证). Merge to main deferred for captain review.
… stale fresh-context-executor test - helper: wrap the real-claude-p session-file wait in an if-else guard; tests set FM_RESEARCH_WORKER_SKIP_SESSION_WAIT=1 to skip it (fake tmux cannot simulate claude -p writing the session file). production never sets it. - test: fm-fresh-context-executor.test.sh is stale — it fakes fm-spawn via FM_SPAWN_BIN, but the helper was rewritten to call claude -p directly via tmux send-text. Skip with reason until re-architected to fake claude-p. (puti review of fm WIP — fm blind spot: committed WIP with a failing test.)
…per + colocated tests + puti test-hook fix
…puti) #1 fm-spawn.sh: accept --effort default (profile sentinel) + translate to high (concrete) instead of hard-rejecting. Callers passing the profile-level value no longer hit the enum error. #3 fm-decision-hold.sh: add early parens check on --reason with a clear hint to rephrase (cite line ranges as L513-517, not (controller:513-517)) rather than failing inscrutably downstream in tasks_axi. (puti acting on fm friction log — Alex directive: puti出手改 fm, not fm self-mod.)
bin/fm-watch-arm.sh's watch_output_has_wake learned the inbox-drain prefix in
the 2026-08-04 P2 live-fix, but bin/fm-claude-stop-autoarm.sh's actionable
loop was not updated to match. When the watcher's auto-drain path surfaces
an inbox-drain wake:
- fm-watch-arm.sh prints the wake (actionable-inbox-drain) and returns 0
- the Stop auto-arm's grep on the arm output finds no signal:/stale:/check:/
heartbeat match, so ACTIONABLE stays 0
- the next loop iteration's fm_watcher_healthy probe fails (the watcher
has exited), so HEALTHY stays 0
- after the bounded 2-attempt loop converges with no actionable and no
healthy watcher, the FAILED notice path fires even though the watcher
just delivered its wake and rewake is the correct outcome
The cycle log confirms this is the dominant close reason in this home:
every recent actionable cycle closed with reason=actionable-inbox-drain,
so the Stop hook routinely hit the false-failure path.
Fix: mirror bin/fm-watch-arm.sh's actionable regex in the auto-arm loop and
the rewake banner grep, so an inbox-drain wake short-circuits both into the
rewake exit-2 path.
Co-Authored-By: Claude <noreply@anthropic.com>
…drain + fm-wake-drain)
…ipt-before-push (worker 122)
…s 1-5 + yolo + precedence byte-identical (worker 125 redo, base b935f7a)
…onstant in fm-claude-stop-autoarm.sh + fm-watch-arm.sh, invariant test pins drift (F5 watcher drop fix per w2-02 brief + captain seq 145)
Wave4 brief fm-0808-w4-02. A worker that runs `git diff HEAD~1..HEAD` over a brand-new file produces a new-file diff from /dev/null, which fails to apply on a captain checkout that already has the file committed (F16, observed on fm-0808-w3-02). This adds bin/fm-worker-diff.sh which inspects each changed path in the worktree: tracked files get `git diff HEAD` (modification diff), untracked files under data/ get `git diff --no-index /dev/null` (new-file diff). A colocated tests/fm-worker-diff.test.sh round-trips every emitted patch through `git apply` on a fresh clone, including the exact F16 scenario and an out-of-scope untracked filter.
Non-FF: branch fork (2d2be63) is not an ancestor of current main (29135f1); a rebase would require resolving AGENTS.md conflicts that are out of scope for F16 (the brief explicitly excludes AGENTS.md). Trade-off: a merge commit joins two independent fork lines, but the joined commit only adds the two new files (bin/fm-worker-diff.sh + tests/fm-worker-diff.test.sh), which no main commit touches, so the substantive change is identical to a rebased FF. # Conflicts: # AGENTS.md
…om fm-review-submit
Closes the puti 监控漏 (P1 fm-side) gap: workers that only knew the
canonical submit helper had no automatic per-task done row in the
captain outbox, so the controller lost completions whenever the
worker forgot to append a chat row.
What lands:
- bin/fm-task-done.sh: new wrapper that builds the canonical
"<task-id> done at <artifact path>" row and routes it through
bin/fm-captain-outbox-append.sh with --idempotent. The helper
never writes to the outbox directly, so the row 17 atomicity
contract is preserved.
- bin/fm-captain-outbox-append.sh: adds --idempotent, which
dedupes on (corr, kind) under the same flock as the append.
Two concurrent callers cannot both insert a duplicate row.
- bin/fm-review-submit.sh: when --task-id is supplied, the
helper also fires fm-task-done.sh with the submitted report
as the artifact, so a worker that only calls the submit
helper now emits the per-task done row for free. The auto
emit is best-effort; the report-submission row remains the
contract.
- tests/fm-task-done.test.sh: colocated test (16 cases) that
covers the new helper, the --idempotent flag on the outbox
appender, and the auto-emit path through fm-review-submit.
Existing behavior preserved: the unchanged-outbox-row surface
(kind=report-submission), the receipt.diff write, and the
existing tests all still pass.
Author
|
Closing — these are captain-side workflow commits (outbox receipts back to a captain session, fm-captain outbox surface, worker产物) specific to our firstmate fleet operation, not general-purpose contributions to firstmate core. Wrong target. Apologies for the noise. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
22 captain-side firstmate机制修复 commits, kept local until now (captain-side workflow + worker产物). Upstreaming for visibility. Includes sync with upstream #2005 / #1997 / #1984 / #2029 (clean auto-merge, no conflicts).
Commits by area
Outbox / receipt:
7d0b3adper-task outbox receipt via fm-task-done.sh + auto-emit from fm-review-submit5cb81ca/611d136F16 modification-vs-new diff scaffolding for worker patchesb935f7afm-review-submit consolidated diff base-sha HEAD + receipt-before-push9566430flock on captain-outbox writers (row 17 corruption)bcf6c47restore fm-review-submit.sh + colocated testAGENTS / docs:
98d0d26compress AGENTS.md hot/warm/cold layering 550→460 (-16.4%), hard rules 1-5 byte-identicalQuota / spawn / workcell:
196e2a6fm-minimax-quota.sh pre-flight + fm-spawn batch sizingeee1c6b/bf7b074workcell: separate admission from dispatch + remove false readiness gates6bce5b5/3008c28research: allow worker cold starts + isolate workers + route revisions02f6ed1P3 fresh-context worker spawn bypass treehouse690da87FM_RESEARCH_WORKER_SKIP_SESSION_WAIT test hook/fm-wake / drain:
eedfcf4/fm-wake surfaces INPUT via two scripts (fm-captain-wake-drain + fm-wake-drain)f46e2b6Stop auto-arm: recognize inbox-drain actionable wake prefix16b8d87P2 inbox-drain-on-signal + wake-drain helper + colocated tests5d4158dstop watcher firing stale: firstmate:0 self-wake loopf1c50d3wake backstop for daemon+argv topology + fix paused over-firingFriction / hardening:
038c7aafm friction chore: initialize no-mistakes gate #1 effort enum + docs: align tmux and harness guidance #3 decision-hold parens hint29135f1FM_AUTOARM_WAKE_PATTERN hardening + invariant testa17519bmerge fm-sprint-0805-wipCaveat
Some commits are captain-workflow-specific (per-task outbox receipts surface back to a captain session, fm-captain outbox rows) and may be too captain-specific for upstream core. Happy to split, scope down, or drop any subset if reviewers prefer.