Conversation
The three ship-mode Definition-of-done blocks built the brief text with `DOD=$(cat <<EOF ... EOF)` nested inside `case`. Bash 3.2's lexer loses quote state across a heredoc-in-$(...) inside `case`, so a single apostrophe anywhere in the body (reintroduced in kunchenguid#945) aborted the whole script with "unexpected EOF while looking for matching ')'". The scout and secondmate paths use a plain `cat > file <<EOF` redirect (no $(...)) and were unaffected. Rewrite each DOD block as `IFS= read -r -d '' DOD <<EOF || true` plus a single `${DOD%$'\n'}` strip. Byte-for-byte identical brief output proven against the bash-5 baseline for no-mistakes, direct-PR, local-only, and scout scaffolds, under both /bin/bash 3.2 and bash 5. Also extend the existing macos-stock-bash CI job to loop /bin/bash -n over every bin/*.sh and bin/backends/*.sh, so any future bash-3.2-only syntax regression in any tracked shell script fails before merge.
…claude pid (kunchenguid#1206) * fix(session-lock): resolve Claude bg-spare ancestry to the outermost claude pid fm_harness_ancestry_pid() previously returned the first ancestor process whose command matched a verified harness name. Claude Code's Stop hook fires as a bg-spare worker several levels below the session's actual lock-owning claude process (hook shell -> claude bg-spare -> claude bg-pty-host -> claude -> claude(lock)), so the first match was the bg-spare worker, not the lock owner. fm_session_lock_owned_by_self() then never matched state/.lock, and the Claude Stop auto-arm silently treated its own primary session as an unrelated live owner and never armed the watcher. The walk now keeps going past a claude-named match, looking for a still more ancestral claude-named match, and stops the instant a non-match follows an already-found match (bounding it to a contiguous run rather than the literal ancestry top, so an unrelated claude-named process further up the real process tree is never mistaken for part of this session's own nested chain). Every other harness keeps the original first-match-wins behavior, since e.g. Pi's shared signed-wrapper ancestry actually holds the session at the inner engine pid, not an outer wrapper pid. Hop limit raised from 8 to 16 to cover the deeper bg-spare chain. * no-mistakes(review): Add nested-claude-ancestry regression test; fix nudge doc depth claim * no-mistakes: apply CI fixes
…d#1195) * fix(spawn): forward firstmate's CLAUDE_CONFIG_DIR to claude crewmates Crewmate panes are created by a long-lived tmux/herdr daemon that does not inherit firstmate's current environment. When firstmate runs under a non-default CLAUDE_CONFIG_DIR (for example a work-vs-personal subscription split), a bare `claude` in the crewmate pane fell back to the default ~/.claude store and launched unauthenticated, blocking the crewmate before it could do any work. fm-spawn now prefixes the claude launch with firstmate's own resolved CLAUDE_CONFIG_DIR when set, so the crewmate uses the same credential/config store firstmate is authenticated with. An unset value is the single-store default and adds no prefix; non-claude harnesses are unaffected. Adds three tests in fm-spawn-dispatch-profile.test.sh (forwarded-when-set, omitted-when-unset, non-claude-ignored) and pins CLAUDE_CONFIG_DIR in the test helper so launch assertions no longer depend on the developer's environment. * no-mistakes: apply CI fixes
Add a turn-independent continuity layer that re-arms a silently-dead watcher on a launchd timer, closing the gap where a watcher dies during an idle gap with no Stop hook to re-arm it and state/.last-watcher-beat goes stale for hours. - bin/fm-watchdog-check.sh: checker reusing the Stop auto-arm's gates (primary scope, supervision need, not-AFK, live session lock) plus the shared single-flight owner lock (state/.claude-autoarm.lock), so it never double-arms with the hook and only acts when the beacon is past grace. It foregrounds the real arm wrapper (never shell &) and never writes outcome=rewake, since a launchd arm produces no Claude continuation and that outcome would falsely suppress one. A launchd process tree is not a harness ancestry, so it gates on a live lock holder rather than owned-by-self. - launchd/com.firstmate.watcher-watchdog.plist: per-user agent template (RunAtLoad, 90s StartInterval) the installer stamps per home. - bin/fm-watchdog-install.sh: stamp/load/unload + status; per-home label so homes under one user do not collide; idempotent reload; fails loud off-macOS. - tests/fm-watchdog-check.test.sh: re-arm vs no-op (fresh beacon, each gate, held single-flight) and never-double-arm concurrency, plus the installer and plist-template contract. - docs/watcher-watchdog.md with a cross-reference from watcher-continuity.md; scripts.md, documentation-audiences.json, and the watcher-wake-lock test family wiring.
# Conflicts: # .github/workflows/ci.yml # CONTRIBUTING.md # bin/backends/herdr.sh # bin/fm-brief.sh # bin/fm-session-lock-lib.sh # bin/fm-spawn.sh # bin/fm-test-run.sh # docs/configuration.md # docs/scripts.md # docs/sessionstart-nudge.md # docs/verification/supervision.md # tests/fm-spawn-dispatch-profile.test.sh
…issing fixture cursor lib
The scenarios that pin single-flight, lease hand-off, and stale-lock refusals deliberately leave run workers alive when their assertion is made; those workers are orphaned to init and keep polling after the suite exits, and on the Linux CI shard their concurrent sleep loops reliably blew the sub-second deadlines of the timing-sensitive suite scheduled next (observed: five orphaned workers made tests/fm-pi-watch-extension.test.sh fail four shard runs in a row while passing five-for-five in isolation). Reap every process still referencing this run's unique TMP_ROOT at exit, collapsing the double slash a trailing-slash TMPDIR (macOS) leaves in TMP_ROOT but never in the workers' own argv.
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.
Intent
Adopt three weeks of upstream firstmate development (132 commits since merge-base a5fe1bc) into hnvivek/firstmate as one reviewed merge, resolving all overlap in favor of upstream where upstream already landed our local fixes (session-lock ancestry, CLAUDE_CONFIG_DIR spawn forwarding, Bash 3.2 brief scaffolding), while preserving the local launchd watcher-watchdog backstop feature and its tests/docs/lane wiring.
What Changed
CLAUDE_CONFIG_DIRforwarding to claude crewmates, and Bash 3.2 parsing offm-brief.sh.bin/fm-watchdog-check.shre-arms the watcher when the continuity beacon is missing or stale pastFM_GUARD_GRACE(skipping AFK, no-work, child-worktree, and held-owner-lock cases), andbin/fm-watchdog-install.shinstalls/uninstalls/audits thecom.firstmate.watcher-watchdogLaunchAgent, stampingFM_HOMEand the user'sPATHinto the plist at install.docs/watcher-watchdog.mdand cross-referenced it from the watcher-continuity, turnend-guard, and configuration docs; addedtests/fm-watchdog-check.test.shcovering arm/no-arm behavior, installer verbs, and the plist template contract.Risk Assessment
✅ Low: The only new change since the reviewed head is a well-bounded fix for the round-1 PATH finding — correctly sed-escaped, wired into the template contract test, and documented — leaving just one rare, fail-loud XML-escaping edge as an informational note.
Testing
Ran the watchdog backstop's own test suite plus the suites covering the three upstream-resolved local fixes (all pass), then demonstrated the feature end-to-end outside the test harness: in a hermetic fixture home the checker armed exactly when the beacon was stale and a live claude session owned the lock, stayed quiet while the beacon was fresh or the owner was dead, and never exited nonzero; the install-time PATH stamping was verified to yield a valid launchd plist under the real user PATH, and the installer's status verb ran cleanly. No failures; working tree left clean and fixtures removed.
Evidence: End-to-end watchdog checker transcript (4-tick lifecycle in a hermetic fixture home)
Evidence: Stamped watchdog LaunchAgent plist as launchd would load it (real user PATH, per-home label)
Evidence: Installer status CLI transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
.agents/skills/afk/SKILL.md- branch carries 6 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (335 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
launchd/com.firstmate.watcher-watchdog.plist:26- The agent sets only FM_HOME in EnvironmentVariables, so the checker and the watcher it arms run under launchd's minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin), not the user's shell PATH. fm-x-poll.sh requires jq (line 103), which macOS does not ship at /usr/bin — on a Homebrew jq install the X-mode poll degrades to a rate-limited 'x-mode-error missing jq' line, which the watcher classifies as an actionable check: wake. The watchdog then re-arms on every grace window with no session to drain, accumulating spurious wake-queue entries instead of providing supervision. The same minimal PATH can also make the scope gate'sgit -C(bin/fm-primary-scope-lib.sh:26) fail on machines without CLT's /usr/bin/git, leaving the watchdog silently inert.bin/fm-watchdog-install.sh:43- home_slug collapses all non-alphanumeric characters to a single dash, so two distinct homes whose paths differ only in such characters (e.g. /Users/x/my.home vs /Users/x/my-home) map to the same launchd label; installing the second silently repoints the shared plist to the second FM_HOME, and uninstalling either removes both homes' agent. The comment promises the slug keeps homes distinct, but it only makes collisions unlikely, not impossible.bin/fm-watchdog-check.sh:105- session_lock_live exits 0 whenever state/.lock names a dead harness pid and never reclaims it (unlike the Stop auto-arm, which recovers stale locks via fm-lock.sh). So if the harness itself crashes mid-idle-gap — not just the watcher — the home stays unsupervised until a new session starts, even with the watchdog installed. This is the documented deliberate boundary (header lines 41-46), noted here only so the backstop's coverage ceiling is visible during review.🔧 Fix: Stamp user PATH into watchdog LaunchAgent plist at install
1 info still open:
bin/fm-watchdog-install.sh:59- sed_escape covers the sed replacement side but not XML: a PATH or home path containing & (or <, >) is stamped verbatim into the plist, producing invalid XML that launchd refuses to load. The failure is loud (do_install dies with the launchctl diagnostic pointing at the plist), and such PATHs are rare, so this is a hardening note, not a merge blocker. XML-escaping stamped values (or running plutil -lint before load) would close it.✅ **Test** - passed
✅ No issues found.
bash tests/fm-watchdog-check.test.sh — full watchdog behavior suite (12 checks: arm on missing/stale beacon, no-op on fresh beacon/no work/AFK/no live lock/child worktree/held owner lock, no double-arm, installer verbs, plist template contract)bash tests/fm-session-lock-ancestry.test.sh — upstream resolution of the local session-lock ancestry fixbash tests/fm-spawn-dispatch-profile.test.sh — upstream resolution of the local CLAUDE_CONFIG_DIR spawn forwarding fix (explicit 'claude forwards firstmate's CLAUDE_CONFIG_DIR' checks)bash tests/fm-brief.test.sh — upstream resolution of the local Bash 3.2 brief scaffolding fixbash tests/fm-watch-arm.test.sh — shared single-flight arm path the watchdog checker foregroundsManual e2e: hermetic fixture primary home (git checkout + AGENTS.md + bin/state) with fake fm-watch-arm.sh and a claude-named live session pid in state/.lock; ran bin/fm-watchdog-check.sh across four ticks (beacon absent → armed; fresh → no arm; stale after grace → armed; session owner killed → no arm) at FM_GUARD_GRACE=2Manual verification of commit 11f13b6: replicated install-time stamp_plist against the real user PATH, validated the stamped plist with plutil, and extracted Label/ProgramArguments/RunAtLoad/StartInterval as launchd reads themFM_HOME=$(pwd) bin/fm-watchdog-install.sh status — real installer CLI verb, read-only, exit 0docs/fm-test-portable-shards.md:79- The portable-serial shard table's script counts (15/18/17/19 of 69 scripts) predate this change and now differ further from actual lane membership (25/30/28/28 of 111, confirmed via bin/fm-test-run.sh --list). The change added tests/fm-watchdog-check.test.sh to portable-serial without a weight hint, so it lands under PORTABLE_SERIAL_DEFAULT_WEIGHT_MS. Not edited: the table is a dated maintainer-verification record of the 2026-08-02 CI measurement and its prose scopes it to that run, and the doc's own refresh procedure requires downloading fresh CI timing artifacts rather than hand-authoring counts. Pre-existing drift, marginally extended by this change.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.