Skip to content

feat(bin): add agent control plane, Cursor harness, and symlink-safe CLAUDE.md syncing - #1

Merged
mfernrdx merged 41 commits into
mainfrom
fm/fm-agentsmd-symlink
Aug 14, 2026
Merged

mfernrdx merged 41 commits into
mainfrom
fm/fm-agentsmd-symlink

Conversation

@mfernrdx

@mfernrdx mfernrdx commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

Intent

Follow-up to the sync-marker fix already validated on this branch (PR #1): apply firstmate's decision to correct two review-flagged message-accuracy issues in bin/fm-ensure-agents-md.sh's CLAUDE.md sync-marker mechanism, with no behavior change and no new refusal path, guarantee, or subsystem. (1) The legacy-duplicate upgrade path (a byte-identical real CLAUDE.md predating the sync marker) reported 'unchanged' even though sync_claude() had just rewritten CLAUDE.md's bytes to add the marker; it now reports 'updated: added the sync marker...' when that write actually changes CLAUDE.md, and 'unchanged:' only when nothing changed. (2) The promotion path (an existing real CLAUDE.md gets copied into AGENTS.md and marked) permanently opts that file into auto-resync without saying so, so a captain who kept hand-editing CLAUDE.md directly after promotion would have those edits silently discarded on the next run with no warning; the 'promoted:' line now states plainly that CLAUDE.md is a managed mirror of AGENTS.md from then on and that future edits should target AGENTS.md instead. Colocated tests updated to assert both new message strings and that a further re-run after the marker upgrade correctly reports unchanged once nothing is left to change. All 21 tests in tests/fm-ensure-agents-md.test.sh pass, shellcheck via bin/fm-lint.sh and bin/fm-doc-audience-check.sh are both clean.

What Changed

  • Adds new crew-control subsystems under bin/: fm-control.sh + fm-control-lib.sh (allowlisted lifecycle verbs addressed to an exact task id, documented in docs/agent-control.md), fm-cursor-lib.sh (Cursor executable resolution and process identity, wired into spawn/harness/busy/tmux), fm-procevent-when.sh (deterministic condition→action watcher over the process-event runner), fm-inactive-reconcile.sh (bounded reconciliation of suspicious inactive terminal outcomes), fm-stow-cascade.sh (enumerates registered secondmates for an internal /stow cascade), fm-remote-job-reap-orphans.sh (reaps workers whose code root is gone), and fm-timing-lib.sh (deferred network-stage elapsed-time instrumentation).
  • Reworks bin/fm-ensure-agents-md.sh so CLAUDE.md is never turned into a symlink when that is unsafe: claude_symlink_unsafe() detects core.symlinks=false and WSL DrvFs mounts via /proc/mounts (overridable through FM_PROC_ROOT_OVERRIDE), promotion copies AGENTS.md instead of linking, and a real duplicate carries a CRLF-aware trailing sync marker so later runs resync it rather than hard-refusing a mismatch they caused. Status output was corrected to report updated: added the sync marker... when the marker upgrade actually rewrites CLAUDE.md, unchanged: only when nothing changed, and to state on promoted: that CLAUDE.md is a managed mirror and future edits belong in AGENTS.md.
  • Broad supporting changes across backends (cmux, herdr, orca, tmux, zellij), composer classification, wake/watch queues, spawn, bootstrap, teardown, and lint; adds VISION.md, a Windows Herdr CI spike workflow, and ~20 new colocated test files with substantial expansion of the existing suites.

Risk Assessment

✅ Low: The change is a small, well-bounded shell helper that is the exact inverse of the existing marker writer, reached only on a narrow promotion path where AGENTS.md is absent, and it is covered by a colocated behavior test that genuinely fails against the pre-fix code; both intent criteria are implemented and independently asserted.

Testing

I ran the smallest relevant automated set — the colocated tests/fm-ensure-agents-md.test.sh (21 tests, all pass) — then reproduced both intent scenarios manually as an end user would, running the real script in throwaway git repos and capturing the CLI transcripts side by side against the pre-fix script. The legacy byte-identical duplicate now reports updated: added the sync marker to CLAUDE.md in <dir> where the old script wrongly said unchanged: (md5sum in the transcript proves CLAUDE.md's bytes really changed), and an immediate re-run correctly reports unchanged:. The promotion path now prints the managed-mirror wording pointing future edits at AGENTS.md. A recursive diff of the resulting file trees old-script vs new-script is identical on both paths, backing the intent's no-behavior-change constraint. Per the phase rules I skipped lint/shellcheck and the full suite; the worktree is clean and all evidence lives in the evidence directory.

Evidence: CLI transcript: legacy sync-marker upgrade, before vs after fix (with md5sum proving the byte change)

--- BEFORE fix (commit 10d3a07): claims 'unchanged' although CLAUDE.md was rewritten --- $ md5sum r/CLAUDE.md # before 0c2e7e891c03173380e2f6e638178aed r/CLAUDE.md $ fm-ensure-agents-md.sh ./r unchanged: AGENTS.md and CLAUDE.md are real, synced files in .../r $ md5sum r/CLAUDE.md # after -- bytes actually changed f4ab1c1f7163b3ef3fb214b732319b79 r/CLAUDE.md --- AFTER fix (HEAD 86f8f8b) --- $ md5sum r/CLAUDE.md # before 0c2e7e891c03173380e2f6e638178aed r/CLAUDE.md $ fm-ensure-agents-md.sh ./r updated: added the sync marker to CLAUDE.md in .../r $ md5sum r/CLAUDE.md # after -- bytes actually changed f4ab1c1f7163b3ef3fb214b732319b79 r/CLAUDE.md $ tail -1 r/CLAUDE.md <!-- fm-ensure-agents-md: this CLAUDE.md is a synced duplicate of AGENTS.md, written because symlinks are not reliable here; edit AGENTS.md instead, then re-run fm-ensure-agents-md.sh --> $ fm-ensure-agents-md.sh ./r # re-run, nothing left to change unchanged: AGENTS.md and CLAUDE.md are real, synced files in .../r

=== Scenario 1 (corrected): legacy byte-identical, already-sectioned duplicate gains the sync marker ===

--- BEFORE fix (commit 10d3a07): claims 'unchanged' although CLAUDE.md was rewritten ---
$ md5sum r/CLAUDE.md   # before
0c2e7e891c03173380e2f6e638178aed  r/CLAUDE.md
$ fm-ensure-agents-md.sh ./r
unchanged: AGENTS.md and CLAUDE.md are real, synced files in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/r
$ md5sum r/CLAUDE.md   # after -- bytes actually changed
f4ab1c1f7163b3ef3fb214b732319b79  r/CLAUDE.md
$ tail -1 r/CLAUDE.md
<!-- fm-ensure-agents-md: this CLAUDE.md is a synced duplicate of AGENTS.md, written because symlinks are not reliable here; edit AGENTS.md instead, then re-run fm-ensure-agents-md.sh -->
$ fm-ensure-agents-md.sh ./r   # re-run, nothing left to change
unchanged: AGENTS.md and CLAUDE.md are real, synced files in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/r

--- AFTER fix (HEAD 86f8f8b): reports the marker write, then 'unchanged' on re-run ---
$ md5sum r/CLAUDE.md   # before
0c2e7e891c03173380e2f6e638178aed  r/CLAUDE.md
$ fm-ensure-agents-md.sh ./r
updated: added the sync marker to CLAUDE.md in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/r
$ md5sum r/CLAUDE.md   # after -- bytes actually changed
f4ab1c1f7163b3ef3fb214b732319b79  r/CLAUDE.md
$ tail -1 r/CLAUDE.md
<!-- fm-ensure-agents-md: this CLAUDE.md is a synced duplicate of AGENTS.md, written because symlinks are not reliable here; edit AGENTS.md instead, then re-run fm-ensure-agents-md.sh -->
$ fm-ensure-agents-md.sh ./r   # re-run, nothing left to change
unchanged: AGENTS.md and CLAUDE.md are real, synced files in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/r
Evidence: CLI transcript: promotion status line, before vs after fix

--- BEFORE fix (commit 10d3a07) --- $ fm-ensure-agents-md.sh ./p promoted: copied CLAUDE.md content into AGENTS.md and kept CLAUDE.md as a real file in .../p --- AFTER fix (HEAD 86f8f8b) --- $ fm-ensure-agents-md.sh ./p promoted: copied CLAUDE.md content into AGENTS.md; CLAUDE.md is now a managed mirror of AGENTS.md in .../p - edit AGENTS.md from now on, not CLAUDE.md

=== Scenario 2: promoting an existing real CLAUDE.md ===

--- BEFORE fix (commit 10d3a07) ---
$ fm-ensure-agents-md.sh ./p
promoted: copied CLAUDE.md content into AGENTS.md and kept CLAUDE.md as a real file in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/p

--- AFTER fix (HEAD 86f8f8b) ---
$ fm-ensure-agents-md.sh ./p
promoted: copied CLAUDE.md content into AGENTS.md; CLAUDE.md is now a managed mirror of AGENTS.md in /tmp/no-mistakes-evidence/01KZZ65YMCJ3V3DJZ6N1SRVEP9/demo/p - edit AGENTS.md from now on, not CLAUDE.md
Evidence: Byte-level parity check: resulting files identical before vs after the change (no behavior change)

Promotion path, resulting files old-script vs new-script: IDENTICAL -> promotion behavior unchanged (message-only) Legacy marker-upgrade path, resulting files old-script vs new-script: IDENTICAL -> upgrade behavior unchanged (message-only)

Promotion path, resulting files old-script vs new-script:
  IDENTICAL -> promotion behavior unchanged (message-only)

Legacy marker-upgrade path, resulting files old-script vs new-script:
  IDENTICAL -> upgrade behavior unchanged (message-only)
Evidence: Colocated test run output (21 tests)
ok - fm-ensure-agents-md.sh: created AGENTS.md includes self-governance section
ok - fm-ensure-agents-md.sh: promoted CLAUDE.md stays a real file, never a symlink, and is idempotent
ok - fm-ensure-agents-md.sh: newline-less promotion keeps a blank separator line
ok - fm-ensure-agents-md.sh: promoting an orphaned marked mirror leaves AGENTS.md marker-free
ok - fm-ensure-agents-md.sh: existing symlinked AGENTS.md gains the section idempotently
ok - fm-ensure-agents-md.sh: existing AGENTS.md without CLAUDE.md gains section and symlink
ok - fm-ensure-agents-md.sh: AGENTS.md that already has the section stays unchanged
ok - fm-ensure-agents-md.sh: CRLF AGENTS.md with the section stays unchanged
ok - fm-ensure-agents-md.sh: CRLF injection preserves line endings idempotently
ok - fm-ensure-agents-md.sh: never symlinks CLAUDE.md on a core.symlinks=false (Windows-shaped) repo
ok - fm-ensure-agents-md.sh: symlinks-unreliable repo gets a real, synced CLAUDE.md and stays idempotent
ok - fm-ensure-agents-md.sh: adds a real, synced CLAUDE.md when only AGENTS.md exists and symlinks are unreliable
ok - fm-ensure-agents-md.sh: never symlinks CLAUDE.md for a fresh repo on a simulated DrvFs (Windows) mount
ok - fm-ensure-agents-md.sh: a non-DrvFs /mnt mount still gets a real CLAUDE.md symlink
ok - fm-ensure-agents-md.sh: refuses two real files with different content
ok - fm-ensure-agents-md.sh: a marked duplicate resyncs instead of conflicting after an AGENTS.md-only edit
ok - fm-ensure-agents-md.sh: a CRLF marked duplicate stays synced and resyncs after a CRLF AGENTS.md edit
ok - fm-ensure-agents-md.sh: a marked duplicate survives a CRLF checkout normalization
ok - fm-ensure-agents-md.sh: a newline-less AGENTS.md still gets the marker on its own line and resyncs
ok - fm-ensure-agents-md.sh: an unmarked real CLAUDE.md still refuses on mismatch rather than resyncing
ok - fm-ensure-agents-md.sh: a legacy byte-identical unmarked duplicate is upgraded with the sync marker and reports the change accurately
ok - fm-ensure-agents-md.sh: refuses a case-variant lowercase agents.md (issue #389)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-ensure-agents-md.sh:432 - The promotion path (cp &#34;$CLAUDE&#34; &#34;$AGENTS&#34; at line 432) does not strip an existing sync-marker line, and emit_synced_claude_content() (line 260) appends a marker unconditionally, so promoting an already-managed mirror bakes the machine marker into the real memory file. Reachable path: a symlink-unsafe repo already has a marked mirror (AGENTS.md + marked CLAUDE.md); AGENTS.md is then removed out of band (stray deletion, partial checkout, revert of the commit that added it) while the marked CLAUDE.md remains. The next run falls through to line 426, copies CLAUDE.md - marker included - into AGENTS.md, then sync_claude() writes CLAUDE.md as content+marker+marker. The state is then stable (claude_matches_agents_synced succeeds, so later runs report unchanged:), permanently leaving AGENTS.md - the file loaded into every agent session - ending with '<!-- fm-ensure-agents-md: this CLAUDE.md is a synced duplicate of AGENTS.md ... edit AGENTS.md instead ... -->', and CLAUDE.md carrying it twice. Fix: drop a trailing marker line during promotion, or have emit_synced_claude_content skip re-appending when content already ends with the marker. Raised rather than fixed because this run's intent is explicitly message-only with no behavior change.

🔧 Fix: strip sync marker when promoting CLAUDE.md into AGENTS.md
1 info still open:

  • ℹ️ tests/fm-ensure-agents-md.test.sh:124 - The new test uses a hand-rolled negative assertion grep -Fq &#34;fm-ensure-agents-md&#34; &#34;$agents&#34; &amp;&amp; fail &#34;...&#34; where tests/lib.sh:294 already provides assert_no_grep &lt;pattern&gt; &lt;file&gt; &lt;msg&gt; for exactly this. It works today only because neither this file nor lib.sh sets -e (the AND-list's non-zero status on the passing path would otherwise abort the suite silently, with no not ok line). Swapping in assert_no_grep &#34;fm-ensure-agents-md&#34; &#34;$agents&#34; &#34;promotion copied the sync marker into AGENTS.md&#34; matches the surrounding style and removes that latent coupling to the absence of set -e.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-ensure-agents-md.test.sh — all 21 colocated tests pass, including the updated legacy-upgrade and promotion assertions
  • Manual CLI run of bin/fm-ensure-agents-md.sh &lt;repo&gt; on a legacy byte-identical, already-sectioned real CLAUDE.md (core.symlinks=false repo): captured status line plus md5sum before/after proving CLAUDE.md's bytes were rewritten, then an immediate re-run to confirm it reports unchanged:
  • Same legacy scenario replayed against the pre-fix script (git show 10d3a07:bin/fm-ensure-agents-md.sh) to reproduce the inaccurate unchanged: report before the fix
  • Manual CLI run of bin/fm-ensure-agents-md.sh &lt;repo&gt; promoting an existing real CLAUDE.md with no AGENTS.md, before-fix and after-fix, to compare the promoted: line wording
  • diff -r --exclude=.git of the resulting repo trees produced by the pre-fix script vs HEAD for both the promotion and legacy-upgrade paths, confirming byte-identical outcomes
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

jayjongcheolpark and others added 30 commits August 7, 2026 14:52
…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
* feat(stow): tiered decaying memory with captain-gated offload to local excluded skills

Implement the captain-adopted /stow redesign from the v2 tiering report as
amended by the adoption decision:

- Per-entry trailing HTML-comment markers with three tiers named for their
  handling: pinned (no clock, no eviction), aging (stale after 30 days),
  perishable (stale after 7 days, mandatory checkable expiry condition).
- File-scoped defaults (captain.md and captain-shared.md pinned,
  learnings.md aging) with a self-describing legend line per file header.
- Reinforcement requires session evidence; re-reading memory never counts.
- Archive-not-delete: stale and budget-evicted entries move with provenance
  to the never-injected data/memory-archive.md; prune always means the cold
  tier, and a stale unique fact is never deleted.
- Captain-gated over-budget offload: staleness evaluated before scope, the
  sweep runs only when still over budget after decay and consolidation,
  proposals go through the receipt plus one durable captain-held backlog
  item, migration runs through the destination's normal path, and the
  memory entry leaves only once the destination is live.
- Offload destination per the adoption decision: a user-owned skill under
  .agents/skills/<freeform-name>/ excluded via the local .git/info/exclude,
  with the hard rule that stow never creates or writes a tracked skill.
- Five graduation moves, receipt verbs archived and proposed-offload, and
  the one-time non-destructive migration of unmarked legacy entries.

The public skills/stow/SKILL.md mirrors the generic parts (markers, decay,
archive exit, user-approved on-demand offload exit, migration) with no
firstmate-specific paths.

The load-bearing assumption that a git-excluded skill is still discovered
was verified empirically against Claude Code 2.1.226 (direct
.git/info/exclude scratch-repo test plus an in-repo ignored-probe test);
the dated evidence is recorded in docs/verification/stow-memory.md.

The graduation list's deletion move is deliberately narrowed to duplicates
already preserved by a stronger owner, reconciling the v2 report's retained
'deletion of a stale entry' wording with its own prune-always-archives
rule.

* no-mistakes(review): Persist legacy migration grace across stow passes

* no-mistakes(review): Enforce archival invariants and exempt default-pinned legacy entries

* no-mistakes(review): Clarify offload scope, archive placement, and marker boundaries

* no-mistakes(review): Enforce aging fallback and verify excluded skill loading

* no-mistakes(review): Fix stow decay, pinned offload, and archival safeguards

* no-mistakes(review): Preserve pinned entries, approvals, and archive provenance

* no-mistakes(review): Restrict stow mutations to editable memory files

* no-mistakes(review): Clarify skill destinations, collision checks, and migration legends

* no-mistakes(review): Resolve exclude paths for linked worktrees

* no-mistakes(review): Secure per-home excluded skill migration

* no-mistakes(test): Require explicit tier markers on new stow entries

* no-mistakes(test): Route missing shared legends to primary owner

* no-mistakes(document): Align stow documentation with tiered memory

* fix(stow): converge the pass on an over-budget home (dogfood D1-D3)

The dogfood run against a copy of the real over-budget home showed the
pass increasing the deficit from 624 to 1,107 estimated tokens and the
relief ladder provably unable to reach budget. Three skill-text fixes:

- D1: markers become single-token spellings (<!--a:DATE-->, <!--p:DATE-->,
  <!--P-->, <!--g-->), entries matching a pinned file default carry no
  marker, the per-file policy legend collapses to a one-line pointer
  naming the stow skill as the scheme owner, and marker/pointer bytes are
  explicitly counted content - roughly 76% less metadata cost on the
  dogfooded home's first installment.
- D2: the eviction rung gains a convergence precondition - total the
  eligible pool first, and when archiving all of it cannot reach budget,
  skip eviction entirely, archive nothing for budget reasons, and report
  the exempt pinned floor as the concrete inability in the final step.
- D3: budget eviction considers only dated aging entries; <!--g-->
  legacy-grace entries are ineligible until their grace cycle resolves,
  so eviction cannot cancel promised grace or invert against validation.

Public skill mirrors the D1 marker/pointer changes; D2/D3 are internal
because the public skill has no budget ladder.

* no-mistakes(test): Enforce evidence-only reinforcement during stow migration

* no-mistakes(document): Clarify stow receipt marker actions
* docs: add firstmate vision

* no-mistakes(test): Classify VISION.md as public product documentation

* no-mistakes(document): Restore approved one-file vision diff

* no-mistakes: apply CI fixes
* fix(spawn): force regular Pi TUI for crews

* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composer

* no-mistakes(review): Normalize cmux NBSP prompts across locales

* no-mistakes(document): Document cmux borderless Claude composer classification
…nchenguid#2091)

The public installer-facing stow skill scoped its classify-then-replace
discipline to TODO/BACKLOG items only, so findings routed to a memory file
had no stated rule against a blind append or a wholesale overwrite.

Step 6 now classifies every finding against the destination's current
contents as new, duplicate, superseding, or obsolete, and states the
considered replacement each classification implies. The outcomes follow the
tiered-memory contract already in the file: an obsolete entry is refreshed,
archived, or replaced in a way that preserves its fact, a duplicate folds
into the entry that already carries it, and a superseded body worth keeping
leaves through step 7's existing exits rather than a second recovery
mechanism.
* fix(watcher): resurface durable work after downtime

* no-mistakes(review): Make watcher rearm recovery durable and cursor-safe

* no-mistakes(review): Persist safe recovery markers across migration lock recovery

* no-mistakes(review): Retain stale lock when recovery marker publication fails

* no-mistakes(review): Preserve delivery-gap recovery and quarantine malformed markers

* no-mistakes(review): Serialize recovery consumption and report acknowledgment failures

* no-mistakes(review): Centralize recovery publication before clearing watcher evidence

* no-mistakes(review): Guarantee recovery evidence across queue and lock handoffs

* no-mistakes(review): Publish recovery evidence before durable wake commits

* no-mistakes(review): Replace recovery marker Perl dependency with Node

* no-mistakes(review): Keep interrupted wakes durable until handling acknowledgment

* no-mistakes(review): Add post-handling durable wake acknowledgements

* no-mistakes(review): Enforce post-handling acknowledgement across recovery and AFK return

* no-mistakes(review): Bind wake acknowledgements to recovery generations

* no-mistakes(review): Align wake regressions with generation-bound acknowledgements

* no-mistakes(document): Document durable re-arm recovery semantics

* no-mistakes(lint): Resolve ShellCheck warnings in recovery and watcher tests

* no-mistakes: apply CI fixes

* test(watcher): assert post-handling wake replay

* no-mistakes(review): Prevent successor loops and adopt legacy wake generations

* no-mistakes(review): Rearm durable wakes without recursive successor recovery

* no-mistakes(review): Align recovery tests with handling marker state

* no-mistakes(review): Delay handling transition until successor launch is established

* no-mistakes(review): Confirm wake handling only after successful prompt delivery

* no-mistakes(review): Acknowledge AFK wakes only after evidence publication

* no-mistakes(review): Prevent AFK wake loss before post-handling acknowledgement

* no-mistakes(document): Document durable wake acknowledgement semantics

* no-mistakes(lint): Suppress false positive for recovery action output

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
* ci: add Windows Herdr automation spike

* ci: run Windows spike on its pull request

* fix: wait for Windows Herdr command output

* fix: run ANSI probe in pane shell

* ci: keep Windows Herdr spike manually triggered

* docs: clarify Windows Herdr spike verdict
* Add guided ahoy decision flow

* no-mistakes(document): Document guided Ahoy decision flow
* Harden stow memory budget policy

* Refine internal stow offload policy

* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
…enguid#2116)

* fix(spawn): refresh pooled worktree base

* no-mistakes(document): Document spawn base-freshness invariant

* no-mistakes: apply CI fixes
…#2102)

* refactor(composer): one shape owner behind thin capture adapters, whole matrix fixed

Consolidate every composer shape - bordered boxes (all families, geometry,
titled bottom borders), bare agent-glyph rows and their wrap regions,
opencode's left bar, and pi's identity-gated separator pair - into
fm_composer_classify_screen in bin/fm-composer-lib.sh. Adapters now
contribute only a capture and a declarative capability descriptor
(styled/cursor/identity/rows); capability differences change how confidently
a shape is judged, never what the shapes are, so a new harness shape is
teachable in exactly one place.

Correctness fixes landed as part of the consolidation (audit
data/fm-composer-consolidation-audit-s1):
- locale-safe Unicode-space normalization in the shared owner (closes the
  fleet-wide half of kunchenguid#1988; cmux's local byte-exact NBSP case deleted;
  naming converges with PR kunchenguid#1995's normalization primitive)
- muse's bare glyph joins the shared set, unbreaking muse on herdr/cmux/orca
- orca learns the borderless bare shape, drops its backward-paged composer
  window, and can no longer classify a stale startup banner as the composer
- tmux tolerates a titled bottom border, unbreaking grok steering
- the left-bar shape makes opencode readable on every backend
- zellij gets a real classifier through dump-screen --ansi, replacing the
  content-diff submit heuristic that could confirm an undelivered message
  and close a --resolve-key decision (the fleet's only false positive)
- fm-spawn's kimi launch-readiness regex (the fourth shape copy) now routes
  through the shared classifier

The strict blank-row posture applies fleet-wide (captain decision
blank-row-injection-posture): no positive container proof = unknown = defer,
replacing tmux's permissive blank-cursor-row rule. Away-mode injection was
re-validated end to end on real tmux (defer on partial input and unproven
rows, clean delivery with swallowed-Enter retry into proven-empty
composers). The tmux submit core gains a baseline-idle turn-started
conversion so pi steering stays confirmed while its working screen hides
the composer; busy conversion without that baseline remains forbidden.

Plain-capture backends now degrade a glyph row carrying trailing text to
unknown instead of a false pending, per the approved capability rule.

Portable regressions pin the full byte-capture matrix from the audit under
a UTF-8 locale and LC_ALL=C, the strict-vs-permissive divergence, and
deliberate signal separation; the opt-in live guard
(tests/fm-composer-matrix-live-e2e.test.sh) verified every installed
harness against the real classifier, recorded in
docs/verification/runtime-backends.md.

* no-mistakes(review): Fix Pi glyph ambiguity and complete profile matrix

* no-mistakes(review): Preserve bare verdict when Pi identity probe is absent

* no-mistakes(review): Harden composer structure and titled-border geometry

* no-mistakes(review): Require proven idle baseline and strict Zellij guard

* no-mistakes(review): Reject box bottom borders as composer input rows

* no-mistakes(review): Prove Zellij probe typing before classifier retries

* no-mistakes(review): Preserve Pi identity uncertainty and scan full left-bar drafts

* no-mistakes(review): Verify Zellij text lands before submitting

* no-mistakes(review): Scope Zellij typing verification to selected composer content

* no-mistakes(review): Verify Zellij pastes through composer-scoped content deltas

* no-mistakes(review): Prove wrapped bare Zellij pastes through composer extraction

* no-mistakes(review): Invalidate stale cursorless composers below dead shell prompts

* no-mistakes(review): Handle shell prompt placeholders in composer extraction

* no-mistakes(review): Classify cursorless bare continuation regions safely

* no-mistakes(review): Reject stale cursorless containers below live activity

* no-mistakes(review): Preserve prompt glyphs in wrapped Zellij pastes

* no-mistakes(review): Reject live shell rows during composer extraction

* no-mistakes(review): Preserve wrapped glyph continuations through submit retries

* no-mistakes(review): Scope idle placeholders to proven positions

* no-mistakes(review): Restore boxed placeholders and live prompt reanchoring

* no-mistakes(review): Fix Zellij placeholder and wrapped glyph paste proof

* no-mistakes(document): Align composer architecture documentation

* no-mistakes(lint): Fix ShellCheck warnings in composer refactor

* no-mistakes: apply CI fixes

* docs(verification): record the trusted-checkout live matrix rerun

The pipeline's isolated gate worktree is untrusted, so claude, grok, and
muse stopped at first-launch trust dialogs there (the guard refuses to
confirm them by design). This rerun from the trusted checkout at the final
validated head verified all six installed harnesses, the strict blank-row
deferral, and the hardened zellij false-positive probe live.

* no-mistakes(document): Align composer verification evidence

* no-mistakes: apply CI fixes

* no-mistakes(review): Restore proven box bottom-cursor classification

* no-mistakes(review): Preserve styled placeholder-like drafts as pending

* no-mistakes(document): Align composer safety and Zellij delivery documentation

* no-mistakes: apply CI fixes

* docs(verification): refresh the live matrix with the final-head trusted rerun

The post-validation rerun from the trusted checkout verified all six
installed harnesses at the branch's final head, including Claude 2.1.227
(auto-updated since the audit's captures) and Grok, which the untrusted
gate worktree could not verify past their first-launch trust dialogs.
* fix(spawn): gate Pi regular TUI flag by capability

* no-mistakes(review): Document conditional Pi TUI capability detection

* no-mistakes(review): Pin Pi probing and launch to one executable

* no-mistakes(review): Preserve literal pinned Pi paths and update documentation

* no-mistakes(review): Defer pinned Pi path insertion until final substitution

* no-mistakes(document): Document version-safe Pi launch probing

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
…unchenguid#2147)

* docs(vision): elevate experience, pain narrative, and distro virtues

Fold the captain's public vision framing into VISION.md: peace of mind as a
primary goal, multi-session context-switch pain as the problem one interface
solves, clone-and-run setup ease, self-evolution including community, and
explicit harness/backend orthogonality. Reconcile experience-as-garnish into
experience-as-purpose and update aligns/resists accordingly.

* docs(vision): state the experience goal positively

Drop the negative "not a smart workflow / useful tool / impressive technology"
pretext. Lead straight into the positive experience north star.
* fix: reconcile inactive terminal outcomes

* fix: stream secondmate summary inputs

* no-mistakes(review): Fix reconciliation locking and request delivery retries

* no-mistakes(review): Prevent retries after unknown request delivery

* no-mistakes(document): Clarify inactive reconciliation cadence and receipts

* no-mistakes(lint): Quote terminal status arguments in reconciliation tests

* refactor: simplify inactive outcome reconciliation

* no-mistakes(review): Bound inactive reconciliation scans with durable progress

* no-mistakes(review): Bound reconciliation and deduplicate recovery notices

* no-mistakes(document): Document inactive outcome reconciliation contracts

* no-mistakes(review): Reject relative local secondmate parent routes

* no-mistakes(review): Key terminal receipts by spawn incarnation

* no-mistakes(review): Stabilize legacy receipts and lock reconciliation snapshots

* no-mistakes(review): Fail closed on invalid secondmate identity markers

* no-mistakes(document): Document durable inactive-outcome reconciliation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
* fix(session-start): refresh drifted instructions on stale rebuilds

* test(session-start): prove Pi instruction refresh end to end

* no-mistakes(review): Fix stale instruction refresh and baseline integrity

* no-mistakes(review): Preserve true-start baselines across Pi continuations

* no-mistakes(review): Correct Pi continuation classification and live expectation

* no-mistakes(review): Correct Pi continuation coverage documentation

* no-mistakes(review): Fix read-only refresh and exact Pi session restores

* no-mistakes(review): Classify Pi create-if-missing sessions correctly

* no-mistakes(review): Classify named Pi sessions using immutable headers

* no-mistakes(review): Correct Codex interactive coverage diagnostic

* no-mistakes(document): Document immutable Pi compaction instruction refresh

* no-mistakes(document): Correct Pi refresh documentation and validation claims
* feat(bin): add deterministic condition->action watch adapter on the process-event channel

Register a (condition, action) pair once with bin/fm-procevent-when.sh and the
existing process-to-event runner polls the condition tokenlessly, fires the
action at most once on a stable true, and wakes firstmate exactly once with the
captured outcome - instead of burning an agent turn per re-check.

The pair is stored privately under state/when/ and hash-bound by a trust record
the same way fm-check-register.sh binds a custom check, so a mutated spec is
refused without executing anything. A durable exclusive fired marker claimed
before the action makes restarts and re-polls unable to double-fire; every
failure path (mutated spec, condition error past budget, expired deadline,
failed action, uncaptured earlier fire) ends in a terminal captured outcome
that wakes firstmate rather than a silent retry. Eligibility stays a firstmate
judgment: only exact, safe, reversible actions may be bound, and judgment-
needing or destructive actions keep the wake-and-decide flow.

* no-mistakes(review): Harden when watcher concurrency, deadlines, timeouts, and output

* no-mistakes(test): Bind watcher actions to registered executable bytes

* no-mistakes(document): Correct condition-action watcher documentation

* no-mistakes(document): Clarify outcome wake re-announcement

* no-mistakes: apply CI fixes
…id#2202)

The open-decisions fold only recognized a [key=<slug>] token between the
verb and the colon (needs-decision [key=x]: note). The common worker
shape with the colon first (needs-decision: [key=x] note) silently
folded its stated key into the shared "default" bucket, so two open
decisions could collapse into one record and fm-send --resolve-key <x>
refused to close the decision it plainly named.

A complete token at the head of the note is now an equivalent stated-key
position for every keyed verb, shared by the whole-file and incremental
folds through the one _fm_decision_key owner. The documented
before-colon position wins when both are present, a token deeper in the
note stays prose, a bare keyless line still folds to "default", and a
stated-but-malformed slug is rejected rather than rewritten to
"default". A consumed note-head token is stripped from the note so both
positions yield identical records, and the incremental fold version is
bumped so persisted cursors folded under the old interpretation are
rebuilt from the authoritative log.

Fixes kunchenguid#2109
…uid#2212)

* fix(bin): keep a recovery acknowledgement valid across republication

A watcher cycle that opened and closed while the model handled its drained
wakes minted a fresh recovery generation, which invalidated the exact
acknowledgement the drain had just printed. That acknowledgement then consumed
nothing, so the marker stayed pending and every later arm spent its whole cycle
re-announcing the same recovery instead of supervising - a livelock the home
could not leave on its own.

A downtime publication now reuses the generation of an outstanding handling
episode, so a close during the handling window cannot orphan the printed
acknowledgement. The acknowledgement itself separates its two facts: queue-row
consumption is bound to the monotonic --ack-through sequence and always
happens, while only retiring the episode is bound to --recovery-generation. A
generation that moved on is a non-fatal result that names its own remedy
instead of a refusal that consumes nothing.

* no-mistakes(review): Preserve recovery generations and consume stale acknowledgements safely

* no-mistakes(document): Document sequence-bound recovery acknowledgements
* feat(fmx-respond): consume in_reply_to_chain conversation context

The relay's poll payload can carry in_reply_to_chain, an oldest-first
transcript of the surrounding conversation, but the mention-handling
procedure only ever read the immediate in_reply_to parent, so referents
like "this" in a standalone mention stayed unresolvable even when
context was delivered.

Teach fmx-respond to read the chain when present (optional and
backward-compatible: often absent today, kind label not required),
resolve referents against the whole transcript, and extend the
untrusted-content framing to every chain entry including the upcoming
kind=history entries. Document the field's wire shape in
docs/configuration.md as the firstmate-side owner.

* no-mistakes(document): Document Relay chain context ownership
* fix(bin): strip every bracket tag, not just [key=...], from a status verb

status_line_verb only stripped a leading "[key=...]" token before the
colon, so a remote secondmate reply's leading "[corr=...]" correlation
tag stayed glued onto the returned verb word ("needs-decision
[corr=...]" instead of "needs-decision"). The open-decisions fold's
verb match then silently failed to recognize the line at all, so
fm-send --resolve-key refused to close a decision that was plainly
open on the status line.

Generalize the parser to strip every "[name=value]" tag before the
colon, in any order and count, so local and remote replies fold
identically.

* no-mistakes(review): Invalidate stale decision cursors after parser fix

* no-mistakes(document): Clarify status metadata verb parsing
* fix: collapse duplicate supervision wakes without losing legitimate updates

One remote-secondmate note produced two handling turns (a procevent check
wake published before autohandle, then a signal wake for the same mirrored
bytes), already-ingested replays such as a cursor-loss whole-log recapture
still woke with nothing to do, this home's own bookkeeping closes (fm-send
--resolve-key, the pending-reply escalation close, the captain-held
transfer) re-woke the session that wrote them, and turn-ended-only wakes
were annotated with already-announced status lines that looked like fresh
progress.

Dedup rules, each at its layer's one owner:
- fm-procevent.sh: an adapter may declare 'self-announcing'; the runner
  then applies first and publishes a check wake only for what remains
  unhandled. fm-procevent-remote-reply.sh declares it: the mirrored status
  append is the single announcement, so a fully applied capture publishes
  nothing and a byte-identical replay stays completely quiet. All other
  adapters keep strict publish-before-apply.
- fm-wake-lib.sh: fm_wake_signal_sig/seen_path/seen_current now own the
  watcher's signal signature and .seen-* marker format, plus
  fm_wake_status_append_self_announced, the guarded bookkeeping append
  that advances the marker only over exactly its own bytes and fails
  toward waking on any pending or interleaved foreign write.
- fm-send.sh, fm-pending-reply-lib.sh, fm-decision-hold.sh: bookkeeping
  closes go through that guarded append; escalation opens stay plain
  appends because a new blocker must wake.
- fm-wake-lib.sh annotations: a historical (turn-ended-only) row skips its
  status annotation only when the file's signature provably matches the
  seen marker; anything unannounced keeps annotating.
- fm-classify-lib.sh: a kind=secondmate task's status signal is never
  absorbed as provably-working, because that stream is the routed-reply
  channel the parent must read.

Also fixes a pre-existing exit-path deadlock the regression run reproduced:
a TERM inside a recovery-marker critical section left fm_lock_try_acquire
spinning against this same process's abandoned hold; a self-held lock is
now reclaimed (a subshell still waits on its parent's live hold).

Regression tests drive the real wake functions and executables in both
directions: each duplicate case collapses, while a new remote reply, new
decision, new blocker, merge result, failure, first status change, and a
later different note on the same task all still wake.

* no-mistakes(document): Document wake deduplication contracts
* feat(harness): add Cursor Agent CLI adapter

# Conflicts:
#	bin/fm-spawn.sh

* fix(composer): read cursor-agent's reverse-video placeholder as idle

cursor-agent renders its idle composer placeholder dim (SGR 2) but paints the
cell under the terminal cursor in reverse video (SGR 0;7). Reverse video is
neither dim nor a dark truecolor foreground, so the shared ghost stripper keeps
that one character and an idle composer reduces to a lone `P`. Judged on its
own, that remnant reads `pending` on a genuinely idle pane, which defers
away-mode escalation indefinitely on the styled cursorless backends.

Teach the ONE fleet-wide classifier the shape instead of adding an adapter-local
copy: register `→` as an agent prompt glyph so the composer row is structurally
findable at all (without it the bottom-most shape is a stale shell prompt echo
in the scrollback), add both verified placeholders to the idle set, and consult
the styling-independent plain row when the styled row is only a remnant.

The plain-row branch demands the remnant be a proper, strictly shorter substring
of a plain row matching a fully anchored placeholder. Real typed text is
uniformly bright, so stripping leaves it equal to the plain row and it stays
`pending` - verified live against a pane where the typed text was exactly the
placeholder string.

Verified live on cursor-agent 2026.08.11-e8db854; the regression pins the real
captured bytes and asserts the remnant survives stripping, so the case cannot go
vacuous if the stripper later learns SGR 7.

Co-authored-by: Amplify Logic AI <lars@sockinator.co>

* feat(cursor): narrow cursor identity and order its marker before CLAUDECODE

Cursor ships two executable names - `cursor-agent` and the legacy alias `agent`
- and runs as a bundled node script, so tmux reports the pane command as a bare
`node`. Neither `agent` nor `node` can be trusted by name, so identity gets one
owner in bin/fm-cursor-lib.sh that demands cursor's own name or install tree in
the path or argv[0], from the structural signal only. Probing an arbitrary pid's
executable during a liveness poll would execute a stranger's binary, which is
the hazard that rule exists to close.

Two consequences wired up:

Detection. cursor-agent does NOT clear an inherited CLAUDECODE, so a cursor
worker launched under a claude primary carries both markers and whichever is
tested first wins. The cursor markers are ordered ahead of the CLAUDECODE check;
fm-spawn additionally clears foreign markers at the launch boundary. Both are
kept deliberately - launch sanitization only covers sessions fm-spawn started,
while the ordering also covers a cursor session started by hand. Verified live
that CURSOR_INVOKED_AS is set on the agent process and CURSOR_AGENT=1 on the
child/tool processes fm-harness.sh actually runs as.

Pane liveness. A cursor pane now classifies `agent`. An unrelated node or agent
stays `other`, which the liveness callers already fold into `ambiguous` rather
than `dead`, so a stranger's node pane is never reported agent-free.

Resolution prints the STABLE launcher rather than the canonical target: identity
is proven through canonicalization, but cursor's canonical path carries a
version its own auto-update replaces, and pinning that would strand a task on a
version that can vanish.

The regression drives the two identity signals apart - a cursor-named executable
outside any cursor tree, and a non-cursor-named alias inside one - and asserts
each carries a verdict alone, so no single vendor string is load-bearing. Its
negative controls are real spawned processes, not fixtures.

Verified live on cursor-agent 2026.08.11-e8db854.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): classify cursor busy state from its own turn transcript

Cursor shipped as "unknown cursor-unverified" on the premise that it exposes no
semantic turn lifecycle, only a rendered "Working" footer. That premise is
wrong: cursor-agent persists an append-only JSONL transcript per conversation
and brackets every submitted turn with a role:user open and a typed turn_ended
close. Verified live on 2026.08.11-e8db854, including the interrupt path, where
Escape closes the turn with status "aborted" - so this source covers manual
interruption, which Claude's Stop hook does not.

That makes it a genuine pull source in the muse mould rather than the rendered
text the redesign forbids: no writer, no arm, no gen, nothing seeded that could
never be cleared. Cursor's `ctrl+c to stop` footer stays out of the verdict, and
herdr's narrower native streaming state cannot stand in for it either.

Binding deliberately does not reconstruct cursor's workspace-slug directory
name. That slug collapses path separators, so rebuilding it would be a guess
that could bind the wrong pane; cursor records the exact absolute workspace path
in each project's .workspace-trusted, and the binding matches on that. A
conversation recorded as prior at spawn is excluded, so a relaunch in a reused
worktree folds its own turn rather than its predecessor's. Requiring a unique
remaining conversation keeps zero and several both unknown, because neither
proves anything about the current turn.

The regression pins the fold with real transcript files and asserts the
dangerous direction stays closed: an unresolvable binding, a record-free file,
an unclaimed workspace, and a workspace-path PREFIX all read unknown, never
idle. The prefix case uses an opaque fixture slug so a slug-rebuilding
implementation cannot pass it.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* feat(cursor): make the cursor launch runnable and give it lifecycle control

Five gaps that together kept a cursor crewmate from being drivable end to end.

Launch. The template invoked `cursor agent`, but `cursor` is not the CLI - the
installed names are `cursor-agent` and the legacy alias `agent` - so the command
could not run at all on a machine with a normal cursor install. It now resolves
through the verified owner, which also refuses a spawn loudly instead of leaving
a pane that dies with command-not-found and reads as a wedged worker.

Session binding. fm-spawn writes state/<id>.cursor-session so the busy fold can
find this pane's transcript, and teardown removes it.

Lifecycle control. No cursor PR touched fm-control-lib.sh, so
`fm-control <id> interrupt|exit|relaunch` could not drive a cursor worker at
all. Verified live: interrupt is a single Escape, exit is /exit, and cursor does
NOT repollute its composer with the cancelled prompt, so unlike muse it needs no
clear key. Secondmate is refused, matching the spawn refusal.

Submit acknowledgement. cursor parks its terminal cursor outside its composer,
so the composer verdict on tmux is always `unknown` and a submit could never be
acknowledged from the composer alone. The submit core's existing idle-to-busy
transition covers that case, but only if the pane's busy footer is recognised,
so cursor's `ctrl+c to stop` joins the harness-less default union the submit
cores read. The TOKEN is matched rather than the spinner verb: the same version
rendered both `Working` and `Running` in consecutive turns.

Bootstrap. A configured cursor crew harness with no cursor executable is now a
loud MISSING diagnostic rather than a first-spawn failure, and it accepts either
installed name.

Interrupt cancellation is deliberately left unconfirmed. The transcript does
type an aborted close, but its post-interrupt write latency measured as
variable - sometimes seconds, sometimes not within twenty - so a claim built on
it would be unreliable. Normal turn completion is prompt, which is what the busy
fold actually depends on.

Two inherited tests are corrected rather than deleted: the busy test asserted
cursor could have no semantic source, and the launch test pinned the literal
`cursor agent` string. Both now pin the verified behaviour, including that the
launch never allocates a second worktree.

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(cursor): record the verified crewmate facts and extend the drift guard

The inherited cursor entry was written against 2026.08.04-aaa8809 and several of
its claims no longer hold: it named `cursor agent` as the binary (not the CLI
name), listed six Grok model ids of which the live catalog now returns two, and
recorded busy state, exit, interrupt, and skill invocation as unverified.

Replaced with what was measured against 2026.08.11-e8db854, including the two
facts most likely to be rediscovered painfully: cursor runs as a bundled node
script so its pane title is a bare `node`, and it parks its terminal cursor
outside its composer, which makes the tmux composer verdict permanently
`unknown` by design rather than a defect to chase.

Model ids now route to `--list-models` for the account instead of a fixed list,
since that list is exactly what drifted.

The live drift guard covers cursor, resolving it through the same verified owner
fm-spawn uses and passing --trust so the probe cannot hang on the workspace
prompt. Run against every installed harness: 8 checked, all alive, with cursor
reporting title='node' foreground=[.../cursor-agent] - the drift shape this
guard exists to catch.

Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>

* docs(agents): record the cursor session-binding state file

The state/ layout section is the inventory every session reads; a busy-source
binding that fm-spawn writes and teardown removes belongs in it alongside muse's.

* no-mistakes(review): Sanitize ambient Cursor marker in harness tests

* no-mistakes(review): Validate Cursor models against live catalog

* no-mistakes(review): Reject unsupported secondmates before binary preflight

* no-mistakes(review): Narrow Cursor ancestry detection to structured process identity

* no-mistakes(review): Parse Cursor transcripts and sanitize inherited markers

* no-mistakes(review): Handle malformed Cursor transcript records safely

* no-mistakes(review): Validate malformed Cursor closes in fallback parser

* no-mistakes(review): Retire stale Cursor bindings during relaunch

* no-mistakes(review): Fix Cursor drift guard command variable

* no-mistakes(review): Narrow Cursor identity to versioned install trees

* no-mistakes(document): Document Cursor harness boundaries

* refactor(composer): move the delivery busy footers to the shared owner

The per-harness rendered busy footers lived in bin/fm-tmux-lib.sh under
FM_TMUX_* names, so cursor's `ctrl+c to stop` signature - and every other
harness's - was reachable only from tmux. That placement was wrong on its own
terms: herdr, zellij, cmux, and orca run the same harnesses and face the same
question these footers answer, which is whether a submitted Enter actually
landed. Nothing about the signature is tmux-specific.

Moved verbatim into bin/fm-composer-lib.sh, the shared composer/delivery owner
every backend already sources, and renamed to FM_DELIVERY_* so the names stop
claiming a scope they never had. All five adapters now reach cursor's signature;
verified per adapter rather than assumed.

The boundary the move must not blur is stated where it now lives: this is a
DELIVERY guard, never a worker-state source. Confirming a keystroke landed is a
different question from asking what a worker is doing, and bin/fm-busy-lib.sh
remains the semantic owner that forbids classifying a harness from rendered
text. Cursor still classifies only from its transcript fold, which is already
backend-agnostic because it folds a file rather than reading a pane - the same
verdict on all six backends.

The old FM_TMUX_* aliases are dropped rather than kept as dead shims: nothing
outside the moved block referenced them except fm-busy-lib.sh's grok fallback,
which now reads the new name. The documented operator override, FM_BUSY_REGEX,
is untouched.

Also removes a dead duplicate CURSOR_INVOKED_AS check in bin/fm-harness.sh,
unreachable behind the marker check above it.

* no-mistakes(review): Correct shared delivery guard ownership references

* no-mistakes(document): Document shared delivery guards and Cursor backend limits

* no-mistakes: apply CI fixes

* fix(composer): bound a bare composer's wrap region at a half-block rule

A live cursor crewmate on herdr classified its IDLE composer as `pending`, and
fm-send consequently exited 1 with "delivery unconfirmed" on a message that had
actually landed. The cause is not cursor-specific.

Herdr draws a composer's top and bottom rules with the half-block glyphs U+2584
and U+2580 rather than the box-drawing family. fm_composer_row_has_edge knew
only the box-drawing set, so no box was detected; the composer was found as a
BARE row, and its wrap region - which extends while rows are non-blank and carry
no structural edge - walked straight through the composer's own closing rule and
swallowed the model and path footer below it. That footer is real text, so the
region classified pending on a genuinely idle pane.

Teaching the shared edge detector the half-block glyphs bounds the region at the
closing rule. Measured on the captured bytes of a real herdr cursor pane: the
same capture that read `pending` now reads `empty`.

This is a shared shape-path change, so it is deliberately narrow - it adds
glyphs to the edge vocabulary and changes no verdict logic - and the whole
composer and backend suite is green, including the other harnesses' herdr
fixtures.

The regression pins the real captured shape and asserts the footer content is
genuinely present, so the case cannot pass vacuously if the region were ever
bounded for some unrelated reason.

* fix(herdr): confirm a cursor submit from the rendered-footer transition

Herdr's composer-shape fix made an idle cursor pane classify `empty`, but
`fm-send` still exited 1 with "delivery unconfirmed" on messages that had
actually landed. Live measurement found the second, independent cause.

Herdr reports a cursor pane `agent_status=blocked` in EVERY state - idle,
mid-turn, and after - so the submit path's idle-baseline native confirmation is
structurally unreachable for cursor and every send falls into the composer
branch. That branch reads cursor's mid-turn composer row, which renders its own
`Add a follow-up` placeholder beside a right-aligned `ctrl+c to stop`. That
token is composer content, so the verdict is `pending` on a composer holding no
user text at all, and the Enter-retry budget then reports pending.

The escape is the same semantic signal the native path uses, read from the
pane's verified busy footer instead of native agent-state, and it is the
rendered-footer twin of the tmux submit core's turn-started confirmation: an
idle-to-busy transition ACROSS our Enter proves the harness accepted the
submission. The baseline is taken before the first Enter and only when the
native baseline was not legibly idle, so the idle-baseline path still never
reads pane content and a pane already mid-turn before we typed keeps reporting
`pending` rather than borrowing another turn as proof of this delivery.

The composer verdict is deliberately NOT relaxed. A right-aligned status token
on the composer row stays content for every other caller, including the
away-mode pre-injection guard, and the shared cursorless submit core is left
untouched so zellij, cmux, and Orca keep the behavior their own follow-up owns.

Verified live on herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 in an isolated
lab session: `fm-send` now exits 0 and the steer executes, interrupt cancels a
running turn, `/exit` stops the agent, and teardown clears the record. All seven
panes of the running default session classify identically before and after the
shape fix, so no other harness regressed.

* no-mistakes(review): Prevent working Herdr baselines from falsely confirming delivery

* no-mistakes(document): Correct Cursor harness and backend documentation

---------

Co-authored-by: ABHISHAKE KUMAR BOJJA <abojja@uvic.ca>
Co-authored-by: Amplify Logic AI <lars@sockinator.co>
Co-authored-by: Ville Penttinen <villem.penttinen@gmail.com>
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness

Homes on latest main need quota-axi kunchenguid#87 so Desktop-absent CLI machines report a fresh Cursor quota instead of a false sign-in-required.

* no-mistakes(document): Update quota floor documentation pointer
…nsafe

fm-ensure-agents-md.sh symlinked CLAUDE.md -> AGENTS.md unconditionally.
On a repo whose git checkout will not materialize symlinks (Windows
checkouts commonly run with core.symlinks=false), a tracked symlink
checks out as a plain text stub holding its link target, silently
breaking any agent that reads CLAUDE.md. site-feasibility hit and
reverted exactly this; running the helper again reintroduced it.

New decision rule:
- A repo that already has a real, regular CLAUDE.md is never promoted
  into a symlink arrangement. Promotion now copies CLAUDE.md's content
  into AGENTS.md and keeps CLAUDE.md as a real file synced to it,
  regardless of whether this worktree's own filesystem happens to
  support symlinks.
- When CLAUDE.md doesn't exist yet, a new claude_symlink_unsafe() check
  decides whether to create it as a symlink or a real synced duplicate.
  It combines two real signals instead of guessing from the path: git's
  own effective core.symlinks for that repo (git's own record of
  whether it will materialize a tracked symlink there), and a live
  filesystem probe as a fallback when that config is unset.
- Two real files with identical content are now treated as the
  synced-duplicate steady state (idempotent "unchanged"); two real
  files with different content still refuse as a genuine conflict.

The generated brief text needs no change: the helper stays a no-arg-
required, safe-by-default call.

Colocated tests cover: a real existing CLAUDE.md never becoming a
symlink (including a core.symlinks=false, site-feasibility-shaped
repro); the symlinks-unreliable fallback producing a synced real file,
both when creating from scratch and when only AGENTS.md exists; the
existing correct-symlink and case-variant conflict paths untouched;
and the different-content conflict refusal preserved.
The round-1 no-mistakes reviewer flagged, and the captain verified live on
his own /mnt/c checkout, that a fresh repo on WSL's DrvFs (the bind that
exposes a native Windows drive into Linux) still passed both existing
signals: git config core.symlinks is unset on a brand-new repo, and ln -s
succeeds on DrvFs itself, so claude_symlink_unsafe() would still emit a
real CLAUDE.md symlink there. A later native-Windows git clone of that same
repo defaults to core.symlinks=false and materializes it as the dead
9-byte stub - the exact regression this change exists to prevent - and
this is the captain's own highest-risk environment, since all his
production checkouts live under /mnt/c.

Add a third, honest signal: on_drvfs() reads the real mount table
(FM_PROC_ROOT_OVERRIDE-overridable, /proc/mounts by default, the same
override convention bin/fm-cursor-lib.sh already uses for testability)
rather than matching the path string. DrvFs surfaces as fstype "drvfs"
directly on older WSL, or as fstype "9p" carrying "aname=drvfs" in its
mount options on current WSL2 - confirmed against a live WSL2 host, where
/mnt/c is aname=drvfs and /usr/lib/wsl/drivers is a same-fstype "9p" mount
with aname=drivers, correctly left alone. The two existing signals
(core.symlinks=false, the live ln -s probe) are kept unchanged as
additional checks; any one signal reporting unsafe is enough.

Colocated tests: a fresh git-init repo under a simulated DrvFs mount (via
FM_PROC_ROOT_OVERRIDE) gets a real, synced CLAUDE.md rather than a
symlink, with a same-directory ln -s sanity check proving the block came
from the DrvFs signal and not an incidental local filesystem limit; and a
plain non-DrvFs /mnt mount (ext4) still gets a real symlink, proving the
check isn't guessing from the /mnt/ path prefix alone.
…d-conflicting

no-mistakes review flagged, and firstmate confirmed as a required fix under
standing authority, that a real-duplicate CLAUDE.md (the symlink-unsafe
fallback) had no way to stay in sync past the first divergence. The normal
workflow - a task edits only AGENTS.md, per bin/fm-brief.sh's Project
memory section, then re-runs this helper - leaves the two real files
differing, and the next run then hard-exited 1 asking for manual
reconciliation instead of re-syncing, on exactly the DrvFs/Windows-checkout
projects this whole change targets.

Firstmate's decision (option c of three considered): mark helper-created
real-duplicate CLAUDE.md files and auto-resync only marked files on a
mismatch, keeping today's hard-conflict refusal for any CLAUDE.md this
helper did not create - closing the drift papercut without weakening the
conflict safety net for a genuinely independent file. Rejected: always
auto-resyncing on mismatch (would silently discard independently-authored
CLAUDE.md content, violating "never clobber a distinct real file"), and
leaving it as a hard conflict (makes every future AGENTS.md edit on the
captain's own Windows checkouts - the highest-risk set this task exists to
protect - require manual reconciliation, failing "safe by default").

Implementation: write_claude_duplicate() writes AGENTS.md's content plus a
single trailing HTML-comment marker line whenever CLAUDE.md is written as a
real duplicate (both on first creation and on resync). claude_has_marker()
checks a CLAUDE.md's actual last line for that marker to tell a
helper-owned duplicate apart from a hand-authored file; its absence always
falls through to today's hard-conflict-on-mismatch behavior (fail safe, no
new abstraction, no config, no state file outside the repo). A legacy
byte-identical duplicate from before this marker existed is still
recognized as in sync and gets upgraded with the marker so its project
self-heals going forward too.

Also fixed the round-2 review's auto-fix finding: the non-DrvFs-mount test
fixture used a repo path under $TMP_ROOT rather than an actual /mnt-shaped
path, so it would have still passed even if on_drvfs() regressed into
path-prefix guessing instead of reading the real mount table. The repo now
lives under a genuine $TMP_ROOT/mnt/z/... subtree, with a fixture assertion
that the path actually resolved to that shape.

Colocated tests added: a marked duplicate resyncs (and stays idempotent)
after an AGENTS.md-only edit; an unmarked real CLAUDE.md still refuses on
a content mismatch; a legacy byte-identical unmarked duplicate is upgraded
with the marker on its first post-upgrade run. Existing tests that
asserted byte-for-byte equality between AGENTS.md and a real-duplicate
CLAUDE.md were updated to a shared assert_synced_duplicate helper that
checks the new expected shape (AGENTS.md content plus exactly one marker
line) instead.
…accurately

Two message-accuracy corrections to bin/fm-ensure-agents-md.sh's CLAUDE.md
sync-marker mechanism, decided by firstmate as message-only fixes with no
new refusal path, guarantee, or subsystem:

- The legacy-duplicate upgrade path (a byte-identical real CLAUDE.md from
  before the sync marker existed) reported "unchanged" even though
  sync_claude() had just rewritten CLAUDE.md's bytes to add the marker.
  Crewmates read this status line, via bin/fm-brief.sh's Project memory
  instruction, to decide whether anything needs committing, so the report
  now distinguishes a real marker-added write ("updated: added the sync
  marker...") from a genuine no-op ("unchanged: ..."), based on whether
  CLAUDE.md already carried the marker before this run touched it.

- The promotion path (an already-real CLAUDE.md gets copied into AGENTS.md
  and marked) permanently opts that file into the auto-resync mechanism
  without saying so: a captain who kept hand-editing CLAUDE.md directly
  after promotion would have those edits silently discarded on the next
  run. The "promoted:" line now states plainly that CLAUDE.md is a managed
  mirror of AGENTS.md from that point on and that future edits should
  target AGENTS.md instead - the promotion behavior itself is unchanged.

Colocated tests updated: the legacy-upgrade test now asserts the accurate
"updated:" report (and that a further re-run correctly reports "unchanged"
once nothing is left to change), and the promotion test asserts the new
managed-mirror wording.
@mfernrdx mfernrdx changed the title fix(bin): keep CLAUDE.md a real synced file where symlinks are unsafe feat(bin): add agent control plane, Cursor harness, and symlink-safe CLAUDE.md syncing Aug 14, 2026
@mfernrdx
mfernrdx merged commit 871bd0a into main Aug 14, 2026
14 checks passed
mfernrdx pushed a commit that referenced this pull request Sep 22, 2026
…nguid#3681)

* fix(bin): recognize active pipeline fix rounds with unfetched run heads

A no-mistakes fix round advances the run head beyond the submitted head,
and the pipeline commits in its own checkout, so the task copy never
receives the new commit object. fm-crew-state's strict head rule rejected
the active row, the coarse runs-list scan skipped it and matched the
older failed row at the submitted head, and an active validation read as
failed (observed on model-routing-benchmark-hardening: active head
ac61c64 vs task copy at fb47636d).

fm_nm_runs_status_for_worktree in bin/fm-nm-run-lib.sh now owns
runs-ledger attribution: the branch's newest row alone decides, and a
newest row whose head cannot resolve locally is recognized only as a
provable pipeline-owned continuation - active (running) and anchored by
the immediately older row for the same branch having ended at exactly
this worktree's HEAD. The reader keeps the axi TOON as full detail for
that proven same-branch run. Unanchored, ancestor-anchored, and terminal
unresolvable rows stay unattributed, so branch-name coincidence and other
tasks' runs never match, and fm_nm_head_matches_worktree keeps its exact
prior semantics for teardown (verified by the full teardown suite).

Tests: reproduction regression for the unfetched active fix head (reads
working via full run-step detail), coarse-path continuation when axi
answers another branch, and negative controls for the unanchored active
row and the unresolvable terminal row with the historical fallback
preserved.

Ported onto upstream/main f4d7875, where kunchenguid#3194 independently added the
branch_sync custody exemption on the full axi-status path: both mechanisms
now coexist, each owning one surface (TOON custody on the full path, the
runs ledger on the coarse path). The port deletes the superseded coarse
scan-and-skip (nm_runs_status_for_branch) and its now caller-less helpers
(fm_nm_head_resolvable, nm_coarse_head_matches_worktree), renames the
exemption comment's "the one exemption" phrasing now that a second
complementary exemption exists, and points the stale
FM_CREW_STATE_RUNS_LIMIT comment at fm_nm_runs_status_for_worktree
(judge follow-up #1). The parent coarse-guard test's fixture is the
ledger-anchored continuation shape, so its expectation flips to the fixed
behavior (working via run-step, never the older failed row); a new
mismatched-anchor coarse negative control preserves that guard's original
no-anchor protection (pane answers, never the older row).

* no-mistakes(document): Clarify pipeline attribution documentation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants