feat(bin): add fork-main operations and evidence-backed crew state - #3249
tiago-peixoto wants to merge 72 commits into
Conversation
* docs(stow): generalize read-before-write in the public stow skill (kunchenguid#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 (kunchenguid#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 (kunchenguid#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 * fix: support Pi 0.83 worker launches --------- Co-authored-by: Kun Chen <3233006+kunchenguid@users.noreply.github.com>
* 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>
* fix(supervision): align Claude guard with session ownership * no-mistakes(review): Fix guard escalation and evidence fingerprinting * no-mistakes(review): Refresh guard evidence and escalation documentation * no-mistakes(review): Keep queued wake delivery under supervision * no-mistakes(review): Use one supervision snapshot for queue warnings * no-mistakes(document): Align supervision documentation with acknowledged wake recovery * test(ci): bound watcher fixture shutdown * test(ci): bound watcher fixture cleanup
* 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>
fix: restore upstream ancestry in fork main
feat: sync upstream workflow and recovery improvements
…nlint ci: gate GitHub workflows with pinned actionlint
fix(bin): install pinned lint tools across supported platforms
docs: align test evidence guidance with repo storage
docs: clarify test evidence branch storage
Canonical divergence topic for the fork-main integration workflow the personal fork carries beyond official upstream: the guarded remote topology and migration, the isolated fork-target validation clone, the divergence manifest and its health report, the topic integrate and discard helpers, the upstream merge helper, the startup upstream probe, and the supporting brief, seed, self-update and documentation surfaces. Submitted upstream as PR 1944 and retired when that lands.
Canonical divergence topic for the Claude primary-session supervision fix the personal fork carries beyond official upstream: session-ownership alignment for the Stop auto-arm and turn-end guard, a bounded volatile entry trace that distinguishes a hook that never ran from one that took a pre-claim gate, one-shot repeated-block escalation, a single supervision snapshot behind queue warnings, queued delivery treated as a supervision need until post-handling acknowledgement, and the BASH_SUBSHELL lock-ownership check stock macOS Bash 3.2 needs because it has no BASHPID. Submitted upstream as PR 2392 and retired when that lands.
feat: register fork-main integration divergence
chore: register supervision recovery divergence
…ffold-2538 fix: trim generated brief scaffolds
…e-progressive-disclosure feat(fork): sync upstream scout-hosted Lavish reviews
…st-classification feat: support issue-based upstream divergence review
* feat(bin): report reclaimable Next.js build output in pooled worktrees Pooled task copies accumulate Next.js build output that nothing removes. `treehouse return` resets tracked content and leaves gitignored output where it is, so a returned copy keeps it. On 2026-08-06 that contributed to a home where every Bash call failed with ENOSPC for several minutes - nothing ran at all, including df and du, so the problem could not even be measured - and a coverage run was aborted mid-verification. bin/fm-next-cache-sweep.sh inspects pooled copies and reports which hold regenerable build output and how much. It deletes nothing. Deletion is absent by design, not postponed, and both paths were removed for proven reasons: - The sweep cannot prove it owns a pooled copy. Holding a Treehouse lease across validation and deletion was the proposed remedy; either acquisition mutates the copy it hands out, or nobody has shown a lease covers the whole validation-through-deletion window. The proof does not exist today. - Teardown-side reclamation is structurally unprovable under Treehouse's process-bound hold: ownership exists while a shell has its cwd in the worktree, and quietness is proven only when no such process remains. Both are read off the same observable, so no ordering makes both true at once, and exempting the holding shell would exempt a shell that can start a build. Reclamation therefore depends on a task-lifetime durable lease, which would make ownership independent of worktree processes. Until then this is report-only, which is a boundary rather than a verdict about Treehouse. What the report still buys, against the incident above: it names where the gigabytes are without touching them, which is what was missing when the problem could not be measured. Every ownership input must be proven readable and complete before a copy is even reported as assessable; anything unreadable, absent, malformed or indeterminable is recorded as an unassessed verdict rather than skipped. The header carries an input inventory in which each verdict states what was checked and what would falsify it, and a falsifier rule governing how those verdicts are written. Fork divergence: it assumes a pooled-worktree layout and Next.js projects. * no-mistakes(review): Report all cache states and accept absent registries * no-mistakes(review): Preserve valid cache reports beside malformed entries * no-mistakes(document): Document report-only Next.js cache inspection * no-mistakes(document): Explain intentional cache-script index omission * no-mistakes(review): Fix cache sweep completeness and integrity coverage * no-mistakes(document): Correct cache-sweep documentation contracts
…ion merge Fork PR 23 landed this unit with a squash, which flattened the two-parent integration merge the divergence ledger relies on. The content is already on fork main from that squash, so this merge is expected to carry little or no diff: the reachable ancestry link is the deliverable, not the change. Restores the branch-level integration merge for fm/divergence/firstmate-next-cache-reclaim so fm-fork-status.sh can prove what fork main carries. # Conflicts: # AGENTS.md # bin/fm-next-cache-lib.sh # bin/fm-next-cache-sweep.sh # tests/fm-next-cache-sweep.test.sh
…erge fix: restore cache reclaim integration ancestry
feat(bin): integrate captain-held task supervision
feat: integrate upstream runtime and supervision updates
Confidence Score: 4/5The reverse migration’s interruption handling should be fixed before merging because it can strand a repository without an The new reverse operation exposes an intermediate topology across two remote renames but only rolls back an ordinary second-command failure, leaving signals or process termination able to strand future update and topology operations. Files Needing Attention: bin/fm-fork-remotes.sh Reviews (1): Last reviewed commit: "no-mistakes(lint): Captain, fix overlapp..." | Re-trigger Greptile |
| git -C "$REPO" remote rename origin fork | ||
| if ! git -C "$REPO" remote rename upstream origin; then |
There was a problem hiding this comment.
Reverse migration lacks atomicity
When the process is interrupted after renaming origin to fork but before renaming upstream to origin, no signal-safe rollback restores the original topology, causing subsequent topology checks, updates, and migration retries to fail until the remotes are repaired manually.
Context Used: If there is a VISION.md file at the root of the re... (source)
Intent
Deliver piece 1 of four sequential fork-main pull requests created by the captain-approved split because a change too large to review within the validation window is too large to review well. Establish one owner for this rule: a verdict is terminal only when there is a complete positive observation of the exact thing claimed, bound to one stable head; missing, failed, timed-out, incomplete, truncated, unattributed, or moved-head evidence is not determined and never defaults green or red. Piece 1 deliberately includes the worst live caller, the active CI monitor, because a rule landed without a caller is dead code. Preserve positive green only with complete head-bound forge evidence. Complete successful zero-suite and zero-combined-status observations are the terminal unverified no-CI-configured outcome, while an empty or incomplete snapshot is nonterminal because observing nothing differs from observing that there is nothing. Truncated producer lists remain nonterminal. All forge evidence calls share one aggregate deadline sized for the reader budget, and independent producer reads may be concurrent within it. Mixed suite snapshots remain nonterminal and may say no checks ran only when the observed run count is zero. The accepted sequence also requires remote green to become terminal only with head-bound proof, but that remote call site is deliberately reserved for piece 4 and must not be bundled here. Tests must be behavioral and red-first. Fresh live proof on 2026-08-28 against kunchenguid/firstmate PR 2752 at head 13642d8 showed fork main reporting done with checks green while piece 1 reported working with awaiting maintainer approval and no checks run; do not substitute the older August 21 evidence. Deliver only to tiago-peixoto/firstmate fork main, never official upstream, and never merge or force-push. Skip the rebase step because its comparison base is official upstream rather than fork main. Record in the PR body that rebase was skipped, that the comparison base was official upstream, and the measured piece delta against fork main. Current measurements after recovering the pipeline fixes: fork main merge-base is origin/main 2e29079 with 0 behind and 2 ahead, four changed files, 656 insertions and 64 deletions; official upstream merge-base is 038d0f7 with 22 behind and 70 ahead across 160 files. This is the final authorized retry after two review-step timeouts. If review times out again, preserve diagnostics and stop rather than restarting.
What Changed
2e290796was 0 behind/2 ahead across 4 files (+656/−64), while the official-upstream merge base038d0f7ewas 22 behind/70 ahead across 160 files.Risk Assessment
✅ Low: Captain, the four-file fork-main delta is well-bounded and preserves exact-head, complete-evidence terminal verdicts, aggregate forge deadlines, and accurate mixed-suite wording without contradicting the stated intent.
Testing
Baseline mapping matched the four-file piece-1 delta; after fixing a flaky test-only timing assertion, red-first proof failed correctly on the pre-fix executable, the focused crew-state suite and bounded active-caller check passed, and a CLI transcript demonstrated every terminal/nonterminal outcome; PR 2752’s exact head was confirmed live, while check-suite refresh was unavailable due isolated credentials, REST rate limiting, and a browser bridge error, so no older evidence was substituted and no screenshot was needed for this CLI-only change.
Evidence: Piece-1 CLI evidence
Source: Piece-1 CLI evidence
CLI matrix, live PR/head confirmation, and pre-fix red-first proof. SHA-256: 739bc0af49d03c23ed25f97204be14dac6ecf3fa6fea2c22feffa69d3390eb29Evidence: Live PR head confirmation
Source: Live PR head confirmation
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
⏭️ **Rebase** - skipped
.agents/skills/afk/SKILL.md- branch carries 68 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (160 file(s)) into the PR:Push main to origin, or rebase your branch onto origin/main, before gating.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
git diff --stat c50a877^..HEADandgit diff --name-status c50a877^..HEADbash tests/fm-crew-state.test.shtest_forge_evidence_uses_one_aggregate_deadlineagainstc50a877:bin/fm-crew-state.sh(expected red: four forge reads and false green)test_stalled_state_read_is_bounded_and_scan_progressesfromtests/fm-inactive-reconcile.test.shManualbin/fm-crew-state.shCLI matrix covering approval-gated zero runs, mixed suites, complete zero-CI, exact-head green, moved head, and aggregate deadline expirygit ls-remote origin refs/pull/2752/headRead-only live check refresh attempts throughgh-axi, GitHub REST, and Chrome✅ **Document** - passed
✅ No issues found.
🔧 **Lint** - 1 issue found → auto-fixed ✅
🔧 Fix: Captain, fix overlapping forge timeout status patterns
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.