feat: harden fleet supervision and agent lifecycle management - #3
Closed
roelofb wants to merge 247 commits into
Closed
feat: harden fleet supervision and agent lifecycle management#3roelofb wants to merge 247 commits into
roelofb wants to merge 247 commits into
Conversation
* fix(fleet-sync): auto-sync on merged-PR wake, accept project name
fm-fleet-sync.sh's single-project form failed on a bare project name
("not a directory"), forcing hand-typed full paths (4 manual runs in
one incident). It now resolves a bare name or projects/<name> against
the home's projects dir.
AGENTS.md now encodes the trigger: a wake whose status reports a
merged PR for a project cloned in this home runs fleet-sync for that
project as part of handling the wake, so a secondmate-reported merge
does not leave the primary's clone stale until the next session start
or teardown.
* no-mistakes(review): Fix fleet-sync project name shadowing
* no-mistakes(document): sync fleet-sync docs
* fix(spawn): canonicalize worktree-isolation guard against symlinked project prefixes fm-spawn.sh compared a logical PROJ_ABS against the physically-resolved pane cwd every backend reports, so a project reached through a symlinked prefix (e.g. macOS's /tmp -> /private/tmp) could trip the isolation guard's false refusal before treehouse ever moved the pane. Canonicalize once into PROJ_ABS_REAL and compare against that everywhere instead. * no-mistakes(review): Canonicalize spawn cwd comparisons * no-mistakes(document): Refresh symlinked spawn docs
* docs: add Orca operator skill * no-mistakes(document): Document Orca checklist --------- Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>
* fix(crew-state): detect green-PR CI monitoring, escalate repeat wedges fm-crew-state.sh's ci step never distinguishes "still waiting on checks" from "checks green, waiting on merge" via axi status alone, since a repo that defers merge to the captain keeps the ci step at status=running for the whole monitor phase. Read the ci step's own log tail (axi logs) for the checks-passed marker and surface done instead of a false "validating (running)" - verified against the real PR kunchenguid#252 run's ci.log. The watcher's wedge timer can re-escalate the same stale pane forever without ever signaling that it is a repeat; track a per-pane consecutive escalation count and add a demand-deep-inspection marker to the wake payload once it crosses a threshold, so the supervisor can no longer dismiss each one as an isolated, still-validating pane. Also clarify the ship-brief's checks-green line: it is owed at the CI-ready return point, not after the background monitor-until-merge loop finishes. * no-mistakes(review): Captain, distinguish pending no-checks CI marker * no-mistakes(review): Harden CI relapse handling * no-mistakes(review): Block stale done during fixing * no-mistakes(review): Captain, tighten CI status gating * no-mistakes(review): Captain, harden stale CI green handling * no-mistakes(review): Captain, recognize ranged CI rearm markers * no-mistakes(document): Sync crew-state supervision docs
* fix(teardown): recover from a stale worktree git index.lock A crew process killed mid-git-operation can leave a stale .git/worktrees/<wt>/index.lock behind, making fm-teardown.sh's `treehouse return --force` fail closed. On that failure, retry once after a short wait (the owning process may be exiting), then remove the lock and retry once more only when it is provably stale: old enough by mtime and lsof shows no live holder on the lock or the worktree itself. A lock that isn't provably stale is left in place and the original failure still surfaces. * no-mistakes(review): Harden teardown lock refusal paths * no-mistakes(review): Harden stale-lock teardown safety rechecks * no-mistakes(review): Harden stale teardown lock checks * no-mistakes(document): Document teardown lock recovery
…-governance section (kunchenguid#307) * Encode project AGENTS authoring bar * no-mistakes(review): Captain, centralize CLAUDE promotion governance * no-mistakes(review): make ensure_maintenance_section idempotent-success, drop || true guards * no-mistakes(review): separate appended maintenance section on newline-less CLAUDE.md promotion * no-mistakes(review): assert maintenance heading present before separator check in test * no-mistakes(document): sync docs with AGENTS.md authoring bar and self-governance --------- Co-authored-by: fmtest <fmtest@example.invalid>
…chenguid#300) * Add captain-invocable bearings skill Generates a pick-up-where-I-left-off status report from live fleet state to data/status-report-<YYYY-MM-DD>.md plus a concise chat summary. Read-mostly procedure: reads backlog, per-task crew state via bin/fm-crew-state.sh, open PRs via gh-axi, scout reports, pending decisions, and date-gated queued work; composes the exemplar's sections (TL;DR, Check first, Landed, In flight, Plans, Decisions pending, Date-gated/queued); never tears down, merges, or mutates task state as a side effect. * no-mistakes(document): docs: list new /bearings skill in README built-in skills table
* fix(watcher): pin LC_ALL=C in fm_pid_identity for locale-invariant identity ps's lstart date format follows the caller's LC_TIME/LC_ALL. The watcher records its process identity under one locale, but arm/guard/turn-end re-read it under the machine's ambient locale. On a non-C locale (e.g. ko_KR) the two strings differ only in the date portion, so fm_watcher_lock_matches_pid / fm_watcher_healthy reject a genuinely live watcher - breaking fm-watch-arm.sh, fm-guard.sh, and fm-turnend-guard.sh on every non-C-locale machine. Pin LC_ALL=C on that one ps call so the write and read sides agree regardless of machine locale, matching the LC_ALL=C determinism the file already uses elsewhere. Add a colocated regression test asserting fm_pid_identity is locale-invariant across exported LC_ALL/LC_TIME. * no-mistakes(document): Document watcher PID identity coverage
* docs: reconcile Codex App backend contract * no-mistakes(document): Sync backend docs * docs: clarify Codex Desktop bridge blocker * no-mistakes(document): Align Codex App backend docs * no-mistakes(test): Captain, stabilize watcher self-eviction test cadence * no-mistakes(document): Document Codex App backend contract * no-mistakes(document): Captain, document blocked codex-app coverage * docs: make Codex App contract doc authoritative * no-mistakes(document): Align Codex App backend docs * docs: redact local Codex App smoke paths --------- Co-authored-by: Stephen Brouhard <vesta@stephens-macbook-air.tail2122af.ts.net>
* docs: add Codex App coordination skill * no-mistakes(review): Captain, mark Codex App skill agent-only * no-mistakes(document): Document Codex App backend boundary * no-mistakes(document): Captain, document Codex Desktop backend boundary * no-mistakes(lint): Captain, lint clean * no-mistakes(document): Document Codex Desktop boundaries * docs: narrow Codex App skill playbook
* fix(afk): recognize unbordered herdr composer rows to stop escalation redelivery loop fm_backend_herdr_composer_state only recognized bordered composer rows (the grok shape). Real claude and codex render their live input row with no border at all, so once a harness's own startup banner scrolled out of the capture window the classifier read the composer as unknown forever. fm_backend_herdr_send_text_submit never confirmed "empty", so escalate_flush never cleared state/.subsuper-escalations, and the away-mode daemon retyped and resubmitted the same buffered digest every housekeeping cycle - reproduced live against a real herdr+claude pane (5+ identical deliveries in 40s). The classifier now recognizes an unbordered (bare) composer row led by a known prompt glyph alongside the existing bordered shape, keeping whichever match is bottom-most so a stale decorative box never outranks the live composer. * no-mistakes(review): Narrow herdr bare prompt matcher * no-mistakes(document): Sync herdr composer docs
…guid#323) * fix(herdr): confirm message submit via native agent-state, not composer text fm_backend_herdr_send_text_submit now confirms a landed submit by polling herdr's own agent-state (agent get) for the idle->working transition instead of reading composer content. Composer scraping remains, unchanged, for the away-mode daemon's pre-injection empty-box guard only. This fixes the practical effect of the codex idle-tip gap from the 2026-07-07 incident: codex's dynamic idle-composer hint text can no longer misread as pending and block/mis-confirm a send, since confirmation no longer looks at composer text at all. Verified empirically against real claude and codex agents (timing, swallowed-Enter, unreadable-target, and already-busy-target scenarios), and against the real away-mode daemon end-to-end after updating its synthetic supervisor-pane test fixture to register itself as a real herdr agent (herdr's own report-agent primitive) so it can still exercise the new confirmation path. * no-mistakes(review): Captain, harden herdr submit confirmation * no-mistakes(review): Captain, harden herdr submit confirmation * no-mistakes(document): Sync Herdr submit docs * no-mistakes: apply CI fixes
* Add quota-balanced dispatch selection * no-mistakes(document): Document dispatch selector guidance
* fix(session-start): deterministically respawn dead-shell secondmates A secondmate agent that exits leaves its backend pane alive as a bare shell. The session-start endpoint check only verified pane presence, so recovery and the watcher (which exempts secondmates from stale-pane detection) never noticed - evidence 2026-07-07: every secondmate in one fleet was found sitting at a dead zsh shell. Add fm_backend_agent_alive (bin/fm-backend.sh), a deeper per-backend liveness probe distinct from pane presence: fm_backend_tmux_agent_alive classifies the pane's live foreground process via tmux's own pane_current_command, and fm_backend_herdr_agent_alive reuses the already-verified pane_agent_state husk classifier. Both are conservative: anything ambiguous reports unknown, never a false dead. Wire this into a new session-start-only, locked-and-primary-only sweep in bin/fm-bootstrap.sh that kills and respawns only a confidently dead secondmate endpoint, leaving alive/unknown readings untouched - idempotent by construction, so repeated runs converge without duplicating agents. * no-mistakes(review): Guard raw secondmate liveness respawns * no-mistakes(review): Fix detect-only bootstrap test * no-mistakes(test): Pin liveness fixture harness * no-mistakes(document): Sync secondmate liveness docs * no-mistakes: apply CI fixes
* Fix NUDGE_SECONDMATES to print stable fm-<id> selectors. Session-start secondmate sync used to accumulate raw backend window targets into NUDGE_SECONDMATES, but the liveness sweep in the same bootstrap run can respawn secondmates onto new endpoints. fm-send with those stale explicit targets bypasses meta resolution and fails, while fm-<id> resolves correctly. Accumulate fm-<id> in process_secondmate, update the bootstrap/update contracts and /updatefirstmate skill, and add a herdr respawn regression test. * no-mistakes(review): Captain, guard herdr regression jq dependency * no-mistakes(document): Document stable secondmate nudge selectors * no-mistakes(lint): Fix shell lint hints
* Make tasks-axi and quota-axi required bootstrap tools Add both to the normal toolchain checks alongside lavish-axi, keep the tasks-axi 0.1.1+ compatibility gate, and report quota-axi through the standard MISSING install-consent flow. TASKS_AXI: available remains a backlog-backend capability signal only; manual opt-out no longer suppresses the missing-tool report. Update bootstrap tests and point docs/configuration.md at the canonical toolchain contract. * no-mistakes(review): Clarify manual backlog bootstrap reporting * no-mistakes(document): Document bootstrap AXI tools
Replace overwrite-in-place wording with explicit delete-then-create instructions so agents do not modify an existing daily report file.
* Add primary turn-end guards for all harnesses * no-mistakes(review): Normalize Codex hook cwd resolution * no-mistakes(review): Fix OpenCode guard worktree anchoring * no-mistakes(review): Anchor Codex guard outside nested roots * no-mistakes(review): Anchor Codex guard to hook root * no-mistakes(review): Avoid Grok permission escalation * no-mistakes(document): Sync turn-end guard docs
* fix backend selector task id resolution * no-mistakes(document): Document selector resolution behavior
* fix bootstrap fleet sync timeout * no-mistakes(review): Fix bootstrap fleet-sync timeout regressions * no-mistakes(document): Sync bootstrap timeout docs * no-mistakes(lint): Clean ShellCheck directives * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
* Add fleet snapshot and view * no-mistakes(review): Fix fleet snapshot parsing and overrides * no-mistakes(review): Fix secondmate fleet rendering * no-mistakes(review): Fix backlog title and completion parsing * no-mistakes(review): Include durable scout reports * no-mistakes(review): Fix fleet snapshot edge cases * no-mistakes(review): Captain: gate fleet hints on current state * no-mistakes(review): Captain: parse bracketed Done PR artifacts * no-mistakes(document): Sync fleet snapshot docs
* Make fm-send fail loudly on unresolved targets * no-mistakes(review): Document fm-send FM_HOME contract * Fix fm-send readiness docs and backend send path * Fix fm-send docs for cmux and X skill metadata * Make gotmp teardown test home-explicit * Scope watcher warning wording to fm-send * Fix fm-send review findings * Verify explicit tmux targets before sending * Isolate turnend guard test home * no-mistakes(document): Documented fm-send FM_HOME/backend guard additions missing from doc inventories --------- Co-authored-by: mielyemitchell <249051873+mielyemitchell@users.noreply.github.com>
* fix afk codex ghost composer delivery * no-mistakes(review): Harden AFK startup flag writes * no-mistakes(review): Harden AFK daemon liveness checks * no-mistakes(document): Sync AFK herdr docs * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
* Add harness-aware supervision * no-mistakes(review): Captain, harden watcher supervision regressions * no-mistakes(review): Captain, harden watcher supervision cadence * no-mistakes(review): Harden watcher supervision ownership * no-mistakes(review): Captain, harden Pi extension marker * no-mistakes(review): Captain, harden Pi supervision restart checks * no-mistakes(review): Harden watcher ownership checks * no-mistakes(review): Captain, harden Pi supervision loading * no-mistakes(review): Captain, require Pi guard extension loading * no-mistakes(review): Captain, harden watcher supervision recovery * no-mistakes(test): Fix fm-send baseline log filtering * no-mistakes(document): Sync harness supervision docs * no-mistakes: apply CI fixes
* fix: make x replies split by platform * no-mistakes(review): Captain: preserve Discord recovery relink context * no-mistakes(test): Captain: keep split markers outside fences * no-mistakes(document): Sync X-mode reply docs
* docs: make stow inspect-then-update * no-mistakes(review): Remove unsupported archive-body guidance * no-mistakes(review): Clarify stow read-before-write exception * no-mistakes(test): Require archive-body for stow task notes * no-mistakes(document): Sync stow memory docs * no-mistakes(lint): Silence ShellCheck source warning
…d#375) * fix: attach-and-wait when arm finds a healthy watcher Grok and Claude re-arm after every turn with work in flight. When a watcher was already healthy, fm-watch-arm exited immediately with watcher: healthy, which completed the harness background task and injected an empty false wake. Attach to the live identity-matched holder instead, stay until that cycle ends, then exit 0 so notify fires for a real end-of-cycle. The peer-startup-race path uses the same contract. --restart and the started path are unchanged. * no-mistakes(review): Gate restart watcher peer attach * no-mistakes(document): Sync watcher arm docs
* docs(readme): reformat Quick Start and recommend Grok equally with Claude Code * no-mistakes(review): Captain: align harness launch guidance * no-mistakes(review): Captain, clarify Pi supervised launch * no-mistakes(review): Captain, document Pi first-launch bridge * feat(pi): track primary watcher extension for plain-pi launch Move Pi's primary watcher bridge from a generated state/ file to a tracked .pi/extensions/fm-primary-pi-watch.ts, matching how the turn-end guard extension already works: self-hashing version, project-local auto-discovery after one-time Pi trust. This drops the state/-generation step and dual -e requirement from the happy path, so Pi's Quick Start launch becomes plain 'pi', the same friction class as 'claude' and 'grok --trust'. - bin/fm-pi-watch-extension.sh is removed; nothing generates the extension anymore since it is committed. - fm-session-start.sh and fm-supervision-instructions.sh resolve the watcher extension path from FM_ROOT instead of state/, and the session-start diagnostic now points at restarting plain pi after trust, with -e as a documented fallback. - fm-spawn.sh points Pi secondmate launches at the tracked extension path in the secondmate home instead of generating a state/ copy. - README Quick Start Pi block is now just 'pi' plus a trust note. - Tests, docs, and the harness-adapters skill updated to match. * fix(pi): drop backticks from session-start diagnostic to satisfy shellcheck SC2016
* feat(supervision): add PreToolUse seatbelt against watcher-arm anti-patterns Adds bin/fm-arm-pretool-check.sh, a shared PreToolUse-style checker that denies a primary shell command backgrounding, piping, or bundling the watcher arm/checkpoint, or force-killing the watcher process broadly - the exact shapes that silently took Grok's supervision down. Wires it into all five verified harnesses (grok, claude, codex, opencode, pi), each validated empirically against the real harness. Also fixes a grok 0.2.93 regression discovered during that validation: the existing turnend-guard Stop hook's bare root variable broke grok's own variable pre-substitution and silently no-op'd the hook. * no-mistakes(review): Harden watcher arm validation * no-mistakes(review): Harden arm guard metacharacter checks * no-mistakes(review): Harden nested shell arm guard * fix(lint): rewrite SC2015 guards in fm-arm-pretool-check.sh as if/then A && B || C is not if-then-else; C can run when A is true. Replace both occurrences of the quote-state early-continue with an explicit if/then.
* fix Pi primary supervision lifecycle * no-mistakes(document): Synchronize Pi primary extension documentation
…guid#1737) * fix(bin): bound OPEN DECISIONS scan cost with a per-status-file cursor The fleet-wide OPEN DECISIONS scan added in kunchenguid#1711 re-reads and refolds every task's entire lifetime status log on every drain, so its cost grows unbounded with total log size. Add status_open_decisions_incremental and scan_open_decisions_incremental to fm-classify-lib.sh: they persist a per-status-file byte cursor plus the folded open-decision set, and fold only newly appended bytes on each call, reusing status_open_decisions' exact fold-line rule (extracted into _fm_decision_fold_line) so the two strategies can never disagree on what is open. A missing or invalidated cursor (new task, truncated/rewritten/shrunk log) falls back to a full re-fold. bin/fm-wake-drain.sh now calls the incremental wrapper instead of the whole-file scan. * fix(bin): add O(1) rotation detection and read-failure guarding to the cursor fold Add the two pieces the incremental open-decisions cursor was missing, scoped to this repo's actual status-file usage (create-once, append-only, never replaced or rewritten in place): - An O(1) device+inode identity check (one stat call) alongside the existing size-shrink check, so a status file replaced/rotated/recreated at the same path is detected and falls back to a full re-fold, even when the replacement is the same size. A same-inode, same-size, in-place byte edit is a deliberately accepted gap: no code path in this repo ever does that to a status file. - Checked reads: a stat/wc/tail failure is a genuine I/O error, not "the file is empty" - it now reports the already-trusted persisted open set unchanged instead of risking a silent invalidation. Both stay O(1) plus new bytes per call, matching the cursor's bounded- cost design; no content hashing or pending-fragment machinery. * no-mistakes(review): Preserve cursor state across failed incremental reads * no-mistakes(review): Refold status when cursor cache reads fail * no-mistakes(document): Document cursor-backed open-decision scanning * no-mistakes: apply CI fixes
…guid#1754) * fix(bin): preempt remote reply long-polls for queued short jobs Session start on a home with live remote second mates could stall silently for many minutes: the single serial remote job worker ran each armed fm-remote-delta-read.sh reply poll to its full 55s window while bootstrap's short sync, inherit, state, and route commands sat queued behind it, and non-FIFO queue pickup let re-armed polls keep winning the lane. Measured end to end, a trivial short job took 31s behind one 30s poll window. The worker now preempts a running preemptible job (the read-only, cursor- anchored delta read is the only member of that class) as soon as a non-preemptible job is queued, publishing exit 75 with emptied output - byte-identical to the poll's own elapsed-window-with-no-data result - so the parent runner takes its existing no-result path and the watcher re-arms from the same cursor with nothing lost. The delta read translates SIGTERM into that same exit after removing its staging directory. Sibling polls never preempt each other, so two armed monitors cannot churn. The same measured scenario now completes in 1s. * no-mistakes(document): Clarify remote poll preemption documentation
…#1778) Discord mentions already ride the same pairing-token opt-in, relay poll, and platform-aware reply path as X mentions, but the docs still read as X-only, so a stranger could not self-serve the Discord path. Add the numbered turn-on steps to the X mode configuration reference, pointing at the myfirstmate dashboard for account creation, bot install, and token issuance rather than duplicating operator setup here, and drop the X-only framing from the README bullet, the documentation index, and the architecture overview.
…#1781) * feat(bin): run session start deterministically on hook-capable harnesses Session start relied on a native nudge that only asked the agent to run bin/fm-session-start.sh, and an agent can defer that. Observed 2026-08-01: an /ahoy-first session followed the recap path and did not take the helm until a later request forced it. Claude, Codex, and Pi now RUN the digest in their session-open hook through the new bin/fm-sessionstart-run.sh, so the full ordered digest is in model context before the first turn. That wrapper is the single owner of what a session-open source means: startup and Pi's "new" take the helm, clear and compact re-emit, resume/reload/fork delegate to the nudge, and an unreadable source takes the helm because doing that redundantly is idempotent while skipping it is the bug. Grok and OpenCode keep the nudge as the floor, since neither can carry hook stdout into a model turn. Because the hook now blocks session initialization, fm-session-start.sh bounds itself first. Its steps are not all individually bounded - bootstrap's gh auth probe, tool version probes, the backlog listing and per-task endpoint reads are unbounded - so the whole digest runs as one bounded child (default 120s). Whatever it emitted before the bound survives, and the parent adds a loud STARTUP TRUNCATED banner naming the stage that stalled and every stage that never ran, still exiting 0. --reemit skips only the sweeps startup already reconciled. It still re-verifies lock ownership and still drains queued wakes, which arrived after startup and are the turn's work. fm-bootstrap.sh gains FM_BOOTSTRAP_LOCKED so a re-emit keeps repair ownership instead of deferring to a lock holder that is itself. Also adds bin/fm-timeout-lib.sh as the single owner of bounded execution, replacing three near-identical copies, and gives the ahoy skill a helm check so a nudge-tier harness cannot recap before taking the helm. Verified live on 2026-08-05 against Claude 2.1.222, Codex 0.146.0, and Pi 0.82.0; docs/verification/supervision.md records the per-harness source vocabulary, the two named gaps, and the refresh command. * no-mistakes(review): Harden session-start completion, timeout, and Pi delivery * no-mistakes(review): Harden completion ownership and portable timeout escalation * no-mistakes(review): Normalize watchdog KILL exits without masking command status * no-mistakes(review): Guarantee startup bounds and align harness delivery tiers * no-mistakes(test): Fix Pi session-start live verification fixture * no-mistakes(document): Align session-start documentation with deterministic hooks * no-mistakes(lint): Silence intentional child-shell expansion lint warning * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
* docs: rename the user-facing product name to Relay The public-mention integration gated by the `.env` pairing token is now called Relay across user-facing prose, covering X and Discord alike instead of implying a single network. Renames the product-name strings only: README, docs, the captain-facing skill descriptions, and the AGENTS.md operating prose, including the `X mode (.env)` and `Optional X mode` headings and every link anchor that pointed at them. AGENTS.md section 14 carries a one-line bridge note so the older name and the unchanged identifier spellings stay discoverable. Internal identifiers are untouched: `FMX_*`, `config/x-mode.env`, `state/x-*`, `bin/fm-x-*`, the `fmx-respond` skill path, `__FM_X_MODE_ENV__`, and `x-mode-error`. Platform references to X and Discord as networks stay as they are, and the bootstrap-diagnostics entry still quotes bootstrap's emitted `FMX: X mode on/off` line verbatim because `bin/` output is out of scope for this pass. * no-mistakes(review): Complete Relay prose rename in maintained docs * no-mistakes: apply CI fixes
* feat(harness): add a verified muse crewmate adapter Muse Code joins the fleet as a crewmate/scout adapter, verified live against Muse Code 0.1.0-R708.1 in an isolated lab. Detection matches the anchored prefix muse-bin*, because the installed launcher execs a version-suffixed binary whose name changes on every auto-update and whose install path carries no muse component to fall back on. The same identity is taught to the tmux liveness classifier, without which a healthy muse pane would have read as a dead endpoint. Busy state folds muse's own durable session event log, bound per task by a sessions-root/worktree sidecar. It is a pull source with no writer, so nothing is armed and no record is ever seeded. The fold is anchored on the full run lifecycle prefix so muse's nested cleanup "terminal" payloads cannot settle an in-flight run, and it is depth-bounded so muse's native sub-agent logs cannot be mistaken for the parent's. The idle half stays gated: an open run proves busy, but a settled log reads unknown until a credentialed multi-step run proves one turn stays inside one run. Two findings corrected the scout report. The exec-only --no-foreign-personal-context flag is rejected by the interactive TUI, so the privacy control that actually reaches a pane worker is MUSE_EXPERIMENTAL_FOREIGN_PERSONAL_CONTEXT_KILL, verified to drop the operator's foreign personal rules while keeping the project's own AGENTS.md. And an unauthenticated muse pane never exits, it waits on a device-code prompt, so credentials are a spawn preflight rather than a screen check. muse is refused for secondmates: it has no primary supervision protocol and its hook dialect rejects the reawakening handlers that protocol needs. Per the captain's decision, auto-update is not pinned, and the credentialed multi-step smoke is deferred with an explicit checklist in docs/verification/muse.md. * no-mistakes(review): Accept Muse dispatch profiles and shared efforts * no-mistakes(review): Bind Muse busy state to current session * no-mistakes(review): Compare Muse workspace bindings literally * no-mistakes(review): Harden Muse worker credentials and live signal verification * no-mistakes(review): Cache Muse session bindings and clarify worker credentials * no-mistakes(review): Clear Muse marker inheritance and normalize interrupt aliases * no-mistakes(review): Verify Muse glyph effective foreground color * no-mistakes(review): Harden Muse XDG paths, session cache, and glyph parsing * no-mistakes(document): Document Muse adapter boundaries
…d#1787) * fix(herdr): floor default-on presentation spaces at Herdr 0.8.0 Default-on presentation projection turns every crewmate teardown into a workspace-emptying removal. The focus-safe removal plan avoids Herdr's focus-stealing explicit close only while the doomed pane's shell can be proved lone, childless, and idle; a persistent child of that shell (gitstatusd, a zsh-async worker, direnv) fails that proof permanently and forces the plain close, which on every release before Herdr 0.8.0 moves the captain's active workspace for ~140ms on each teardown. Gate the unconfigured default behind a Herdr 0.8.0 floor. At or above it, project as before; below it, fall back to the flat per-home layout with one warning per home per detected release naming the version and the upgrade. An explicit "on" - including the historical empty opt-in file - is still honored below the floor, so a deliberate opt-in is never silently downgraded. The floor reads two independent signals from the client's own status, either of which can establish a supported release: the protocol number and the release core of the version string. Measured against the real release binaries, no build lacking both upstream focus fixes reaches protocol 19 and every pre-fix build tops out at 17, so protocol 19 is a safe structural expression of the floor. A release that reports neither signal readably is treated as unsupported rather than guessed at. Also: - Correct the adapter comment claiming the mitigation "stays safe without any version gate". That holds for the pane-death route only; the plain-close fallback is reachable precisely on the releases where it is unsafe. - Stop discarding the projected-close helper's stderr at teardown, so a refused or failed focus restore is visible instead of silent. The close stays non-fatal; the presence gate still decides record removal. - Add Part C to the focus-flash regression: a doomed pane whose shell holds a persistent child, in the geometry where the closing workspace's right neighbour is not the anchor. That is the fallback branch the suite could not structurally reach. On 0.7.5 it observes a bounded four-sample wrong-focus window restored exactly; on 0.8.0 it observes none. It also cross-checks its own measurement against the floor classifier, so a drifted protocol mapping fails loudly. - Make the projection suite's unconfigured-home case release-aware, so the whole real-Herdr lane passes on both the CI-pinned 0.7.4 and 0.8.0. - Add an opt-in live guard that re-measures the release-to-protocol mapping against the pinned upstream binaries. The immediate no-code mitigation for a home that cannot upgrade remains writing "off" into config/herdr-presentation-spaces. * no-mistakes(review): Pin Herdr live-guard digests across supported platforms * no-mistakes(review): Document authorized Herdr cleanup containment * no-mistakes(review): Harden Herdr warning marker publication * no-mistakes(review): Honor running Herdr server presentation floor * no-mistakes(review): Recheck Herdr floor after server ensure * no-mistakes(review): Refresh 0.7.5 and 0.8.0 focus transcripts * no-mistakes(review): Route Herdr floor probe through lab session * no-mistakes(document): Align Herdr floor documentation and comments * no-mistakes(lint): Document Herdr presentation out-parameter consumer
* fix(muse): trust the settled session log as idle The credentialed multi-step smoke on Muse Code 0.1.0-R708.1 answered the one question the idle half was held back for: one real 75-second tool-loop turn with 23 tool batches stays inside exactly one run started/terminal pair, and an Escape mid tool loop closes that run as cancelled rather than leaving the turn to continue in another run. A settled log is therefore a finished turn, not a pause between the runs of one turn. Remove fm_busy_muse_idle_verified and FM_BUSY_MUSE_IDLE_VERIFIED_VERSIONS outright rather than pinning them to a version: the session log's own metadata carries only semver 0.1.0 and a build sha, so a version allowlist could not actually match the running build and would be false precision. A settled log now classifies idle, an open run still classifies busy, and only a resolution failure - no binding, no matching log, an unreadable or run-free log - stays unknown. Record the evidence in docs/verification/muse.md, including the run-scoped grep the counts must use, and keep the post-upgrade re-check guidance. * no-mistakes(review): Document Muse idle trust and remove stale gate reference * no-mistakes(document): Clarify Muse idle verification ownership
…guid#1798) * feat(session-start): order the startup digest for truncation safety and bound its bulk The digest is delivered through a harness that truncates an oversized payload from the tail, and it really has been truncated: a 70KB digest arrived as lines 1-435 of 578, cutting off eight lines before the live-task inventory. That session took the helm without ever seeing which tasks were live or where their endpoints were. Three changes, one file's worth of composition: - FLEET STATE is emitted before CONTEXT, so a truncated tail drops curated memory - stable session to session, already governed by a captain-set budget, recoverable with one targeted read - instead of live fleet identity. The LOCK/BOOTSTRAP/WAKE-QUEUE safety preamble keeps its order. The read-once contract moves out of the closing reminder into its own section ahead of both, and now names the condition that voids it: a stage the truncation banner reports as never emitted. - Status-tail lines are capped per line, reusing the cut the wake digest's OPEN DECISIONS section already applies. An observed tail line ran 865 characters and nothing bounded it. The cut and its marker now live in one place, bin/fm-line-cap-lib.sh, so the two digests cannot drift apart; each task's full status log path is still printed beside its tail. - The backlog listing is composed as a recovery input: done rows are never listed, every in-flight, held, and blocked row is shown in full with its hold and blocked-by metadata, and only the dispatchable-now listing is bounded - with an exact remainder count and the command that shows the rest. FM_SESSION_START_QUEUED_LIMIT (default 20) replaces FM_SESSION_START_BACKLOG_LIMIT, which bounded the whole listing indiscriminately and so could drop a held or blocked row. Tests exercise the real digest output: section ordering with the preamble pinned, the per-line cap and its marker, and the backlog composition including the remainder counters on both the tasks-axi and manual paths. * no-mistakes(document): Clarify digest source recovery comments
…kunchenguid#1842) A captain decision opened by a keyed needs-decision:/blocked: status line orphaned as permanently open whenever the answer kicked off work: the worker's next event is working [key=<workstream>] in a different key namespace, so no resolved [key=<decision>] ever landed and the OPEN DECISIONS fold kept listing the answered decision forever. Remove the writer-dependency at its source: the answering firstmate already holds the decision key when it sends the answer, so fm-send's new --resolve-key flag (repeatable) appends the closing resolved line to this home's own state/<id>.status after the submit is confirmed. The close is a local ledger append for crewmates, local secondmates, and remote secondmates alike - a remote mate's escalations reach this ledger through the parent-replies ingest, so only the answer message crosses the transport. Safety: each named key must currently be open per the authoritative status_open_decisions fold or fm-send refuses before sending; a failed or unconfirmed send never closes a key; an append failure after a delivered answer exits nonzero with the manual close command so the decision re-surfaces instead of silently vanishing; a send without the flag closes nothing, and working:/done: still never clear a captain decision. Complementary fixes: the wake-drain OPEN DECISIONS section prints the answer-with-close command hint at the moment of use; brief scaffolds separate resolved's two duties (keyed-phase end vs decision closure) and state that a done:/working: line never closes a decision even when the answer started that work, keeping worker self-close for blockers that clear without a firstmate reply; AGENTS.md and docs/architecture.md carry the one-line pointers to the fm-send contract.
…1836) * feat(secondmate): seed a remote home from a supplied project origin Remote seeding required a local projects/<name> clone purely to read `git remote get-url origin` into the provisioning manifest, so setting up a remote second mate forced disposable clones and no-mistakes inits in the primary home for projects that home has no reason to hold. Firstmate now resolves the origin itself and names it as <project>=<origin-url>. The seed validates and transports what it is given, and the receiving host re-validates it rather than trusting the sender; bin/fm-project-origin-lib.sh is the single owner of which URLs are accepted, refusing executable remote-helper transports, option-shaped values, and unusable spellings at both ends. A bare <project> still reads an already-present clone's origin, so nothing that works today has to change. Registry consistency is unchanged: an unregistered or local-only project is still refused. A remote seed therefore creates nothing in the primary home beyond the route, the charter, and its launch record. The lifecycle test now seeds a registered project the primary has never cloned and asserts the primary project tree is byte-identical afterwards, alongside refusals for a missing origin, an unsafe origin, a local-only project, and an unregistered project. * no-mistakes(review): Clarify project origin documentation ownership * no-mistakes(document): Document supplied-origin remote seeding contract * feat(secondmate): accept project origins from any host or forge Firstmate is a shared template, so a project origin must be able to name any host: GitHub Enterprise on a private domain, GitLab hosted or self-hosted, Bitbucket, Gitea, Codeberg, sr.ht, a bare IP, an SSH config alias, or a plain server nobody else has heard of. The validator already decided on structure rather than on a forge allowlist, and this makes that guarantee explicit and closes the two gaps that a host-agnostic rule exposed: - a bracketed IPv6 literal in the scp-like form is now accepted, so a host reachable only by address is not excluded - a "/../" traversal inside a local or file: origin is now refused, because that names a path on the cloning host's own filesystem The library is the single owner of the accepted forms, and its header says plainly that there is no host, domain, or forge allowlist and there must never be one. The skill keeps its distinct agent-operating lines (the agent resolves and supplies the origin; a remote seed creates nothing in the primary home beyond the route, the charter, and its launch record) and points at the library for URL acceptance and at the operator doc for the rest. The lifecycle test now drives Bitbucket, a self-hosted enterprise domain, a self-hosted GitLab over ssh with a port, and a bare scp-like custom host through the real seed, manifest, transport, and remote provisioning path in one seed, asserting each URL reaches git unchanged and each clone carries its own origin's content. The unit matrix leads with non-GitHub hosts for the same reason. * no-mistakes(review): Validate project origin authorities safely * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes
…guid#1851) * fix(tests): copy the whole bin/ tree into the old-vs-new conformance shim main went red on tests/fm-backend.test.sh's "fm-send --key: old vs new exit code" assertion, which reads as an fm-send fail-closed regression from build_old_bin enumerated by hand the sibling scripts it copied into the synthetic pre-refactor tree. kunchenguid#1842 made bin/fm-send.sh source bin/fm-line-cap-lib.sh (added by kunchenguid#1798) and the list never learned about it, so the pinned old fm-send.sh aborted at `. "$SCRIPT_DIR/fm-line-cap-lib.sh"` under set -eu and exited 1 before parsing a single argument, while the current one delivered the key and exited 0. The parity check compared a crashed process against a working one and reported a behavior divergence that never happened - the more so because BASE_REF collapses to HEAD on main, where both sides run byte-identical source and a genuine divergence is impossible. fm-send's --key exit path is unchanged and its fail-closed contract is intact. Copy the tree whole instead of enumerating it. An enumerated list has to be extended by hand every time an entrypoint gains a dependency and is the only thing that knows; it has been patched a dozen times for exactly that. A whole-tree copy has nothing to forget. Extracting a refactored entrypoint the baseline does not have now fails loudly instead of writing an empty file. Only old-vs-new parity covered that exit contract, and parity is near-vacuous on main. Pin it directly: tests/fm-send-strict.test.sh drives delivery both ways from one stub and asserts an undelivered key exits nonzero naming the key, so swallowing that error fails the suite. * no-mistakes(review): Materialize historical fixture dependencies from baseline * no-mistakes(document): Clarify fm-send key regression scope
* fix(bin): mirror the whole remote secondmate status stream A remote secondmate's reply channel required corr=<16hex> on every line and failed the entire delta when one line lacked it, so the cursor could never advance past that line and the channel wedged permanently. The charter tells a secondmate to report its own progress phases and to raise new decisions with no correlation token, because correlation only answers a marked parent request. Those lines were therefore unrepresentable on the remote channel, while a local secondmate writes them straight into the parent's status file. Treat the channel as what it is: a mirror of the mate's status stream. A remote mate now presents the same status and decision model as a local one, so a newly raised needs-decision reaches the parent's open-decision fold identically, and correlation goes back to being a per-line property that settles a pending request rather than a gate on the stream. Only what crossing a machine boundary genuinely adds stays behind: cursor continuity, confined document fetch and rewrite, at-most-once append, and control-byte normalization that rewrites bytes without ever dropping a line. Line framing and size bounding already belong to fm-remote-delta-read.sh. A document the remote reader refuses is named in one escalation instead of stalling the stream, while an unavailable transport still leaves the delta for the existing retry. * refactor(bin): give the remote reply stream one append owner Every line entering the parent status stream - a mirrored line, the continuity escalation, and the undelivered-document escalation - now goes through one at-most-once append, so the idempotence a replayed generation depends on is stated once instead of copied at three call sites. * no-mistakes(review): Keep local document transfer failures retryable * no-mistakes(review): Isolate reply headers and normalize payload bytes * no-mistakes(review): Correct remote reply mirror contract wording * no-mistakes(review): Update remote reply script catalog description * no-mistakes(document): Document remote status-stream mirroring
…chenguid#1847) fm_secondmate_parent_record_parse read the .fm-secondmate-parent record with bash's read, which drops NUL bytes - and different bash generations disagree on the result: 3.2 truncates the value at the NUL while 5.x splices the surrounding bytes together. A NUL-bearing parent_home could therefore resolve to a home the record's bytes never name contiguously, and which home fm-teardown.sh's promised-public-reply resolution read (registration, registry, relay state) - or whether that protection engaged at all - depended on which interpreter ran the cleanup. Reproduced end to end: the same NUL-bearing record cleaned up under bash 5.x by resolving the spliced-together registered parent, while bash 3.2 refused it as unresolved, and a literal truncated path refused under both. Reject any NUL byte in the record before field parsing, putting corrupt records in the same fail-closed bucket as duplicate fields, malformed local bindings, unsupported routes, and symlinked records. The regression test drives the real bin/fm-teardown.sh over the proven clean-cleanup fixture with a NUL spliced mid-path into the recorded parent_home, so before the fix it reproduced the wrong-home cleanup and now it must refuse with the explicit binding refusal.
…unchenguid#1853) * docs(secondmate-provisioning): require record intake for an inherited domain A new mate seeded for an existing or inherited domain previously pulled in charter, inherited config, captain-shared preferences, project clones, and queued backlog rows with zero instruction about the domain's shipped history, so it assumed a greenfield domain. A live backlog keeps only the configured recent Done entries, so an inherited queue structurally over-represents plans and under-represents deliveries, and already-delivered work resurfaced as open. Add a record-intake step to the creation/seed path: classify greenfield versus existing or inherited, and for the latter reconcile every inherited plan against origin/main plus the live deployment, take only genuinely open work and still-live durable knowledge, never carry a plan row for shipped work, and record what could not be reconciled. Greenfield domains are untouched. The skill owns the procedure; the backlog handoff section carries a one-line reinforcement at the point where plan rows actually move. * no-mistakes(document): Clarify secondmate record-intake scope
…guid#1860) * perf(session-start): run every network check off the blocking path The session-start digest runs on a session-open hook that blocks session initialization, and every external-network call it made was individually unbounded: `gh auth status`, secondmate liveness, secondmate convergence, pending remote handoff delivery, and the fleet-sync fetch. One unreachable remote secondmate could consume the whole FM_SESSION_START_TIMEOUT and truncate the digest, so a slow network could cost the work queue itself. Measured against a host hanging 25s per SSH connection, that startup took 1m18s. The digest is now composed from local reads alone. bin/fm-startup-network.sh runs the same checks concurrently in a bounded detached worker and the digest harvests whatever finished, without ever waiting. Same fixture: 0.84s. Nothing is dropped. fm-bootstrap.sh stays the single owner of every sweep and still runs all of them, through a new FM_BOOTSTRAP_NETWORK phase split whose `skip` and `only` halves are a partition of the unsplit run. Deferral is safe because the sweeps are idempotent detectors, the result is durable and always surfaces (inline, or as a `check: startup-network` wake), and the worker re-verifies that the fleet lock still names the session that asked before it mutates anything. While the worker is still running the digest names exactly what is unconfirmed rather than implying it passed. A relaunch performed by the deferred pass is now always reported, because the digest that printed the superseded endpoint record is already out. Also collapses the duplicate tasks-axi compatibility probe: the verdict is computed once and handed to the bootstrap child for one process hop, then consumed so it never reaches a spawned agent's environment. 10 tasks-axi invocations per startup become 7. Verified on Claude Code 2.1.222 that a worker detached by the session-open hook survives the hook returning, the one vendor behavior this design needs and no portable test can see. Re-landed on current main, superseding PR kunchenguid#1845, which was cut from a pre-kunchenguid#1842 base. The digest's section numbering in AGENTS.md section 3 now states the emission order directly - supervision block and its read-once contract, fleet state, network checks, then context - which keeps kunchenguid#1826's fleet-state-before-context ordering. The old-bin test shim keeps main's git-archive baseline from kunchenguid#1851, which already subsumes this branch's reason for widening that shim. * docs(verification): re-measure the deferred startup stage on the current base Re-runs the unreachable-remote latency fixture against default-branch tip 8398d31 rather than the now-historical 345de4e, and records the sweep-result comparison the deferral's safety argument rests on: the deferred worker's published report is byte-identical to the three sweep lines the blocking baseline printed, with the unreachable route preserved in both. * no-mistakes(review): Fail deferred startup when report publication fails * no-mistakes(document): Document deferred startup network behavior accurately
* fix(procevent): apply a captured adapter result in code, not by instruction A remote secondmate's reply was captured and announced, but never applied. Nothing dispatched the reply adapter's `handle` on a `procevent remote-reply` wake, and the handling instruction named only the generic acknowledgement, so the wake was retired while everything it carried was dropped: the reply never reached the secondmate's local status mirror, the request it answered kept escalating as a missed report, and the relay - whose registration each capture retires, and which only that same handling re-arms - was left dead until the next session start armed it again. Applying such a result carries no judgement, so it belongs in code. After publishing, the runner now calls `bin/fm-procevent-<adapter>.sh autohandle <source-id> <sequence> <result-file>` and lets the adapter apply and acknowledge its own result, through the same kind of seam that already owns the terminal verdict. It runs strictly after terminal retirement, because a handling adapter re-arms its own next source and retiring afterwards would drop that fresh registration. An adapter with no such command, or one whose pass does not complete, leaves the result unacknowledged and therefore still announced, so a handler receives it exactly as before. Resolving the request was not enough on its own either. An escalation opens a durable keyed decision in the parent status log, and nothing ever closed it, so a request the remote had answered kept surfacing in every later open-decisions fold. The pending-reply library now owns both ends of that decision: it opens one under a per-request key rather than the shared default key, and closes it once the record resolves, appending the closing line only while that exact decision is still open in the fold so it can neither double-close nor clear an unrelated decision that has since taken the same key. The handling instruction still routes a wake to its adapter, now as the idempotent confirmation of what the runner already did rather than as the guarantee. Verified end to end in a throwaway isolated home driving the real armed source, blocking delta reader, runner, and wake queue, with the handler doing only the generic acknowledgement and no part of the ingest stubbed: before, seven failed observations reproducing the incident; after, none. Each half is independently load-bearing - without the runner change the reply never reaches the mirror, without the escalation close the settled request still surfaces as an open decision. * no-mistakes(review): Prevent legacy reply closure from masking decisions * no-mistakes(review): Serialize pending reply resolution and escalation closure * no-mistakes(review): Serialize pending reply escalation with resolution * no-mistakes(review): Clarify guarded legacy escalation closure behavior * no-mistakes(review): Guard legacy closure and reserve pending reply keys * no-mistakes(review): Match pending reply escalations by construction * no-mistakes(document): Document automatic remote reply resolution * no-mistakes(lint): Fix unused concurrent escalation loop variable * no-mistakes(lint): Fix unused concurrent resolution loop binding * no-mistakes(review): Version fold cache and gate autohandle on publication * no-mistakes(document): Clarify remote reply relay documentation
…kill (kunchenguid#1841) Bring the public installer-facing stow skill up to the internal skill's current curation behavior while keeping it fully standalone: - Replace the total-capture thesis with the compact-operating-map framing. - Add read-the-destination-before-writing with the inspect-then-update triad (supersedes what, one-sentence rewrite, delete stale now). - Add the concrete prune list together with its unique-fact guard, as an accuracy discipline with no size-budget machinery. - Curate every memory file the pass has open, not only the routed one. - Add the standing-decisions sweep category. - Add the stronger-owner pointer-over-copy test before filing. - Add tool-agnostic task-note discipline (inspect, classify, considered replacement body, never blind-append) and blocked-on recording. - Give .stow-notes.md a closed set of three exits. - Forbid storing, creating, or editing a skill as a stow destination. - Report per-file action verbs in the completion receipt. - Consolidate the repeated local-vs-external and .gitignore prose and fix the second-person voice slip, so the file does not grow (11334 -> 11276 bytes).
…nchenguid#1917) * fix(hooks): keep tracked Claude entries inert under grok 1.0.0 hooks Grok loads Claude-compatible settings, so the tracked `.claude/settings.json` hook entries also fire under Grok. They were meant to be inert there, guarded by `[ -z "${GROK_AGENT:-}" ] || exit 0`. That guard silently stopped working. Verified from the live process environment of a wedged grok 1.0.0 Stop hook on 2026-08-07: a grok 1.0.0 HOOK process carries GROK_HOOK_EVENT, GROK_HOOK_NAME, GROK_SESSION_ID, and GROK_WORKSPACE_ROOT, but no GROK_AGENT. The observed hook process was labelled `GROK_HOOK_NAME=project/settings:stop[0].hooks[1]`, which is the Claude-only auto-arm entry. Consequence: Grok ran `bin/fm-claude-stop-autoarm.sh` synchronously. Grok has no `asyncRewake`, so it waited on the foregrounded watcher for that entry's declared 28800-second timeout and the Grok turn never ended - the operator saw an infinite "Responding". Widen the guard to `[ -z "${GROK_AGENT:-}${GROK_HOOK_EVENT:-}" ] || exit 0` on the five entries that have a `.grok/hooks/` counterpart: both Stop entries, the SessionStart entry, and the two PreToolUse Bash entries. Two deliberate limits: - The guard is NOT widened to GROK_SESSION_ID. Grok injects it into every child process, so it can survive into a Claude session that Grok launched and would silently disable Claude's own watcher continuity. GROK_HOOK_EVENT is per-hook-invocation and does not leak that way. - `bin/fm-subagent-pretool-check.sh` stays unguarded on purpose. It is the one tracked entry with no `.grok/hooks/` counterpart, so guarding it would remove the guard from Grok entirely rather than deduplicate it. The new test asserts it stays unguarded so the exception cannot be closed silently, and docs/subagent-guard.md is honest that the coverage it leaves is partial. `bin/fm-harness.sh` corrects a comment that presented GROK_AGENT as reliably present; it is a fast path only, and the ancestry walk is what actually guarantees grok identification. tests/fm-turnend-guard.test.sh adds test_tracked_claude_entries_inert_under_grok, which runs every tracked entry under a real grok 1.0.0 hook environment, a legacy GROK_AGENT environment, and a native Claude environment. * no-mistakes(document): docs: sync grok hook-marker guard facts to owners * no-mistakes(review): docs: state grok guard criterion by event coverage
… stage (kunchenguid#1918) The deferred network stage published one aggregate started/finished pair, so a run that took a minute could not be attributed to a phase, a host, or a clone without re-running it by hand under manual tracing. Add bin/fm-timing-lib.sh as the single owner of elapsed-time records, and bracket each network owner with one: the gh auth probe, the secondmate liveness sweep, secondmate convergence, pending handoff delivery, and the project clone refresh, plus one record per secondmate for the remote-touching steps (id and host) and one per project clone. Each record carries a start offset from one shared origin, so the artifact reads as a timeline. The stage publishes them beside its report as state/.startup-network.timings, for a timed-out or failed run too, where the partial record is the answer. Only the on-demand `report` command prints them: `harvest` composes the session-start digest, so its output, the wake cadence, and every other part of a normal session start are unchanged. Recording is inert unless a run asks for it, so nothing else that sources these scripts pays for it. Details are identities only - a detail carrying whitespace is refused rather than cleaned up, which is what keeps a command line, an environment dump, or a captured error out of the file. Split two per-item loop bodies into their own functions so each iteration can be timed; every `continue` became a `return 0` with the same meaning, and the sweeps still run directly, in the same order, returning the same results.
…kunchenguid#1928) * feat(stow): cascade the internal /stow to every registered secondmate Invoked in a primary home, /stow now sweeps every registered secondmate after the primary's own required pass, enforcing the same startup-memory threshold in each home against that home's own allowance rather than a fleet total. bin/fm-stow-cascade.sh owns the mechanical inputs: it enumerates each registered secondmate exactly once from data/secondmates.md, reports that home's own budget accounting, and resolves how the sweep reaches it. A live agent sweeps its own home so its uncaptured session knowledge is captured too; a local home without one is curated in place; a remote home without one is accounted read-only and deferred, because there is no generic remote write path for a home's own memory files. Every host- crossing step and each home's accounting runs under one hard bound, so a slow or unreachable home reports an exception and the sweep continues. Nothing changes until /stow is invoked: no new notification, digest section, or background work. The public skills/stow skill is untouched. * no-mistakes(review): fix(stow): extend cascade --help range to include full exit-code contract
…nguid#1927) 29 fm-remote-job-worker.sh processes were found running at ppid 1, 1-2 days old, each still polling and appending to a log inside a no-mistakes gate worktree that had already been returned. Three things combined to make that possible: - The recorded worker.pid is the serving child, not the restart supervisor above it, so a teardown that stops that one pid only makes the supervisor respawn. The Linux start path also left the worker tree in the launching command's process group, so there was no group to signal instead. - Neither the serving loop nor the supervisor ever rechecked whether its configured FM_ROOT still existed, so a worker launched from a worktree outlived that worktree indefinitely. - The supervisor restarted a failing child with a fixed 0.1s delay and no bound, which is what grew the logs (~66MB/day measured). The Linux start path now puts the worker tree in its own process group, and fm_remote_job_stop_worker_tree signals that whole group - refusing any group whose leader is not itself a worker, so a worker from an older build or from launchd's own session is still stopped safely as a single process. The worker stops itself once its code root stops being a Firstmate checkout, confirmed across a grace window so an ordinary transient cannot stop a healthy worker. The supervisor backs off and gives up rather than restarting forever. bin/fm-remote-job-reap-orphans.sh is the belt-and-suspenders sweep for workers already orphaned that way, wired into fm-teardown.sh. Its reap condition is exactly "the code root named in the worker's own command line is gone", which is why the account's healthy LaunchAgent worker and every live remote secondmate worker are never candidates. The two suites that leaked these in the first place now stop the worker tree rather than the recorded pid alone.
…henguid#1925) * fix(bin): lint only the changed shard locally, full lint in CI Two ships hitting fm-lint.sh at once could spike CPU to 190% and load to 8.58 on a captain's Mac, even though each run finishes quickly. fm-lint.sh now defaults to linting only the canonical-set files changed since the merge-base with origin/main (including uncommitted edits) on an ordinary local branch, using plain local git with no network calls. It still lints the full canonical set in CI (GITHUB_ACTIONS=true or CI=true), on the main branch, or whenever no merge-base can be found, so CI coverage never depends on a local diff. Explicit paths keep bypassing this selection entirely. * no-mistakes: apply CI fixes
* feat(bin): add deterministic agent lifecycle control Separate firstmate's data plane from its control plane. bin/fm-send.sh is the data plane: conversational text, always routing-marked for a kind=secondmate target. That marking is right for a message and wrong for a lifecycle command - a marked "/quit" arrives as ordinary chat the agent reasons about instead of executing. bin/fm-control.sh is the control plane: allowlisted interrupt, exit, and transactional relaunch verbs addressed to an exact task id, with per-harness mechanics owned by the executable bin/fm-control-lib.sh rather than improvised in agent prose, and a verified postcondition for every action. There is no arbitrary-text and no raw-key entry point. relaunch runs as a transaction with a durable journal: it resolves the profile, proves the work it must preserve is recoverable, records the required progress note, stops the old agent, then delegates the launch to its single owner, bin/fm-spawn.sh --relaunch, which adopts the recorded endpoint and worktree instead of creating either. A refusal before the stop leaves the record and instructions byte-identical; a failure after it reports the concrete state rather than claiming an agent that is not running. Teardown and discard stay separate and explicit. exit and relaunch require a backend with a recovery-grade agent-state classifier, so zellij, orca, and cmux are refused rather than reported as successful blind. A remotely placed secondmate is refused by name, because its agent runs on a host where none of these postconditions can be read. * fix(control): resolve a recorded harness to its adapter before retiring wiring fm-spawn arms per-task harness wiring on prefixes, because a task launched from a raw command records that command's basename rather than the exact adapter name. The control plane's retirement tables are keyed by the exact adapter, so a task recorded as `grok-2` had its turn-end token, private registry entry, and worktree hook pointer armed and never retired - leaving a registry entry that outlived the agent that owned it. State the prefix rule once, in the capability owner, and resolve the recorded value through it before every table lookup. bin/fm-send.sh's composer-clear lookup reads the same owner instead of keeping its own copy of which adapters need one. * test(control): pin muse session-binding retirement across a harness switch * no-mistakes(review): Resolve prefixed harnesses across lifecycle control verbs * no-mistakes(review): Report interrupt delivery without fabricating cancellation state * no-mistakes(review): Clear disabled relaunch trace context atomically * no-mistakes(review): Clarify control interrupts and restore legacy send state * no-mistakes(review): Refuse ambiguous relaunches and report exit delivery * no-mistakes(review): Revalidate interrupts and accept interrupt-stopped exits * no-mistakes(review): Lock descendant tasks before forced recursive teardown * no-mistakes(document): Align lifecycle adapter documentation with control plane * no-mistakes: apply CI fixes * fix(bin): serialize fresh task publication with forced teardown Forced secondmate teardown enumerated a home's task set, locked what it found, then re-enumerated while removing. A fresh spawn takes only its own per-task lock, so a record published inside that window was invisible to the preflight and visible to the cleanup: it was destructively processed while never lifecycle-locked. Reproduced with real agents. A record published 0.249s after teardown began was removed, its window closed, and its worktree returned to the pool - while both commands reported success. A per-task lock cannot protect a task that does not exist yet. Add a per-home task-set lock guarding WHICH tasks a home has, as opposed to the metadata lock guarding one task's record. Teardown takes it per home, parent before child, before enumerating and holds it through cleanup. A fresh spawn takes it before its own per-task locks and holds it through publication; a relaunch is exempt, because it republishes an existing task already covered by that task's control lock. Either the spawn publishes first and the teardown's preflight covers it, or the teardown owns the set and the spawn refuses. Both directions fail closed, and both are pinned by tests that hold the lock rather than racing on timing. * no-mistakes(review): Serialize remote secondmate publication with forced teardown * no-mistakes(review): Preserve remote spawn routing and state initialization * no-mistakes(review): Serialize teardown when descendant state is absent * no-mistakes(review): Cover symlinked descendant state refusal * no-mistakes(document): Document task-set serialization safeguards * no-mistakes(lint): Isolate task-set lock path resolution * no-mistakes: apply CI fixes
Owner
Author
|
Superseded: re-targeted upstream where the change belongs. The alarming commit count here was a stale fork base, not real content; the branch carries one commit. |
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
Make fm-spawn launch every generated interactive pi and pi-signed supervised crew command with --tui-mode regular unconditionally, so a user-global ~/.pi/agent/settings.json value of tuiMode=fullscreen cannot select the experimental fullscreen renderer, which rewrites or clears terminal scrollback while streaming and can bury injected fm-send steers from programmatic pane-tail delivery checks (upstream earendil-works/pi#7304; observed as three apparent delivery failures on 2026-08-09). Pi v0.84.0 packages/coding-agent/docs/settings.md, UI & Display, documents regular as the tuiMode default, fullscreen as experimental, and --tui-mode as the supported startup override for the setting. Every fleet Pi supports the flag, and a hypothetical old Pi failing loudly at spawn is acceptable and visible. Do not add executable resolution, --help capability probes, probe timeouts, version fallback, or any other compatibility machinery. Keep the launcher change minimal. Keep focused fm-spawn coverage that asserts --tui-mode regular is present for Pi-family generated launches and absent for non-Pi harnesses. Update the harness-adapters skill Pi section with one line that cites the settings documentation and records both the fullscreen scrollback-burial fact and unconditional launcher default.
What Changed
Risk Assessment
✅ Low: The change is minimal, scoped to generated Pi-family launch templates, and has focused coverage for Pi, pi-signed, secondmate, and non-Pi behavior.
Testing
Completed 1 recorded test check.
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
.agents/skills/afk/SKILL.md- branch carries 246 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (370 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
✅ **Review** - passed
✅ No issues found.
command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.