Skip to content

rebase: rebuild lila-main overlay onto upstream 038d0f7 (CI gate only - lands via force-push, not merge) - #10

Closed
jokim1 wants to merge 96 commits into
lila-mainfrom
rebase-upstream-038d0f7
Closed

rebase: rebuild lila-main overlay onto upstream 038d0f7 (CI gate only - lands via force-push, not merge)#10
jokim1 wants to merge 96 commits into
lila-mainfrom
rebase-upstream-038d0f7

Conversation

@jokim1

@jokim1 jokim1 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

CI vehicle for the 2026-08-24 patch-queue rebase. Do NOT merge: lila-main is replaced by force-with-lease push of this exact head (334d0cd) after CI is green, per data/patch-queue discipline.

🤖 Generated with Claude Code

kunchenguid and others added 30 commits August 12, 2026 13:50
* fix(bin): strip every bracket tag, not just [key=...], from a status verb

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

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

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

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

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

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

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

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

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

# Conflicts:
#	bin/fm-spawn.sh

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

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

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

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

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

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

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

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

Two consequences wired up:

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

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

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

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

Verified live on cursor-agent 2026.08.11-e8db854.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* no-mistakes: apply CI fixes

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

* no-mistakes(document): Update quota floor documentation pointer
…id#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(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>
…id#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 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(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): 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
A wedged family-run step was occupying the runner until the 75-minute
job cap; bound that step so cleanup and timing artifacts still upload.
…d mate (kunchenguid#2457)

The lightweight Relay follow-up link lives in the answering home's own
state/<task-id>.meta, so it can only bind work that home owns. When a
Relay-linked request is routed to a second mate, the task record lives in the
second mate's home, fm-x-link.sh failed with a bare "no such task ...meta", and
nothing else picked the promise up: only the soft acknowledgement was ever
posted. The typed promised-final path already supports --work-home
secondmate:<id>; the playbook simply never chose it.

- fmx-respond now states the routing rule crisply: a task in this home takes the
  lightweight link, and second-mate-routed work takes a promised-final
  commitment bound to that home, registered up front with the brief command
  carried into the routed worker's instructions.
- fm-x-link.sh refuses a task with no local record by naming the registered
  second mate whose home actually holds it and printing the promised-final
  registration command, with the exact --work-home when the match is
  unambiguous. A home with no registered second mates keeps the plain error.
- fm-backlog-handoff.sh reports, after a successful move, any moved key that
  still owes a public reply bound to main/<key>, since that binding no longer
  names the home owning the work. The move itself is never blocked.

Docs and the secondmate handoff prose follow the same rule. Tests cover the
refusal, its scoping, the unchanged local-link path, and both handoff outcomes
at the script boundary.
…verdicts (kunchenguid#2456)

* fix(skills): hint that remote secondmate liveness verdicts false-negative

fm-crew-state and fm-send routinely misreport a live remote secondmate
as dead; confirm against the pane before relaunching, and relaunch only
through fm-spawn.sh, never raw herdr pane surgery.

* no-mistakes: apply CI fixes
Pi 0.83.0 added a status line to every tool-expansion change, and Pi
updates the previous status line in place when two status messages
arrive back to back. Calm's post-export redraw cycled tool expansion on
the macrotask right after Pi printed "Session exported to: <path>", so
both expansion status lines coalesced over that confirmation and the
captain was left with no record of where their export landed.

Calm now repaints only the tool rows it presents, by invalidating each
row through the render context Pi hands its render slots, and requests
the surrounding redraw through setStatus. Neither appends to the
transcript. The repaint is still needed because Pi can re-render a row
asynchronously - the built-in edit row invalidates itself once its diff
is ready - and that re-render can land inside the window where /export
forces stock rendering.

The real-terminal /export case now asserts the confirmation is still on
screen after the redraw has settled, and that the redraw restored every
Calm-hidden row, instead of only racing the moment the confirmation
first appeared.
…nguid#2488)

* feat(stow): persist the open records a session is holding

/stow curated memory and captured session knowledge, but never touched
record state, while AGENTS.md called it an "unfinished-work sweep" and the
receipt declared the session "safe to reset" - wording that implied a
record-correctness guarantee stow does not make. A shipped PR with no
backlog item, a queued umbrella whose phases had merged, and four decision
holds left open after their answers shipped all survived repeated stows.

Add a bounded pass that files record state from the same volatile input the
rest of stow already uses: the open threads in context, minutes before the
reset destroys them. It creates a record for an unfiled thread and corrects
one the session knows is wrong, through the owning path, and states its
boundary as part of the contract - it never enumerates the backlog, lists
holds, or queries a forge, because it cannot be a reconciliation and must
not be read as one.

Correct the wording in AGENTS.md and the completion receipt so reset-safe
means what it actually guarantees: nothing this session knew was lost.

* no-mistakes(review): correct stow decision-hold inspection to read hold via tasks-axi

* no-mistakes(document): note /stow open-record persistence in README command catalog

* refactor(stow): state open-record persistence as principle, not procedure

The first version enumerated triggers, named commands, and prescribed an
ordered procedure. That is too rigid for an agent skill: it invites literal
execution of a checklist instead of judgment, and every enumerated example
is a way for the guidance to go stale.

Reduce it to the intent - before a reset, the important open work you are
holding in context must end up durably recorded rather than dying with the
session, filing what is unfiled and correcting what is stale - and let the
agent judge importance, the record, and the owning write path.

Keep the scope bound, since it is a decided contract and not a mechanic:
this covers the open work the session is holding, never a reconciliation of
durable records against repository or forge reality. The wording
corrections in AGENTS.md and the completion receipt are unchanged.
…eyed-answer path (kunchenguid#2490)

* fix(decisions): close captain holds at answer time

Firstmate had two "a decision is open" ledgers with asymmetric closing
mechanics. The live status-log ledger closes atomically at answer time,
because bin/fm-send.sh --resolve-key makes answering a decision be the
act that closes it. The durable backlog hold ledger had no such coupling:
answering and recording were two separate acts, and only the first was
forced by the workflow.

That asymmetry lost four real captain decisions. Their answers were
captured durably to disk, keyed character for character by the hold
decision keys, acknowledged, and even implemented and shipped, yet the
holds stayed open for two days and the captain was asked to re-answer
decisions already on his own disk.

Give the hold ledger the same answer-time-closure property:

- bin/fm-decision-hold.sh gains an `answer` subcommand, the hold ledger's
  counterpart to --resolve-key. It shares one unrouted close
  implementation with `decline`, so it carries every existing guard - the
  captain decision file, the active-hold requirement, retry identity, and
  the refusal to release still-routed work - and differs only in the
  resolution mode it records. `decline` keeps its stronger meaning that
  the answer routes no follow-up work at all.
- bin/fm-procevent-lavish.sh wires the channel that actually carried the
  lost answers. `arm --decisions-origin` binds a deck to the origin whose
  holds it carries, `answers` reads the structured choices out of a
  captured poll result, `close-decisions` maps each key to its hold and
  closes it through the command above, and `autohandle` lets the runner
  apply that at capture time.

Safety is preserved rather than traded away. Only rows tagged `choice`
are read, so freeform captain prose cannot forge a decision key. Closure
is confined to the one bound origin. The decision text is a pure function
of the captured result, so a replayed capture is idempotent. A hold that
is absent, already closed, or still blocking routed work is skipped and
left for `resolve`, never forced. A deck armed without the binding
touches no hold at all. And autohandle deliberately never reports full
handling, because recording an answer is transcription while acting on it
is firstmate's judgement - so the check wake still reaches the handler.

fm-send --resolve-key is untouched.

* no-mistakes(document): document state/lavish-decisions binding dir in AGENTS.md state inventory

* refactor(decisions): make keyed-answer closure one general capability

The previous pass gave holds answer-time closure but built it as bespoke
Lavish wiring: the review adapter carried the source-to-origin binding,
mapped keys to hold identities, wrote decision records, decided what to
skip, and closed holds itself. That treated a review deck as a special
decision source. It is not - it is an ephemeral discussion format that
happens to carry answers.

Collapse it into ONE general capability with one owner.

bin/fm-decision-hold.sh now owns the whole of "a keyed answer closes its
matching hold":
- `answers <origin> --source <provenance>` is the channel-agnostic
  intake. It reads key/answer/label lines on stdin, maps each key to its
  hold, and closes it through the same `answer` path, so every guard
  applies identically whatever channel the answer came from. --source is
  provenance recorded in the decision, never a behavior switch; there is
  no per-channel branch and no knowledge of chat, decks, or transports.
- `bind`/`unbind`/`binding` own the source-to-origin binding for any
  channel whose answers arrive detached from their origin.

Every channel is now an ordinary caller that only turns what it received
into keyed lines:
- bin/fm-send.sh (chat) feeds the intake for a key that names an active
  hold. This also fixes a real gap: once `complete` transfers a decision
  to its hold it closes the live status copy, so --resolve-key alone
  could never answer a transferred decision.
- bin/fm-procevent.sh feeds it generically. A bound source's captured
  result goes to `<adapter> answers <result-file>` and whatever that
  prints is piped into the intake. The runner names no adapter, parses
  no result, and carries no decision rule, so any future adapter with an
  `answers` command works with no change here.
- bin/fm-procevent-lavish.sh keeps only `answers`, which reports the
  structured choices a review captured and stops. It maps nothing to a
  hold and closes nothing; it lost ~160 lines of decision logic.

Feeding is independent of handling, so it never acknowledges a result
and never suppresses a wake - recording an answer is transcription,
acting on it stays firstmate's judgement.

The regression that proves closure now drives a FIXTURE adapter that is
not the review adapter, so what is proven is that any bound channel
reaches the intake rather than that one channel is wired specially. A
new regression drives the real fm-send over a stubbed transport for the
chat side. Every prior guarantee still holds, and fm-send's status-log
behavior is unchanged.

* no-mistakes(review): test(decisions): drop source-content grep from hold-closure regression
…mlink (kunchenguid#2512)

A Write aimed at CLAUDE.md followed the symlink and destroyed AGENTS.md.
The installer now creates and migrates to a recoverable two-line pointer file.
* fix(lint): catch malformed GitHub workflows before merge

A self-broken ci.yml cannot report its own breakage, so parse every
workflow in the local lint path that no-mistakes already runs.

* fix(lint): pin actionlint instead of Ruby for workflow lint

A self-broken ci.yml still has to fail in the local lint path, and the
named tool for that gate is actionlint, not a new Ruby runtime.

* no-mistakes(document): Clarify pinned workflow lint documentation
…d#2546)

* fix: install pinned shellcheck and actionlint on macOS and linux arm64

The installers were hardcoded to linux amd64 and sha256sum, so a Mac
dev could not satisfy the refuse-on-mismatch lint gate. Select the
official per-platform archive and checksum, and fall back to shasum -a 256.

* no-mistakes(document): Document cross-platform pinned lint installers
…uid#2548)

.no-mistakes.yaml has set test.evidence.store_in_repo: true since kunchenguid#2355, but
CONTRIBUTING.md, docs/configuration.md, and docs/architecture.md still described
the old policy of keeping evidence out of the repo in a temp directory.

The current no-mistakes behavior for store_in_repo: true is to publish each run's
test evidence to the orphan no-mistakes/evidence branch and link it from the PR
body. That branch shares no history with code branches, so evidence never enters
a pushed feature branch or the default branch, and CI's tracked personal fleet
paths rule stays accurate.

Docs only. No change to .no-mistakes.yaml or any workflow.
* docs: correct test evidence storage comment in .no-mistakes.yaml

* no-mistakes: apply CI fixes
…nchenguid#2563)

Make that a first-class option in always-loaded instructions so firstmate does not default to mediating and tearing the scout down between iteration rounds.
…chenguid#2570)

* fix(bin): report remote secondmate delivery and state truthfully

A steer to a remote secondmate crosses fm-on.sh to a host-local fm-send
leg whose unconfirmed submit read-back (verdict=pending, typically a busy
mate whose harness queues the steer) was flattened into exit 1, so the
parent printed "error: text not submitted" / "error: text not sent" and
discarded the pending-reply expectation for a steer that had actually
landed. fm-send now carries the verdict across the ssh boundary as a
documented delivered-unconfirmed exit 3: the parent reports the steer as
delivered with confirmation pending, exits 0, keeps the expectation armed
(awaiting_report), and closes --resolve-key decisions, while transport
loss (ssh 255) and real remote failures keep failing loudly with the
remote leg's stderr attached. A local unconfirmed submit now also exits 3
with an honest non-error message and still never closes a decision key.

fm-crew-state.sh and fm-peek.sh no longer read a remote mate's endpoint
through local probes (which misreported a healthy mate as "worktree gone"
/ "can't find session: remote"): both now use the true remote source over
fm-on.sh, and an unreachable or unreadable remote reads as unknown-remote,
never as gone or dead.

* no-mistakes(document): Document remote delivery and state truth

* no-mistakes: apply CI fixes
* Adopt quota-axi 0.1.29 spendPriority-primary array dispatch.

quota-axi 0.1.29 publishes schema 5 with selection.spendPriority as the primary comparative signal and demotes derivation fields out of default --json. Rank comparable-fit candidates on that scalar, keep runway versus the completion horizon as a hard gate, and raise the compatibility floor so a pre-consolidation build cannot reach dispatch intake.

* no-mistakes(review): Correct schema fixtures and remove prescriptive selection prompts

* no-mistakes(document): Correct quota verification evidence chronology

* Collapse quota-array-dispatch onto TOON-first spendPriority ranking.

Decide from quota-axi's default TOON; keep --json as a rare defensive fallback.
Rank by spendPriority after eligibility, reasoning-class, and runway-feasibility gates, and drop the hand-computed Pareto, pace, reserve, and window-id layers.

* no-mistakes(review): Permit ambiguous JSON fallback and correct reset fixtures

* no-mistakes(review): Correct runway semantics and escalate unresolved uncertainty

* no-mistakes(document): Document TOON-first quota dispatch evidence
* docs: add GROK_BOT.md Grok Bot system prompt

* docs: amend GROK_BOT.md with charter report-back and delegation marker

* docs: classify GROK_BOT.md as public-product

* docs: make GROK_BOT.md the plain Grok Bot system prompt
Refine language for clarity and consistency in instructions.
kunchenguid and others added 28 commits August 23, 2026 23:44
* feat(bin): parallelize session-start remote secondmate network sweeps

Run per-secondmate liveness and convergence probes concurrently and overlap clone refresh, while replaying each mate's fail-closed diagnostic in original order. Ignore scratchpad* so untracked scratch no longer blocks remote sync.

Co-authored-by: Cursor <cursoragent@cursor.com>

* no-mistakes(document): Document parallel startup network sweeps

* no-mistakes(lint): Fix empty environment assignment lint warning

* no-mistakes: apply CI fixes

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(tests): count declared-pause wakes without crashing on an absent queue

The exited-declared-pause case counts queued stale wakes by handing
state/.wake-queue straight to awk. A watcher that queues nothing never
creates that file, and awk aborts on a missing path before its END rule
runs, so the count collapses to the empty string. The next comparison
then fails as an integer-expression error and surfaces as a wake flood
with no number, hiding the real contract breach the following grep names.

Read the queue the way the drain-count assertion at the end of this file
already does: silence awk's open error and default an absent queue to
zero. Applied to all four counts in this case, including the live
external-decision gate pair whose queue an acknowledged drain can also
leave behind. An absent queue now reports "did not use the bounded
paused recheck", while a genuine flood still fails with its real count.

Fixes kunchenguid#2628

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
… icon (kunchenguid#2934)

* style(pi): restyle supervision merge notes with a sailboat and matching pad

Secondary-session notes were flush against the TUI edge and fully tinted.
Use the sailboat prefix, Pi's default outputPad, boat-only color, and dim remainder so they sit like real messages.

* style(pi): distinguish routine and captain merge notes by icon only

Visible notes now lead with a sailboat or anchor, then only the dim outcome.
Drop the branch-merged wording and verdict brackets so the icon is the only kind signal.
…id#2938)

The markdown contract stays the owner; the still is only the visual of the idea.
* fix(bin): bound remote worker supervisors

* no-mistakes(review): release incumbent supervisor before starting its replacement

* no-mistakes(review): wait out a healthy same-root supervisor instead of replacing it

* no-mistakes(review): narrow remote worker change to restart accounting only

* no-mistakes(document): clarify supervisor restart guard is a lifetime total
[queue: PR#1875; retire-when: upstream .claude/settings.json guards empty CLAUDE_PROJECT_DIR on every Claude-settings hook entry]
[queue: PR#987; retire-when: upstream brief Definition of done names the real finish line per ship mode and reserves done: for that finish line]
[queue: PR#988; retire-when: work_is_landed refuses teardown when recorded pr= is OPEN]
[queue: PR#2041; retire-when: fm-send --resolve-key exits nonzero when the key remains open after the close append]
[queue: PR#1295; retire-when: home-seed writes a home-scoped treehouse.toml pool root so secondmate and main homes never share pool slots]
…reeing transitions

[queue: PR#2051; retire-when: capacity-freeing status/teardown/merged-PR transitions enqueue an idempotent refill wake]
[queue: PR#2054; retire-when: upstream lands fm-focus.sh and fail-open suspend-before-switch on captain prompts]
[queue: fork-fix fm-spawn-foreign-marker-scrub; retire-when: upstream fm-spawn scrubs foreign primary-harness env markers at launch]
[queue: fork-fix fm-herdr-stale-projection-cleanup; retire-when: upstream live_binding_matches accepts non-contiguous parent nesting]
…ren per-child

[queue: fork-fix fm-watcher-beacon-grace-mismatch; retire-when: upstream derives guard grace from FM_POLL via one shared validated parser with a cap]
Wire experimental backend=playbot into the existing per-backend dispatch
seams (backend, spawn transaction, control, teardown, brief) without
touching lock/wake/send/watch/crew-state owners. Live spawn stays refused
until Phase 1/2 native gates pass; hermetic tests may unlock with
FM_PLAYBOT_NATIVE_SPAWN=1. Adapter implementation remains in
bin/backends/playbot.sh (parallel additive lane).

Diff budgets (inserted lines): backend +71, spawn +308, control-lib +32,
control +30, teardown +112, brief +55, AGENTS +4, configuration +14.
Compress the Playbot dispatch helpers so bin/fm-spawn.sh total changed
lines vs base fall under the plan §6.2 hard stop (206 < 312). Silence
SC2016 on intentional brief-prose backticks. Evidence and budgets live
in the task report under data/fm-playbot-core-seam/report.md.
…ts, docs

Plan v3 (data/lanemcp-impl-plan/report.md) additive components, hermetic-first:

- bin/fm-playbot-lanes.mjs: read-only topology/rollout client, compatibility
  manifest with per-operation Phase 1 mutation gate, doctor/ready, bounded
  CDP transport, content-addressed stdio MCP server (health only until
  per-thread identity is proven), controller lease validation, lock-owner
  bind-project/bind-controller CLI.
- bin/backends/playbot.sh: fm_backend_playbot_* adapter interface for the
  shared-core seam; read-only operations work, every mutation refuses with
  PHASE1-EVIDENCE-REQUIRED before any IPC call.
- bin/fm-playbot-reconcile.mjs: durable completion reconciliation (strict
  per-line JSONL parsing, outbox pending->acknowledged state machine,
  turn-ended touch, 32 KiB/1 MiB copy caps, one static pointer line,
  lock-owner-only ack, generated registerable check wrapper).
- tests/fm-playbot-{lanes,backend,reconcile}.test.sh plus synthetic fixtures
  covering the plan section 8.1-8.3 matrix; docs/playbot-lanes.md,
  docs/verification/playbot-lanes.md, .agents/skills/playbot-lanes/SKILL.md.
Implement the dispatch-transaction and teardown proof surfaces the shared
core seam already calls: binding_resolve, workspace/thread create stubs,
route_write, endpoint_confirmed_gone, and retained/refuse teardown tokens.
Keep live mutations PHASE1-EVIDENCE-REQUIRED; hermetic suite remains green.
* feat(playbot): implement native mutations, Phase 1 smoke, and evidence gate

Replace refuse-only mutation stubs with real CDP IPC operations gated by a
content-hash-bound evidence overlay the disposable smoke alone may extend.
Record live 0.92.0 smoke evidence (gate-8 confinement re-scope), wire the
backend adapter, and flip doctor/ready to native-enabled when verified.

* no-mistakes(review): Harden Playbot mutation readiness, evidence, and cleanup gates

* no-mistakes(review): Harden Playbot smoke evidence and cleanup gates

* no-mistakes(review): Harden Playbot identity proofs and evidence publication

* no-mistakes(review): Guard fixture evidence and extend interrupt verification

* no-mistakes(review): Match confinement proofs and bound interrupt timing

* no-mistakes(review): Bind confinement proofs to structured shell operations

* no-mistakes(review): Bind confinement proof to executed command results

* no-mistakes(review): Bind confinement proof to fixed scripts

* Record final Playbot native smoke evidence

* no-mistakes(document): Document Playbot native readiness and smoke evidence

* fix(ci): run workflows on lila-main PRs

Fork default branch is lila-main; workflows only targeted main so PR checks never registered.

* fix(tests): parse playbot backend mocks on Bash 3.2

Nested case statements inside command substitutions fail stock macOS Bash
parse; rewrite interrupt mocks with if/elif for CI snapshot compatibility.

* fix(tests): portable in-place sed for playbot endpoint fixtures

sed -i '' is BSD-only and treats the script as a file on GNU sed in CI.

* fix(tests): portable mode and mtime checks in playbot suites

BSD stat -f fails on Linux CI; use Darwin/Linux helpers for mode and mtime.

* fix(tests): complete gotmp teardown fixture lib links

Teardown now sources send-followup, classify, and remote-job libs; the
gotmp fixture must symlink them and avoid duplicate wake-lib links.
* fix(fm-teardown): default-deny ship teardown without positive land proof

Empty-unpushed remote reachability is not landing. Require live default tip
proof (ancestry, no-pr tree equality, or MERGED containment/tree) against a
default tip whose remote OID still matches after fetch. Always refuse OPEN.
Unconfirmed never accepts tree equality. Missing worktree refuses. Recheck
dirty+land+full PR classify after quiescence before destructive return.

* no-mistakes(review): Close teardown revalidation and remote symref races

* no-mistakes(review): Recheck Playbot worktrees after endpoint quiescence

* no-mistakes(document): Document teardown safety rechecks and proof ownership

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes
* fix(supervision): retire resolved pending-replies and keep beacon alive

Answered pending-reply records were left on disk forever, so the watcher
tick walked a growing archive every poll and could starve the liveness
beacon past grace while still healthy (false WATCHER DOWN). Retire only
resolved records after closing any open escalation, touch the beacon at
bounded intervals during a large walk, and attach re-arms to a live
identity-matched holder even when its beacon is temporarily stale mid
poll so Stop auto-arm does not thrash replacement cycles.

* no-mistakes(review): Keep watcher beacons fresh and require confirmed starts

* no-mistakes(review): Bind pending-reply beacons to watcher lock ownership

* no-mistakes(document): Update watcher continuity documentation

* no-mistakes(lint): Suppress intentional ShellCheck fixture warnings

* fix(supervision): quarantine stuck-resolved pending-replies off the hot walk

Resolved records that cannot close their escalation (blank parent_status,
unwritable status append) no longer remain in state/pending-replies/ forever.
Quarantine them under pending-replies-stuck/ with a durable receipt so open
status-fold decisions stay intact and polls cannot re-accumulate answered
files. Fail closed if the quarantine move cannot complete; regressions use
chmod 444 (readable) for the unwritable-status fixture.

* fix(supervision): drop unused local in pending-reply quarantine

* fix(tests): drop unused locals in stuck-quarantine fixtures

* fix(supervision): quarantine stuck-resolved records; preserve peer stand-down

Close BREAK 1: resolved+escalated records that cannot close leave the hot
pending-replies walk via quarantine to pending-replies-stuck/ with a durable
receipt (chmod-444 unwritable fixture; fail closed on quarantine move failure).
Restore peer-startup race: arm entry still requires a fresh beacon to attach
without starting a child, and stand-down diagnostics stay on disk during
live-holder attach so the child "already running" line remains visible.

* no-mistakes(review): Attach stale-beacon watcher holders before spawning

* no-mistakes(review): Bind watcher attachment to generation beacon proof

* no-mistakes(review): Enforce generation-bound health across watcher guards

* no-mistakes(test): Bound watcher teardown and stabilize supervision tests

* no-mistakes(test): Fix stale lock reclamation and remote teardown completion

* no-mistakes(document): Document generation-bound watcher health

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* fix(supervision): reap immediate watcher wakes

* fix(ci): bound watcher lifecycle teardown
…atcher stop (#5)

* fix(daemon): suppress same-window refill after capacity-freeing escalate

Away-mode Scenario B double-injected U+2063 when a done/failed/blocked/
needs-decision status already forced capacity re-evaluation and a same-
window refill wake fired a second escalate. Cover the refill for one TTL-
bounded shot, keep later refill-only wakes escalating, and align gotmp
fixtures with teardown's remote-job lib sources.

* fix(watch-arm): bound stop so hung watcher cleanup cannot wedge the suite

Arm signal handling and --restart waited unboundedly on a child watcher
whose EXIT cleanup can block on recovery-marker locks. Escalate TERM to
KILL after a short bound, and make wait_for_exit do the same so bare
waits after intentional interrupts cannot hang the full portable suite.

* no-mistakes(test): Fix stale lock reclamation and remote teardown hangs

* no-mistakes(test): Synchronize watcher recovery acknowledgement fixtures

* no-mistakes(test): Bound watcher shutdowns and stabilize recovery tests

* no-mistakes(test): Fix Playbot dedupe and load-sensitive test timing

* no-mistakes(test): Stabilize scheduler-sensitive lifecycle test synchronization

* no-mistakes(test): Process-event lifecycle coverage continues green, including replacement, ordering, and runner retirement

* no-mistakes(lint): fm-watch-checkpoint.sh: add shellcheck source directives for sourced libs
* fix(bin): allow endpoint-less tmux husk teardown after name-independent absence proof

Terminal ship/scout records with no window= target could not be cleaned up even
when work was fully landed, because endpoint validation ran before force and
land checks. Open a guarded husk path only for exact tmux husk shape, only after
protocol-level proof that no live tmux server remains for this uid (plus add-only
name secondary), and never skip land/scout gates under --force for husks.

* no-mistakes(review): fix(bin): refuse husk absence proof without hard-bounded probe runner

* no-mistakes(document): note endpoint-less husk teardown exception in configuration doc

* no-mistakes(lint): remove unused REAL_LSOF/REAL_PGREP vars in husk test

* ci: raise portable-serial shard hang tripwire to 20 minutes

Live serial shard walls are commonly 8-14 minutes; the 15-minute cap left
almost no margin once a new husk regression script rebalanced shard 3 over
the limit. Keep the job as a hang tripwire, not the expected healthy end.
…ary (#4)

* docs: add FORK.md for origin-only overlay rules

Document the jokim1 fork model, origin-only PR boundary, rebase ownership, and diverged-checkout expectations so every worktree shares the same fork rules.

* no-mistakes(review): Clarify immutable upstream write boundary

* no-mistakes(test): Fix fork policy and documentation audience classification
…ads:launch contract (#9)

* feat(playbot-lane): release-aware thread-open contract for Playbot 0.94.0

Playbot 0.94.0 removed the threads:openThread and db:workspaceThreads:open
IPC channels. "Open a thread" was restructured: the operation now flows
through threads:launch, which the app mints the thread id for and returns
the persisted thread ({workspace, thread, activate, createdWorkspace}),
with activation split into threads:setActiveThread. This inverts every
assumption the legacy lane relied on (caller-minted chat-N-N id, undefined
result, single-channel open).

Make the thread-open wire contract release-aware while keeping the abstract
operation and evidence key stable at threads:openThread:

- releaseCompatibilityShape now parameterizes ipcChannelStrings and carries a
  threadOpen descriptor; the 0.94.0 seed uses the launch/setActive surface and
  the app-minted-id, non-undefined-result contract.
- MUTATION_WIRE_CHANNELS decouples the IPC-channel allowlist from the abstract
  operation keys so threads:launch is invokable; gatedInvoke takes an explicit
  wireChannel so the evidence gate still keys off threads:openThread.
- mutationOpenThread dispatches to the legacy path (unchanged) or a new launch
  path that consumes the app-minted result.thread.id via validateThreadLaunchResult
  and re-asserts the persisted row.
- The Phase-1 smoke records the real wire channel and app-minted id in the
  evidence body so the 0.94.0 overlay honestly reflects the launch contract.

The static IPC surface and payload shapes were verified read-only against the
live 0.94.0 app.asar. doctor --json on 0.94.0 now passes release_compatibility,
both DB-schema dims, and ipc_channel_strings_static honestly; native-enabled
still awaits the Phase-1 disposable smoke as designed.

Tests and the fixture bundle extend to the 0.94.0 launch contract.

* feat(playbot-lane): fuse 0.94.0 workspace-create into threads:launch + record evidence

0.94.0 removed the standalone workspace:create channel and fused workspace
creation into threads:launch with a new-workspace destination (one call creates
the workspace and opens its first thread). The prior static IPC scan had
false-positived workspace:create by matching it as a substring of the
workspace:created event.

Extend the release-aware adapt to the fused create+open contract:

- Add a release-aware workspaceCreate descriptor (legacy standalone
  workspace:create vs 0.94.0 fused threads:launch), parallel to threadOpen.
  mutationWorkspaceCreate dispatches to the legacy or fused-launch path.
- The fused path creates the workspace via threads:launch(new-workspace),
  reconstructs the workspace result from the authoritative DB row (unchanged
  downstream contract), and returns the fused thread id. The abstract evidence
  keys stay workspace:create and threads:openThread; the smoke adopts the fused
  thread rather than opening a second one and records BOTH guarantees (workspace
  created + thread opened) from the single launch, each body annotated with the
  real wire channel and the fusion.
- The fused launch provisions its worktree root asynchronously after returning,
  so add waitForWorkspaceProvisioned to poll until workspace_roots lands before
  asserting workspace identity and branch.

Harden the static IPC surface check against substring false-positives:

- scanFileForNeedles gains an exactToken mode requiring each needle to be
  bounded by non-channel-token bytes, so an event string can never satisfy a
  command-channel needle. The doctor's ipc_channel_strings_static uses it; the
  preload-bridge substring scan is unchanged. Streaming boundary handling keeps
  cross-chunk matches correct.

The 0.94.0 IPC surface, fused-launch payload/result, and async provisioning were
verified read-only against the live app.asar and then proven end to end by the
Phase-1 disposable smoke: doctor reports native-enabled and the smoke recorded
signed evidence for every 0.94.0 mutation plus write-denial confinement
(0.92.0/0.93.1 evidence preserved). The smoke targets only the registered
disposable project, never MAIN.

Tests cover the workspace-create contract, the fused launch result validation,
and the exact-token accept/reject behavior.

* no-mistakes(review): reject explicit launch thread id, require provisioned path, dedupe payload

* no-mistakes(review): restore smoke-attested lanes script, defer fixes to follow-up

* no-mistakes(document): document 0.94.0 fused threads:launch contract and smoke evidence
@jokim1

jokim1 commented Aug 25, 2026

Copy link
Copy Markdown
Owner Author

History-rewrite PR cannot build a merge ref (CONFLICTING), so pull_request CI never runs. CI runs instead on a push-triggered temp commit on this branch; lila-main will be replaced by force-with-lease with the clean tip 334d0cd after green.

@jokim1 jokim1 closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants