feat: add Cursor CLI crew harness - #2238
Merged
Merged
Conversation
# Conflicts: # bin/fm-spawn.sh
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>
…DECODE 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>
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>
…ontrol 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>
… 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>
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.
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.
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.
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.
kunchenguid
force-pushed
the
fm/fm-cursor-crew-harness-build-r1
branch
from
August 13, 2026 02:20
d7240fe to
f52dc1e
Compare
spirispark
added a commit
to spirispark/firstmate
that referenced
this pull request
Aug 13, 2026
* fix: preserve fleet state in truncated session-start digests (#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
* feat(send): close answered decisions at answer time via --resolve-key (#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.
* fix(bin): seed remote secondmates from supplied origins (#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
* fix(tests): restore reliable fm-send backend parity coverage (#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. #1842 made bin/fm-send.sh source
bin/fm-line-cap-lib.sh (added by #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 remote secondmate status streams (#1846)
* 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
* docs(agents): describe the digest's fleet-state-before-context order (#1826)
* fix(bin): fail closed on NUL bytes in the durable parent binding (#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.
* fix(skills): reconcile inherited secondmate plans with shipped state (#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
* fix: move network checks off the session-start blocking path (#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 #1845, which was cut from a
pre-#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 #1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from #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 remote replies during capture (#1831)
* 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
* feat(skills): port internal stow curation disciplines to the public skill (#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).
* chore(bootstrap): raise lavish-axi version floor to 0.1.46 (#1865)
* fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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
* feat(startup-network): record per-step elapsed times for the deferred stage (#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.
* feat(stow): cascade the internal /stow to every registered secondmate (#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
* fix(remote-job): stop workers abandoned by a pruned code root (#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.
* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)
* 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): add tiered decaying memory management (#1984)
* 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 project vision (#1997)
* 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 (#2005)
* fix(spawn): force regular Pi TUI for crews
* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composers (#2029)
* fix(cmux): classify borderless Claude composer
* no-mistakes(review): Normalize cmux NBSP prompts across locales
* no-mistakes(document): Document cmux borderless Claude composer classification
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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 ro…
mfernrdx
added a commit
to mfernrdx/firstmate
that referenced
this pull request
Aug 14, 2026
…CLAUDE.md syncing (#1) * fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (#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 * feat(startup-network): record per-step elapsed times for the deferred stage (#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. * feat(stow): cascade the internal /stow to every registered secondmate (#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 * fix(remote-job): stop workers abandoned by a pruned code root (#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. * feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568) * 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): add tiered decaying memory management (#1984) * 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 project vision (#1997) * 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 (#2005) * fix(spawn): force regular Pi TUI for crews * no-mistakes(document): Documented Pi regular TUI launch mode * fix(cmux): classify borderless Claude composers (#2029) * fix(cmux): classify borderless Claude composer * no-mistakes(review): Normalize cmux NBSP prompts across locales * no-mistakes(document): Document cmux borderless Claude composer classification * docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065) * 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: measure Herdr automation on Windows runners (#2100) * 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 * feat(ahoy): guide captains through open decisions (#2099) * Add guided ahoy decision flow * no-mistakes(document): Document guided Ahoy decision flow * fix(stow): enforce startup-memory budget decisions (#2110) * Harden stow memory budget policy * Refine internal stow offload policy * no-mistakes(review): Enforce shared-budget decisions and autonomous offload * fix(spawn): refresh pooled worktrees from origin before launch (#2116) * fix(spawn): refresh pooled worktree base * no-mistakes(document): Document spawn base-freshness invariant * no-mistakes: apply CI fixes * fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted; naming converges with PR #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 TUI mode by CLI capability (#2117) * 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 * docs(vision): elevate experience, pain narrative, and distro virtues (#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. * feat(bin): reconcile inactive terminal crew outcomes (#2167) * 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 * ci: raise Herdr test timeout (#2191) * fix: refresh stale Pi instructions after compaction (#2163) * 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: add deterministic condition-to-action watcher (#2200) * 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 * fix(bin): honor a decision key stated after the verb colon (#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 #2109 * fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206) * 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: parse decision verbs before status metadata tags (#2280) * 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(bin): collapse duplicate supervision wakes (#2287) * 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: add Cursor CLI crew harness (#2238) * 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(bin): require quota-axi 0.1.25 (#2300) * fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness Homes on latest main need quota-axi #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 * fix(bin): never turn a real CLAUDE.md into a symlink, avoid it when unsafe 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. * no-mistakes(review): trap-clean symlink probe, fix architecture doc claim * no-mistakes(document): document unconditional real-CLAUDE.md rule in architecture.md * fix(bin): close the fresh-repo DrvFs gap in claude_symlink_unsafe() 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. * fix(bin): self-heal a drifted real CLAUDE.md duplicate instead of hard-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. * no-mistakes(review): make CLAUDE.md sync marker CRLF-aware * no-mistakes(review): keep CLAUDE.md sync marker on its own line * no-mistakes(document): document CLAUDE.md sync-marker resync contract in architecture.md * fix(bin): report the sync-marker upgrade and promotion mirror status 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. * no-mistakes(review): strip sync marker when promoting CLAUDE.md into AGENTS.md * no-mistakes(document): document marker adoption and promotion mirror contract --------- Co-authored-by: Jay Park <jay.jongcheol.park@gmail.com> Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com> Co-authored-by: Roelof Blom <roelof@rb2.nl> 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> Co-authored-by: Michael Fernandez <michael@…
tiago-peixoto
added a commit
to tiago-peixoto/firstmate
that referenced
this pull request
Aug 14, 2026
* Add permanent fork main integration workflow
* no-mistakes(review): gate startup upstream probe on validated fork topology
* no-mistakes(test): retire upstream-accepted divergences on re-provable Git evidence
* no-mistakes(document): add fork manifest to shared tracked material list
* fix(fork): make integration workflow actor-safe
* no-mistakes(review): guard empty resolved revert and tighten fork path handling
* no-mistakes(document): correct fork-upstream network-check and fork script doc facts
* no-mistakes(document): name fork-upstream probe in label; deduplicate fork skill
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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(bin): require quota-axi 0.1.25 (#2300)
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness
Homes on latest main need quota-axi #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
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* no-mistakes(review): Fix fork update and divergence lifecycle safeguards
* no-mistakes(review): Guard fork propagation and document daily cadence
* no-mistakes(review): Support nested fork delivery and safe upstream retirement
* no-mistakes(document): Clarify fork-main documentation contracts
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
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>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
tiago-peixoto
added a commit
to tiago-peixoto/firstmate
that referenced
this pull request
Aug 15, 2026
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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(bin): require quota-axi 0.1.25 (#2300)
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness
Homes on latest main need quota-axi #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
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* no-mistakes(review): Fix wake annotation parsing for unbounded status reads
* no-mistakes(document): Correct stale wake-read documentation comments
---------
Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
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>
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
This was referenced Aug 16, 2026
This was referenced Aug 17, 2026
huynhtandat223
added a commit
to huynhtandat223/firstmate
that referenced
this pull request
Aug 18, 2026
* feat(bin): add deterministic agent lifecycle control (#1568)
* 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): add tiered decaying memory management (#1984)
* 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 project vision (#1997)
* 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 (#2005)
* fix(spawn): force regular Pi TUI for crews
* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composers (#2029)
* fix(cmux): classify borderless Claude composer
* no-mistakes(review): Normalize cmux NBSP prompts across locales
* no-mistakes(document): Document cmux borderless Claude composer classification
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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(bin): require quota-axi 0.1.25 (#2300)
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness
Homes on latest main need quota-axi #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
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
* fix: keep the public promise reachable when work is routed to a second mate (#2457)
The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.
- fmx-respond now states the routing rule crisply: a task in this home takes the
lightweight link, and second-mate-routed work takes a promised-final
commitment bound to that home, registered up front with the brief command
carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
second mate whose home actually holds it and printing the promised-final
registration command, with the exact --work-home when the match is
unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
still owes a public reply bound to main/<key>, since that binding no longer
names the home owning the work. The move itself is never blocked.
Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)
* fix(skills): hint that remote secondmate liveness verdicts false-negative
fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.
* no-mistakes: apply CI fixes
* fix(calm): keep Pi's export confirmation visible (#2461)
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.
Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.
The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
* feat(stow): add open-record persistence to /stow before reset (#2488)
* feat(stow): persist the open records a session is holding
/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to re…
kaan-sirin
pushed a commit
to kaan-sirin/firstmate
that referenced
this pull request
Aug 18, 2026
* feat(harness): add Cursor Agent CLI adapter * 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>
guanchengh-lgtm
added a commit
to guanchengh-lgtm/firstmate
that referenced
this pull request
Aug 25, 2026
* feat(bin): add deterministic agent lifecycle control (#1568)
* 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): add tiered decaying memory management (#1984)
* 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 project vision (#1997)
* 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 (#2005)
* fix(spawn): force regular Pi TUI for crews
* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composers (#2029)
* fix(cmux): classify borderless Claude composer
* no-mistakes(review): Normalize cmux NBSP prompts across locales
* no-mistakes(document): Document cmux borderless Claude composer classification
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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(bin): require quota-axi 0.1.25 (#2300)
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness
Homes on latest main need quota-axi #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
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
* fix: keep the public promise reachable when work is routed to a second mate (#2457)
The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.
- fmx-respond now states the routing rule crisply: a task in this home takes the
lightweight link, and second-mate-routed work takes a promised-final
commitment bound to that home, registered up front with the brief command
carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
second mate whose home actually holds it and printing the promised-final
registration command, with the exact --work-home when the match is
unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
still owes a public reply bound to main/<key>, since that binding no longer
names the home owning the work. The move itself is never blocked.
Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
* docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456)
* fix(skills): hint that remote secondmate liveness verdicts false-negative
fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.
* no-mistakes: apply CI fixes
* fix(calm): keep Pi's export confirmation visible (#2461)
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.
Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.
The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
* feat(stow): add open-record persistence to /stow before reset (#2488)
* feat(stow): persist the open records a session is holding
/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declare…
kuan0808
added a commit
to kuan0808/firstmate
that referenced
this pull request
Sep 2, 2026
tests/fm-composer-lib.test.sh fails on stock macOS /bin/bash 3.2 with "a half-block rule row must count as a structural edge". The classifier is fine: bin/fm-composer-lib.sh already treats the upper and lower half-block rows as edges. The fixture wrote those glyphs as \u escapes inside printf and $'...', and Bash 3.2 has no \u escape (added in 4.2), so the classifier received the literal escape text and no edge matched. Linux CI runs Bash 5, which is why this merged green in kunchenguid#2238. - Write the half-block fixture with literal UTF-8 glyphs, matching every other fixture in the file, and say why in a comment. - Run tests/fm-composer-lib.test.sh in the existing macos-stock-bash CI job under real /bin/bash 3.2, so a Bash 4+-only fixture cannot land again. No classifier behavior changes.
kuan0808
added a commit
to kuan0808/firstmate
that referenced
this pull request
Sep 2, 2026
tests/fm-composer-lib.test.sh fails on stock macOS /bin/bash 3.2 with "a half-block rule row must count as a structural edge". The classifier is fine: bin/fm-composer-lib.sh already treats the upper and lower half-block rows as edges. The fixture wrote those glyphs as \u escapes inside printf and $'...', and Bash 3.2 has no \u escape (added in 4.2), so the classifier received the literal escape text and no edge matched. Linux CI runs Bash 5, which is why this merged green in kunchenguid#2238. - Write the half-block fixture with literal UTF-8 glyphs, matching every other fixture in the file, and say why in a comment. - Run tests/fm-composer-lib.test.sh in the existing macos-stock-bash CI job under real /bin/bash 3.2, so a Bash 4+-only fixture cannot land again. No classifier behavior changes.
lytv
pushed a commit
to lytv/mymate
that referenced
this pull request
Sep 8, 2026
* 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>
cipherholdingsllc
added a commit
to cipherlab-ai/firstmate
that referenced
this pull request
Sep 11, 2026
…w scoring (#7) * fix(cmux): classify borderless Claude composers (#2029) * fix(cmux): classify borderless Claude composer * no-mistakes(review): Normalize cmux NBSP prompts across locales * no-mistakes(document): Document cmux borderless Claude composer classification * docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065) * 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: measure Herdr automation on Windows runners (#2100) * 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 * feat(ahoy): guide captains through open decisions (#2099) * Add guided ahoy decision flow * no-mistakes(document): Document guided Ahoy decision flow * fix(stow): enforce startup-memory budget decisions (#2110) * Harden stow memory budget policy * Refine internal stow offload policy * no-mistakes(review): Enforce shared-budget decisions and autonomous offload * fix(spawn): refresh pooled worktrees from origin before launch (#2116) * fix(spawn): refresh pooled worktree base * no-mistakes(document): Document spawn base-freshness invariant * no-mistakes: apply CI fixes * fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted; naming converges with PR #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 TUI mode by CLI capability (#2117) * 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 * docs(vision): elevate experience, pain narrative, and distro virtues (#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. * feat(bin): reconcile inactive terminal crew outcomes (#2167) * 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 * ci: raise Herdr test timeout (#2191) * fix: refresh stale Pi instructions after compaction (#2163) * 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: add deterministic condition-to-action watcher (#2200) * 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 * fix(bin): honor a decision key stated after the verb colon (#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 #2109 * fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206) * 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: parse decision verbs before status metadata tags (#2280) * 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(bin): collapse duplicate supervision wakes (#2287) * 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: add Cursor CLI crew harness (#2238) * 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(bin): require quota-axi 0.1.25 (#2300) * fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness Homes on latest main need quota-axi #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 * fix(bin): prevent false Pi watcher alarms during hand-offs (#2304) * fix(guard): stop the false send-time watcher-down alarm on Pi primaries On a Pi primary the watcher process is not the liveness signal. The Pi extension tears the watcher down on every actionable wake and spawns the replacement itself, so the singleton lock is legitimately unheld between cycles: every one of the 799 cycles in a live primary's ledger ends with lock_after=pid:none, and a live capture caught the guard verdict flipping to no-watcher during one hand-off with the beacon 63s old. bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a live identity-matched lock holder at all times, so any guarded command landing in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and told firstmate to repair a cycle the extension already owns and is restoring. Add an extension supervision model for pi and pi-signed. A live identity-matched watcher stays the ordinary healthy state; an unheld lock is healthy only while the beacon is fresh within grace AND a live Pi session provably owns continuity - both primary extensions recorded in their state markers at their current on-disk builds by the process named in state/.lock, with that process still alive. Without that proof the banner fires exactly as before, so an unloaded, version-drifted, or exited Pi session is loud immediately and a cycle the extension never restores is loud once the beacon passes grace. The queued-wake warning, the PID-strict turn-end guard, and every other primary's detection are untouched. Fold session-start's duplicate Pi marker predicate into the shared library so the ownership contract has one owner. * no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks * no-mistakes(document): Document Pi watcher hand-off supervision * feat: support Cursor Agent CLI as a primary harness (#2305) * feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start Register a tracked project-scope .cursor/hooks.json for Cursor's stop, sessionStart, preCompact, and preToolUse steps. bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it foregrounds the watcher arm, holds the boundary open until an actionable close, and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's stop step, so the adapter never uses it. The follow-up loop is bounded twice, by Cursor's own loop_limit and by the payload's loop_count. bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at sessionStart, and stages it for the next turn boundary at preCompact, which cannot inject context. Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload rather than running every covered event twice. bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because Cursor parks its terminal cursor outside the composer, which restores a genuine composer-empty proof and unblocks away-mode escalation delivery. * feat(cursor): make Cursor Agent CLI a verified primary harness Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which a Cursor primary needs before it can hold its own home lock, and classify its stop-hook park under the autoarm supervision model so the mid-turn pull guard stops reporting a healthy between-turns watcher as down. Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own structural process identity, which restores a genuine composer-empty proof and lets away-mode escalations reach a Cursor primary with no daemon change. Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now that the supervision protocol exists and is recorded. Cover the whole surface with a portable regression over real processes, an opt-in live guard against the installed cursor-agent, and dated per-harness evidence. * docs(cursor): record Cursor as a verified primary across the owning surfaces Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher continuity, architecture, configuration, README, and harness-adapters owners, and add dated live evidence to the supervision and runtime-backend verification records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored read is still blind, but the composite reader is no longer unknown. Lift the remaining remote-secondmate refusal missed in the previous commit, and add the new libs to the existing fixtures that copy a fixed dependency list. * refactor(cursor): name the park's stand-down condition for both its causes Also record that Cursor's preCompact firing itself is not yet live-verified, while the static evidence that it cannot inject context, and the staging path that follows from it, both are. * test: give the pretool fixtures their new dependency and one lint owner The cd-guard fixture copies a fixed dependency list and now needs the shared hook-host predicate. Both pretool suites also asserted cleanliness with a bare shellcheck call, a second and weaker copy of the lint definition that bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these checkers sourced a shared library. They now delegate to that owner. * test: assert the cursor secondmate contract instead of its removed refusal A cursor secondmate now launches, so the suite asserts what its park actually needs: --trust so the home's project hooks load at all, its own home pinned as the workspace, and the autoarm supervision model inherited across the launch. * no-mistakes(review): Serialize Cursor wakes and bind staged context * no-mistakes(review): Serialize Cursor context and nag state commits * no-mistakes(review): Enforce Cursor ceiling before staged context delivery * no-mistakes(review): Serialize Cursor claims and staged context * no-mistakes(review): Serialize Cursor ownership and state commits * no-mistakes(review): Protect Cursor context across session takeover * no-mistakes(review): Preserve Cursor context across session takeover * no-mistakes(review): Enforce owner-keyed Cursor staged context * no-mistakes(review): Atomically claim Cursor follow-ups and staged context * no-mistakes(review): Defer Cursor preCompact staging and simplify supersession * no-mistakes(review): Serialize Cursor park commits and defer preCompact * no-mistakes(review): Stop Cursor parks after session takeover * no-mistakes(test): Route Cursor preCompact context through stop follow-up * no-mistakes(document): Update Cursor primary documentation * revert(cursor): cut preCompact staging from this change Carrying a compaction digest across two concurrently running stop hooks kept producing races that could deliver it twice or strand it indefinitely, and closing them kept enlarging a critical section inside a hook Cursor awaits at the turn boundary. Native preCompact firing was never observed either, so the surface has no empirical basis yet. Remove the adapter, its registration, its staged path in the park, and its tests, and record the surface as deferred and uncovered alongside the Codex interactive TUI. A regression now asserts preCompact stays unregistered so it cannot return without its own design and evidence. This change ships the proven core only: the turn-end follow-up park, the run-tier session start, and away-mode delivery. * no-mistakes(review): Correct Cursor park supersession documentation * no-mistakes(document): Clarify Cursor run-tier verification ownership * no-mistakes: apply CI fixes * no-mistakes: apply CI fixes --------- Co-authored-by: kunchenguid <kun-1@kunchenguid.com> * feat(bin): add decline and repair paths for decision holds (#2330) * feat(bin): add unrouted close paths to the captain decision gate A captain who declines a held decision leaves no follow-up work to route, so `resolve` could not express that answer: it requires at least one `--routed-to` task. The only way to close such a hold was a direct `tasks-axi done`, which never writes the durable resolution record the completion gate reads, so the originating investigation could no longer pass `verify` and its cleanup stayed blocked. Add two close paths that route no work: - `decline` closes an actively held hold with a recorded captain decision and no routed task. It refuses while any task is still blocked by the hold, because releasing routed work without recording it is `resolve`'s job. - `repair` records the missing resolution block on a hold that was already closed outside this script. It never reopens a hold and never clears a dependency edge, and it refuses a hold that is still actively held. Both require a non-empty captain decision file and share `resolve`'s digest-based retry identity, so an exact retry is idempotent while a changed decision is rejected. The recorded body now also names which path closed the hold, and each routed entry regains its own line. The gate itself is unchanged: an unanswered decision still fails completion and blocks teardown, and neither new path can close a hold without the captain's recorded word. * fix(bin): require captain-hold provenance before repairing a decision `repair` checked only that the backlog item was kind captain and Done, so an ordinary captain-kind task that was never held for the captain could be closed, repaired, and then pass the completion gate. tasks-axi keeps `hold_kind` through a close, so it is the surviving proof that an identity really was a captain hold. Require it before writing the resolution record, and cover the case in the gate regression. * no-mistakes(document): Correct decision-hold lifecycle documentation * fix(bin): surface buried wake status lines once (#2331) * fix(bin): surface buried status notes on wake drain A note: answer immediately followed by a routine note was dropped because annotations kept only the newest line and note: never enters OPEN DECISIONS. Present every unread note and pending-reply resolution since the last drain cursor, and annotate every unread line on a queued signal. * no-mistakes(review): Fix unread status cursor races and overflow * no-mistakes(review): Preserve cursors when status span reads fail * no-mistakes(review): Make status presentation transactional under I/O failures * no-mistakes(review): Simplify unread status cursor and presentation locking * no-mistakes(review): Align cursor failure regressions with transactional presentation * no-mistakes(review): Retire stale presentation cursors during task teardown * no-mistakes(review): Preserve routine status until signal annotation * no-mistakes(review): Correct unread status cap documentation * no-mistakes(document): Document unread wake status presentation * no-mistakes(lint): Fix wake surfacing ShellCheck warnings * no-mistakes: apply CI fixes * feat: add max Calm presentation level (#2334) * feat(calm): add a max presentation level that hides mid-turn working notes Calm's home-local preference becomes a three-state level instead of a boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus hiding the assistant text of messages the model did not end its response with. `/calm max` selects it from any state, a plain `/calm` steps max back to ordinary Calm and otherwise keeps the existing on/off cycle, and any other argument keeps that cycle too. `config/calm` now persists "max" as its own literal value, so a session start, resume, fork, or reload restores the stored level rather than treating it as unrecognized and dropping to off. The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse", or "length" with tool calls present. Streaming ("pending") text is never filtered, because suppressing it would also stop a genuine reply from streaming. The existing assistant layout adapter filters the blocks out of the same shallow presentation copy it already uses for collapsed thinking, so the message, model context, session storage, /export, and delivery are untouched and a hidden mid-turn row collapses to zero height. The new "assistant-working-note" class keeps that choice in the visibility policy owner, where ordinary Calm keeps it visible. * no-mistakes(document): Clarify Calm max persistence and taxonomy * feat(calm): hide mid-turn working notes by default (#2339) * feat(calm): make hiding mid-turn working notes the ordinary Calm state Calm collapses back to the two-state on/off toggle it was before the max presentation level, with max's hide rule promoted into ordinary Calm. Calm on now hides mid-turn assistant working notes in addition to what it already hid, and the /calm command parses no argument again. The hide rule itself is unchanged: assistant text is removed from the shallow presentation copy when the message's own stopReason is "toolUse", or "length" with tool calls present. Streaming ("pending") text is never filtered, so a genuine reply still streams. The message, model context, session storage, /export, and delivery remain untouched. config/calm persists only "on" and "off" again, but the reader still maps a persisted "max" to on so a home upgraded from the removed level keeps Calm on instead of dropping to off. The mid-turn hide is now default behavior rather than an opt-in level, so docs/calm.md documents it for users, docs/configuration.md records the two written values plus the legacy max mapping, and the feasibility taxonomy drops its level-scoped wording. * no-mistakes(document): Document ordinary Calm working-note hiding * chore: store no-mistakes test evidence in the repo (#2355) * chore: ignore scratchpad/ at the repo root (#2359) * fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413) A wedged family-run step was occupying the runner until the 75-minute job cap; bound that step so cleanup and timing artifacts still upload. * fix: keep the public promise reachable when work is routed to a second mate (#2457) The lightweight Relay follow-up link lives in the answering home's own state/<task-id>.meta, so it can only bind work that home owns. When a Relay-linked request is routed to a second mate, the task record lives in the second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and nothing else picked the promise up: only the soft acknowledgement was ever posted. The typed promised-final path already supports --work-home secondmate:<id>; the playbook simply never chose it. - fmx-respond now states the routing rule crisply: a task in this home takes the lightweight link, and second-mate-routed work takes a promised-final commitment bound to that home, registered up front with the brief command carried into the routed worker's instructions. - fm-x-link.sh refuses a task with no local record by naming the registered second mate whose home actually holds it and printing the promised-final registration command, with the exact --work-home when the match is unambiguous. A home with no registered second mates keeps the plain error. - fm-backlog-handoff.sh reports, after a successful move, any moved key that still owes a public reply bound to main/<key>, since that binding no longer names the home owning the work. The move itself is never blocked. Docs and the secondmate handoff prose follow the same rule. Tests cover the refusal, its scoping, the unchanged local-link path, and both handoff outcomes at the script boundary. * docs(skills): add remote-secondmate recovery hint for false-negative verdicts (#2456) * fix(skills): hint that remote secondmate liveness verdicts false-negative fm-crew-state and fm-send routinely misreport a live remote secondmate as dead; confirm against the pane before relaunching, and relaunch only through fm-spawn.sh, never raw herdr pane surgery. * no-mistakes: apply CI fixes * fix(calm): keep Pi's export confirmation visible (#2461) Pi 0.83.0 added a status line to every tool-expansion change, and Pi updates the previous status line in place when two status messages arrive back to back. Calm's post-export redraw cycled tool expansion on the macrotask right after Pi printed "Session exported to: <path>", so both expansion status lines coalesced over that confirmation and the captain was left with no record of where their export landed. Calm now repaints only the tool rows it presents, by invalidating each row through the render context Pi hands its render slots, and requests the surrounding redraw through setStatus. Neither appends to the transcript. The repaint is still needed because Pi can re-render a row asynchronously - the built-in edit row invalidates itself once its diff is ready - and that re-render can land inside the window where /export forces stock rendering. The real-terminal /export case now asserts the confirmation is still on screen after the redraw has settled, and that the redraw restored every Calm-hidden row, instead of only racing the moment the confirmation first appeared. * feat(stow): add open-record persistence to /stow before reset (#2488) * feat(stow): persist the open records a session is holding /stow curated memory and captured session knowledge, but never touched record state, while AGENTS.md called it an "unfinished-work sweep" and the receipt declared the session "safe to reset" - wording that implied a record-correctness guarantee stow does not make. A shipped PR with no backlog item, a queued umbrella whose phases had merged, and four decision holds left open after their answers shipped all survived repeated stows. Add a bounded pass that files record state from the same volatile input the rest of stow already uses: the open threads in context, minutes before the reset destroys them. It creates a record for an unfiled thread and corrects one the session knows is wrong, through the owning path, and states its boundary as part of the contract - it never enumerates the backlog, lists holds, or queries a forge, because it cannot be a reconciliation and must not be read as one. Correct the wording in AGENTS.md and the completion receipt so reset-safe means what it actually guarantees: nothing this session knew was lost. * no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi * no-mistakes(document): note /stow open-record persistence in README command catalog * refactor(stow): state open-record persistence as principle, not procedure The first version enumerated triggers, named commands, and prescribed an ordered procedure. That is too rigid for an agent skill: it invites literal execution of a checklist instead of judgment, and every enumerated example is a way for the guidance to go stale. Reduce it to the intent - before a reset, the important open work you are holding in context must end up durably recorded rather than dying with the session, filing what is unfiled and correcting what is stale - and let the agent judge importance, the record, and the owning write path. Keep the scope bound, since it is a decided contract and not a mechanic: this covers the open work the session is holding, never a reconciliation of durable records against repository or forge reality. The wording corrections in AGENTS.md and the completion receipt are unchanged. * fix(decisions): close decision holds at answer time via one general keyed-answer path (#2490) * fix(decisions): close captain holds at answer time Firstmate had two "a decision is open" ledgers with asymmetric closing mechanics. The live status-log ledger closes atomically at answer time, because bin/fm-send.sh --resolve-key makes answering a decision be the act that closes it. The durable backlog hold ledger had no such coupling: answering and recording were two separate acts, and only the first was forced by the workflow. That asymmetry lost four real captain decisions. Their answers were captured durably to disk, keyed character for character by the hold decision keys, acknowledged, and even implemented and shipped, yet the holds stayed open for two days and the captain was asked to re-answer decisions already on his own disk. Give the hold ledger the same answer-time-closure property: - bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's counterpart to --resolve-key. It shares one unrouted close implementation with `decline`, so it carries every existing guard - the captain decision file, the active-hold requirement, retry identity, and the refusal to release still-routed work - and differs only in the resolution mode it records. `decline` keeps its stronger meaning that the answer routes no follow-up work at all. - bin/fm-procevent-lavish.sh wires the channel that actually carried the lost answers. `arm --decisions-origin` binds a deck to the origin whose holds it carries, `answers` reads the structured choices out of a captured poll result, `close-decisions` maps each key to its hold and closes it through the command above, and `autohandle` lets the runner apply that at capture time. Safety is preserved rather than traded away. Only rows tagged `choice` are read, so freeform captain prose cannot forge a decision key. Closure is confined to the one bound origin. The decision text is a pure function of the captured result, so a replayed capture is idempotent. A hold that is absent, already closed, or still blocking routed work is skipped and left for `resolve`, never forced. A deck armed without the binding touches no hold at all. And autohandle deliberately never reports full handling, because recording an answer is transcription while acting on it is firstmate's judgement - so the check wake still reaches the handler. fm-send --resolve-key is untouched. * no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory * refactor(decisions): make keyed-answer closure one general capability The previous pass gave holds answer-time closure but built it as bespoke Lavish wiring: the review adapter carried the source-to-origin binding, mapped keys to hold identities, wrote decision records, decided what to skip, and closed holds itself. That treated a review deck as a special decision source. It is not - it is an ephemeral discussion format that happens to carry answers. Collapse it into ONE general capability with one owner. bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its matching hold": - `answers <origin> --source <provenance>` is the channel-agnostic intake. It reads key/answer/label lines on stdin, maps each key to its hold, and closes it through the same `answer` path, so every guard applies identically whatever channel the answer came from. --source is provenance recorded in the decision, never a behavior switch; there is no per-channel branch and no knowledge of chat, decks, or transports. - `bind`/`unbind`/`binding` own the source-to-origin binding for any channel whose answers arrive detached from their origin. Every channel is now an ordinary caller that only turns what it received into keyed lines: - bin/fm-send.sh (chat) feeds the intake for a key that names an active hold. This also fixes a real gap: once `complete` transfers a decision to its hold it closes the live status copy, so --resolve-key alone could never answer a transferred decision. - bin/fm-procevent.sh feeds it generically. A bound source's captured result goes to `<adapter> answers <result-file>` and whatever that prints is piped into the intake. The runner names no adapter, parses no result, and carries no decision rule, so any future adapter with an `answers` command works with no change here. - bin/fm-procevent-lavish.sh keeps only `answers`, which reports the structured choices a review captured and stops. It maps nothing to a hold and closes nothing; it lost ~160 lines of decision logic. Feeding is independent of handling, so it never acknowledges a result and never suppresses a wake - recording an answer is transcription, acting on it stays firstmate's judgement. The regression that proves closure now drives a FIXTURE adapter that is not the review adapter, so what is proven is that any bound channel reaches the intake rather than that one channel is wired specially. A new regression drives the real fm-send over a stubbed transport for the chat side. Every prior guarantee still holds, and fm-send's status-log behavior is unchanged. * no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression * fix(memory): emit a real @AGENTS.md pointer instead of a CLAUDE.md symlink (#2512) A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md. The installer now creates and migrates to a recoverable two-line pointer file. * fix(ci): keep CLAUDE.md pointer check valid (#2515) * ci: gate GitHub workflows with pinned actionlint (#2517) * fix(lint): catch malformed GitHub workflows before merge A self-broken ci.yml cannot report its own breakage, so parse every workflow in the local lint path that no-mistakes already runs. * fix(lint): pin actionlint instead of Ruby for workflow lint A self-broken ci.yml still has to fail in the local lint path, and the named tool for that gate is actionlint, not a new Ruby runtime. * no-mistakes(document): Clarify pinned workflow lint documentation * fix: install pinned lint tools across supported platforms (#2546) * fix: install pinned shellcheck and actionlint on macOS and linux arm64 The installers were hardcoded to linux amd64 and sha256sum, so a Mac dev could not satisfy the refuse-on-mismatch lint gate. Select the official per-platform archive and checksum, and fall back to shasum -a 256. * no-mistakes(document): Document cross-platform pinned lint installers * docs: reconcile test-evidence docs with store_in_repo: true (#2548) .no-mistakes.yaml has set test.evidence.store_in_repo: true since #2355, but CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described the old policy of keeping evidence out of the repo in a temp directory. The current no-mistakes behavior for store_in_repo: true is to publish each run's test evidence to the orphan no-mistakes/evidence branch and link it from the PR body. That branch shares no history with code branches, so evidence never enters a pushed feature branch or the default branch, and CI's tracked personal fleet paths rule stays accurate. Docs only. No change to .no-mistakes.yaml or any workflow. * docs: clarify test evidence branch storage (#2549) * docs: correct test evidence storage comment in .no-mistakes.yaml * no-mistakes: apply CI fixes * docs: hint that live scouts may host their own Lavish review loop (#2563) Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds. * fix(bin): report remote secondmate delivery and state truthfully (#2570) * fix(bin): report remote secondmate delivery and state truthfully A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send leg whose unconfirmed submit read-back (verdict=pending, typically a busy mate whose harness queues the steer) was flattened into exit 1, so the parent printed "error: text not submitted" / "error: text not sent" and discarded the …
Alchemy86
added a commit
to Alchemy86/firstmate
that referenced
this pull request
Sep 14, 2026
* fix(remote-job): stop workers abandoned by a pruned code root (#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.
* feat(bin): lint only the changed shard locally, full lint in CI (#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 (#1568)
* 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): add tiered decaying memory management (#1984)
* 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 project vision (#1997)
* 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 (#2005)
* fix(spawn): force regular Pi TUI for crews
* no-mistakes(document): Documented Pi regular TUI launch mode
* fix(cmux): classify borderless Claude composers (#2029)
* fix(cmux): classify borderless Claude composer
* no-mistakes(review): Normalize cmux NBSP prompts across locales
* no-mistakes(document): Document cmux borderless Claude composer classification
* docs(stow): generalize read-before-write in the public stow skill (#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: resurface durable supervision work after re-arm (#2065)
* 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: measure Herdr automation on Windows runners (#2100)
* 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
* feat(ahoy): guide captains through open decisions (#2099)
* Add guided ahoy decision flow
* no-mistakes(document): Document guided Ahoy decision flow
* fix(stow): enforce startup-memory budget decisions (#2110)
* Harden stow memory budget policy
* Refine internal stow offload policy
* no-mistakes(review): Enforce shared-budget decisions and autonomous offload
* fix(spawn): refresh pooled worktrees from origin before launch (#2116)
* fix(spawn): refresh pooled worktree base
* no-mistakes(document): Document spawn base-freshness invariant
* no-mistakes: apply CI fixes
* fix(composer): unify safe classification across backends (#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 #1988; cmux's local byte-exact NBSP case deleted;
naming converges with PR #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 TUI mode by CLI capability (#2117)
* 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
* docs(vision): elevate experience, pain narrative, and distro virtues (#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.
* feat(bin): reconcile inactive terminal crew outcomes (#2167)
* 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
* ci: raise Herdr test timeout (#2191)
* fix: refresh stale Pi instructions after compaction (#2163)
* 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: add deterministic condition-to-action watcher (#2200)
* 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
* fix(bin): honor a decision key stated after the verb colon (#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 #2109
* fix(bin): prevent watcher recovery acknowledgement livelock (#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 Relay conversation chains (#2206)
* 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: parse decision verbs before status metadata tags (#2280)
* 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(bin): collapse duplicate supervision wakes (#2287)
* 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: add Cursor CLI crew harness (#2238)
* 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(bin): require quota-axi 0.1.25 (#2300)
* fix: raise quota-axi floor to 0.1.25 for Cursor CLI quota awareness
Homes on latest main need quota-axi #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
* fix(bin): prevent false Pi watcher alarms during hand-offs (#2304)
* fix(guard): stop the false send-time watcher-down alarm on Pi primaries
On a Pi primary the watcher process is not the liveness signal. The Pi
extension tears the watcher down on every actionable wake and spawns the
replacement itself, so the singleton lock is legitimately unheld between
cycles: every one of the 799 cycles in a live primary's ledger ends with
lock_after=pid:none, and a live capture caught the guard verdict flipping to
no-watcher during one hand-off with the beacon 63s old.
bin/fm-guard.sh classified Pi as a persistent-watcher harness, which demands a
live identity-matched lock holder at all times, so any guarded command landing
in a hand-off painted the full WATCHER DOWN - SUPERVISION IS OFF banner and
told firstmate to repair a cycle the extension already owns and is restoring.
Add an extension supervision model for pi and pi-signed. A live
identity-matched watcher stays the ordinary healthy state; an unheld lock is
healthy only while the beacon is fresh within grace AND a live Pi session
provably owns continuity - both primary extensions recorded in their state
markers at their current on-disk builds by the process named in state/.lock,
with that process still alive. Without that proof the banner fires exactly as
before, so an unloaded, version-drifted, or exited Pi session is loud
immediately and a cycle the extension never restores is loud once the beacon
passes grace. The queued-wake warning, the PID-strict turn-end guard, and
every other primary's detection are untouched.
Fold session-start's duplicate Pi marker predicate into the shared library so
the ownership contract has one owner.
* no-mistakes(review): Restrict Pi hand-off tolerance to unheld watcher locks
* no-mistakes(document): Document Pi watcher hand-off supervision
* feat: support Cursor Agent CLI as a primary harness (#2305)
* feat(cursor): add Cursor Agent CLI primary hooks, park supervision, and session start
Register a tracked project-scope .cursor/hooks.json for Cursor's stop,
sessionStart, preCompact, and preToolUse steps.
bin/fm-turnend-guard-cursor.sh owns Cursor's turn boundary as a park: it
foregrounds the watcher arm, holds the boundary open until an actionable close,
and returns that wake as one follow-up. Exit 2 is a silent no-op on Cursor's
stop step, so the adapter never uses it. The follow-up loop is bounded twice,
by Cursor's own loop_limit and by the payload's loop_count.
bin/fm-sessionstart-cursor.sh delivers the digest as additional_context at
sessionStart, and stages it for the next turn boundary at preCompact, which
cannot inject context.
Cursor also loads the tracked Claude settings, so bin/fm-hook-host-lib.sh lets
each tracked Claude-shaped entrypoint stand down on a Cursor-delivered payload
rather than running every covered event twice.
bin/fm-tmux-lib.sh reclassifies a Cursor pane's composer cursorlessly, because
Cursor parks its terminal cursor outside the composer, which restores a genuine
composer-empty proof and unblocks away-mode escalation delivery.
* feat(cursor): make Cursor Agent CLI a verified primary harness
Resolve Cursor in the session-lock ancestry through bin/fm-cursor-lib.sh, which
a Cursor primary needs before it can hold its own home lock, and classify its
stop-hook park under the autoarm supervision model so the mid-turn pull guard
stops reporting a healthy between-turns watcher as down.
Read a Cursor pane's composer cursorlessly on tmux, gated on Cursor's own
structural process identity, which restores a genuine composer-empty proof and
lets away-mode escalations reach a Cursor primary with no daemon change.
Lift the secondmate refusals in bin/fm-spawn.sh and bin/fm-control-lib.sh now
that the supervision protocol exists and is recorded.
Cover the whole surface with a portable regression over real processes, an
opt-in live guard against the installed cursor-agent, and dated per-harness
evidence.
* docs(cursor): record Cursor as a verified primary across the owning surfaces
Update the turn-end guard, session-start, arm-seatbelt, cd-guard, watcher
continuity, architecture, configuration, README, and harness-adapters owners,
and add dated live evidence to the supervision and runtime-backend verification
records. Correct the recorded Cursor tmux composer verdict: the cursor-anchored
read is still blind, but the composite reader is no longer unknown.
Lift the remaining remote-secondmate refusal missed in the previous commit, and
add the new libs to the existing fixtures that copy a fixed dependency list.
* refactor(cursor): name the park's stand-down condition for both its causes
Also record that Cursor's preCompact firing itself is not yet live-verified,
while the static evidence that it cannot inject context, and the staging path
that follows from it, both are.
* test: give the pretool fixtures their new dependency and one lint owner
The cd-guard fixture copies a fixed dependency list and now needs the shared
hook-host predicate. Both pretool suites also asserted cleanliness with a bare
shellcheck call, a second and weaker copy of the lint definition that
bin/fm-lint.sh owns: it omits --external-sources, so it failed the moment these
checkers sourced a shared library. They now delegate to that owner.
* test: assert the cursor secondmate contract instead of its removed refusal
A cursor secondmate now launches, so the suite asserts what its park actually
needs: --trust so the home's project hooks load at all, its own home pinned as
the workspace, and the autoarm supervision model inherited across the launch.
* no-mistakes(review): Serialize Cursor wakes and bind staged context
* no-mistakes(review): Serialize Cursor context and nag state commits
* no-mistakes(review): Enforce Cursor ceiling before staged context delivery
* no-mistakes(review): Serialize Cursor claims and staged context
* no-mistakes(review): Serialize Cursor ownership and state commits
* no-mistakes(review): Protect Cursor context across session takeover
* no-mistakes(review): Preserve Cursor context across session takeover
* no-mistakes(review): Enforce owner-keyed Cursor staged context
* no-mistakes(review): Atomically claim Cursor follow-ups and staged context
* no-mistakes(review): Defer Cursor preCompact staging and simplify supersession
* no-mistakes(review): Serialize Cursor park commits and defer preCompact
* no-mistakes(review): Stop Cursor parks after session takeover
* no-mistakes(test): Route Cursor preCompact context through stop follow-up
* no-mistakes(document): Update Cursor primary documentation
* revert(cursor): cut preCompact staging from this change
Carrying a compaction digest across two concurrently running stop hooks kept
producing races that could deliver it twice or strand it indefinitely, and
closing them kept enlarging a critical section inside a hook Cursor awaits at
the turn boundary. Native preCompact firing was never observed either, so the
surface has no empirical basis yet.
Remove the adapter, its registration, its staged path in the park, and its
tests, and record the surface as deferred and uncovered alongside the Codex
interactive TUI. A regression now asserts preCompact stays unregistered so it
cannot return without its own design and evidence.
This change ships the proven core only: the turn-end follow-up park, the
run-tier session start, and away-mode delivery.
* no-mistakes(review): Correct Cursor park supersession documentation
* no-mistakes(document): Clarify Cursor run-tier verification ownership
* no-mistakes: apply CI fixes
* no-mistakes: apply CI fixes
---------
Co-authored-by: kunchenguid <kun-1@kunchenguid.com>
* feat(bin): add decline and repair paths for decision holds (#2330)
* feat(bin): add unrouted close paths to the captain decision gate
A captain who declines a held decision leaves no follow-up work to route,
so `resolve` could not express that answer: it requires at least one
`--routed-to` task. The only way to close such a hold was a direct
`tasks-axi done`, which never writes the durable resolution record the
completion gate reads, so the originating investigation could no longer
pass `verify` and its cleanup stayed blocked.
Add two close paths that route no work:
- `decline` closes an actively held hold with a recorded captain decision
and no routed task. It refuses while any task is still blocked by the
hold, because releasing routed work without recording it is `resolve`'s
job.
- `repair` records the missing resolution block on a hold that was already
closed outside this script. It never reopens a hold and never clears a
dependency edge, and it refuses a hold that is still actively held.
Both require a non-empty captain decision file and share `resolve`'s
digest-based retry identity, so an exact retry is idempotent while a
changed decision is rejected. The recorded body now also names which path
closed the hold, and each routed entry regains its own line.
The gate itself is unchanged: an unanswered decision still fails
completion and blocks teardown, and neither new path can close a hold
without the captain's recorded word.
* fix(bin): require captain-hold provenance before repairing a decision
`repair` checked only that the backlog item was kind captain and Done, so
an ordinary captain-kind task that was never held for the captain could be
closed, repaired, and then pass the completion gate.
tasks-axi keeps `hold_kind` through a close, so it is the surviving proof
that an identity really was a captain hold. Require it before writing the
resolution record, and cover the case in the gate regression.
* no-mistakes(document): Correct decision-hold lifecycle documentation
* fix(bin): surface buried wake status lines once (#2331)
* fix(bin): surface buried status notes on wake drain
A note: answer immediately followed by a routine note was dropped because
annotations kept only the newest line and note: never enters OPEN DECISIONS.
Present every unread note and pending-reply resolution since the last drain
cursor, and annotate every unread line on a queued signal.
* no-mistakes(review): Fix unread status cursor races and overflow
* no-mistakes(review): Preserve cursors when status span reads fail
* no-mistakes(review): Make status presentation transactional under I/O failures
* no-mistakes(review): Simplify unread status cursor and presentation locking
* no-mistakes(review): Align cursor failure regressions with transactional presentation
* no-mistakes(review): Retire stale presentation cursors during task teardown
* no-mistakes(review): Preserve routine status until signal annotation
* no-mistakes(review): Correct unread status cap documentation
* no-mistakes(document): Document unread wake status presentation
* no-mistakes(lint): Fix wake surfacing ShellCheck warnings
* no-mistakes: apply CI fixes
* feat: add max Calm presentation level (#2334)
* feat(calm): add a max presentation level that hides mid-turn working notes
Calm's home-local preference becomes a three-state level instead of a
boolean: "off" is stock Pi, "on" is today's Calm, and "max" is Calm plus
hiding the assistant text of messages the model did not end its response
with. `/calm max` selects it from any state, a plain `/calm` steps max
back to ordinary Calm and otherwise keeps the existing on/off cycle, and
any other argument keeps that cycle too.
`config/calm` now persists "max" as its own literal value, so a session
start, resume, fork, or reload restores the stored level rather than
treating it as unrecognized and dropping to off.
The hide rule keys on Pi's intrinsic per-message stopReason: "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, because suppressing it would also stop a genuine reply from
streaming. The existing assistant layout adapter filters the blocks out
of the same shallow presentation copy it already uses for collapsed
thinking, so the message, model context, session storage, /export, and
delivery are untouched and a hidden mid-turn row collapses to zero
height. The new "assistant-working-note" class keeps that choice in the
visibility policy owner, where ordinary Calm keeps it visible.
* no-mistakes(document): Clarify Calm max persistence and taxonomy
* feat(calm): hide mid-turn working notes by default (#2339)
* feat(calm): make hiding mid-turn working notes the ordinary Calm state
Calm collapses back to the two-state on/off toggle it was before the max
presentation level, with max's hide rule promoted into ordinary Calm.
Calm on now hides mid-turn assistant working notes in addition to what it
already hid, and the /calm command parses no argument again.
The hide rule itself is unchanged: assistant text is removed from the
shallow presentation copy when the message's own stopReason is "toolUse",
or "length" with tool calls present. Streaming ("pending") text is never
filtered, so a genuine reply still streams. The message, model context,
session storage, /export, and delivery remain untouched.
config/calm persists only "on" and "off" again, but the reader still maps
a persisted "max" to on so a home upgraded from the removed level keeps
Calm on instead of dropping to off.
The mid-turn hide is now default behavior rather than an opt-in level, so
docs/calm.md documents it for users, docs/configuration.md records the
two written values plus the legacy max mapping, and the feasibility
taxonomy drops its level-scoped wording.
* no-mistakes(document): Document ordinary Calm working-note hiding
* chore: store no-mistakes test evidence in the repo (#2355)
* chore: ignore scratchpad/ at the repo root (#2359)
* fix(ci): fail hung Herdr behavior runs in 20 minutes (#2413)
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
* fix: keep the public promise reachable when work is routed to a second mate (#2457)
The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.
- fmx-respond now states the routing rule crisply: a task in this home takes the
lightweight link, and second-mate-route…
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
BUILD Cursor CLI (cursor-agent) as a firstmate CREW harness - crewmate/scout ONLY, never a primary harness. firstmate SHARED, TRACKED material. This is a CONTINUATION run on the same branch and PR (#2238). Two further captain-directed commits are folded into the same ship. Do NOT rebase, do NOT reset, do NOT rewrite existing commit history.
ORIGINAL SCOPE (validated in earlier runs, unchanged): consolidate a crew-scope cursor adapter from 15 open contributor PRs rather than merging any one whole, because none was complete for crew scope, several reached into primary-session territory the captain did not ask for, and CLI drift had invalidated parts of each. Primary-only surface (turn-end guard, session-start nudge, watcher protocol, subagent guard, worker-server reaping) is deliberately excluded.
CREDIT PRESERVATION is a hard captain requirement and is DELIBERATE, not an oversight: PR #1814 (abojja9) is cherry-picked verbatim so original git authorship is preserved, and #1726 (vipentti) and #787 (Amplify-Logic) are credited with Co-authored-by trailers because their diffs could not be cherry-picked (16 and 13 conflicts; #787 predates the shared composer classifier). The captain has EXPLICITLY reviewed and APPROVED the trailer 'Co-authored-by: Amplify Logic AI lars@sockinator.co' and instructed it be kept VERBATIM: it is the real human contributor's own git identity from #787's commits, and the no-AI-co-author rule targets the assistant itself, not a human whose account name contains 'AI'. Do not flag, remove, reword, or rewrite that trailer.
THE TWO NEW COMMITS IN THIS RUN close a captain-directed gap: the captain required cursor to be E2E LIVE-VALIDATED on the HERDR backend too, not just tmux, and when that live run found a real defect the captain chose "fix it" (Option A) over "document it and ship tmux-only". Both commits are herdr-scoped defect fixes proven against a real signed cursor-agent 2026.08.11-e8db854 worker on herdr 0.8.0 in an isolated lab session.
COMMIT 1 (composer): 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 walked straight through its own closing rule and swallowed the model and path footer below it - real text, which forced
pendingon a genuinely idle pane. Teaching the shared edge detector those glyphs bounds the region at the closing rule. This is a GENERAL herdr bare-composer bug, not cursor-specific. Measured as an A/B on the SAME live pane: the pre-fix classifier returns pending, the current one returns empty.COMMIT 2 (herdr submit): the idle fix alone did NOT make delivery confirm, and this is the second, independent cause found by live measurement. A 60-sample probe across a full turn showed 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 placeholder beside a right-aligned
ctrl+c to stop; that token is composer content, so the verdict ispendingon a composer holding no user text, and fm-send exited 1 with "delivery unconfirmed" on messages that had actually landed. The fix confirms the submit from a RENDERED-FOOTER idle-to-busy transition across our Enter - the same semantic signal the native path uses and the same one the tmux submit core already reads - with the footer baseline taken before the first Enter and only when the native baseline was not legibly idle.DELIBERATE DESIGN BOUNDARIES, do not flag as missing work:
pending, so the delivery confirmation can only be coming from the footer transition and never from a softened verdict.pending.REQUIRED, EXPLICIT LIMITATION STATEMENT (captain instruction, must not be silently omitted): cursor delivery confirmation is verified on tmux and herdr ONLY. On zellij, cmux, and orca a cursor steer LANDS but fm-send reports delivery unconfirmed and exits non-zero, because those three share a submit core that never consults the busy footer. That is documented in .agents/skills/harness-adapters/SKILL.md and docs/verification/runtime-backends.md, and follow-up task fm-cursor-shared-submit-busy-transition-a2 owns the shared change and its own live validation.
CAPTAIN DECISIONS already made on this branch, do not re-litigate: cursor is NOT gated to tmux (every harness on every session backend; no backend gate may be added); model ids validate against the live cursor-agent --list-models catalog rather than a hardcoded list, failing open when the catalog cannot be reached; the tmux composer verdict for cursor is permanently 'unknown' BY DESIGN because cursor-agent parks its terminal cursor outside its composer; and cursor's recorded worker state comes only from its transcript fold, never rendered text or native state.
LIVE VALIDATION EVIDENCE for this run is dated 2026-08-12 in docs/verification/runtime-backends.md, section "Cursor Agent CLI" -> "Herdr backend". A real cursor scout was spawned on herdr through bin/fm-spawn.sh and driven end to end: brief executed, transcript fold read busy mid-turn and idle after, fm-send exited 0 with the steer executing in the pane, fm-control interrupt cancelled a running turn, fm-control exit stopped the agent, and fm-teardown refused until the decision gate was satisfied then cleared the record. All seven panes of the running default herdr session - one Pi, four Claude, two plain shells - classify identically before and after the shape fix, so no other harness regressed. All herdr lab work ran through bin/fm-herdr-lab.sh in an isolated fm-lab- session with the live default session's pane count checked before, during, and after; it never changed.
Delivery: mode=no-mistakes, yolo OFF - the captain merges. Homes need a self-update after merge. Escalate any ask-user finding rather than deciding it.
What Changed
fm-sendreports unconfirmed delivery and exits non-zero.Risk Assessment
✅ Low: The Cursor crew adapter is bounded away from primary harness use, required backend limitations are documented, and the Herdr fix now prevents a positively working pre-Enter baseline from using footer confirmation.
Testing
Inspected the targeted changes, verified the installed Herdr 0.8.0 and cursor-agent 2026.08.11-e8db854 versions, exercised focused composer, Herdr, Cursor, spawn, secondmate, control, relaunch, and busy-state tests, and captured a public fm-send transcript showing blocked-state Cursor delivery confirmed through the rendered-footer transition; all checks succeeded and the worktree remained clean.
Evidence: Cursor-on-Herdr fm-send delivery-confirmation transcript
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 1 issue found → auto-fixed ✅
bin/backends/herdr.sh:2776- Contradicts the required invariant: “A pane already mid-turn before the text was typed must NOT confirm from that same busy footer.” A native pre-Enterworkingstatus is collapsed tobusy, but the footer fallback remains enabled. If the initial footer capture occurs before the existing turn renders its token, then that same turn renders it after Enter while the composer remainspending, lines 2785–2787 falsely confirm delivery. Preserve the raw status and allow this fallback for Cursor'sblocked/unreadable baseline only, never a positivelyworkingbaseline.🔧 Fix: Prevent working Herdr baselines from falsely confirming delivery
✅ Re-checked - no issues remain.
✅ **Test** - passed
✅ No issues found.
git diff --stat b91016f2c60fe753b2f7f728f2ed3b7dd61ad34a..590da2a415bd95002e4a131eb353afa52b886aedand focused commit/diff inspectionbash tests/fm-composer-lib.test.shbash tests/fm-backend-herdr.test.shbash tests/fm-cursor-harness.test.shbash tests/fm-spawn-dispatch-profile.test.shbash tests/fm-secondmate-harness.test.shbash tests/fm-control-relaunch.test.shbash tests/fm-control.test.shbash tests/fm-busy-state.test.shChecked installedherdr --versionandcursor-agent --versioncompatibilityExecuted publicbin/fm-send.sh default:w1:p2 'hello captain'against a deterministic Herdr protocol simulation reproducing blocked native state and an idle-to-busy rendered-footer transition; it exited 0 after one Entergit status --shortconfirmed testing left the worktree clean✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.