Skip to content

feat(omp): port full Oh My Pi (omp) support into firstmate - #4

Merged
frasdl merged 38 commits into
mainfrom
fm/omp-support
Aug 27, 2026
Merged

feat(omp): port full Oh My Pi (omp) support into firstmate#4
frasdl merged 38 commits into
mainfrom
fm/omp-support

Conversation

@frasdl

@frasdl frasdl commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Intent

Port full Oh My Pi (omp) support into the firstmate fork: detect omp before the Claude marker in fm-harness.sh, add the omp primary supervision stack (.omp/extensions watch + turn-end guard with the delegation-shape subagent guard), omp crewmate/scout dispatch in fm-spawn with busy-state/composer/control wiring, omp session-lock identity, supervision protocol and verification docs, and the omp test suites. Also sync the fork to upstream main and make MISSING-diagnostic test assertions host-deterministic.

What Changed

  • Detect the omp (Oh My Pi) harness before the Claude-Code marker in bin/fm-harness.sh via the unambiguous OMPCODE env var and an exact process-name match, and treat omp as Claude-identified for session-lock ownership without misclaiming a foreign omp process's lock identity (bin/fm-session-lock-lib.sh, bin/fm-lock.sh).
  • Add the omp primary supervision stack under .omp/extensions (watcher bridge plus turn-end guard with the delegation-shape subagent guard), omp crewmate/scout dispatch in bin/fm-spawn.sh with busy-state, composer, and control wiring, and omp harness support in bin/fm-classify-lib.sh, bin/fm-composer-lib.sh, bin/fm-busy-lib.sh, and bin/fm-send.sh.
  • Add omp supervision-protocol and runtime-backend verification docs plus the omp test suites (harness detection, primary extension typechecks and live e2e, session-lock live e2e), sync the fork to upstream main (supervision-branch model/effort picker, send, pr-merge, and remote-secondmate reconcile fixes, CI workflow updates), and make MISSING-diagnostic test assertions host-deterministic.

Risk Assessment

✅ Low: The fix round is a single, well-bounded, verified-correct tightening of the omp args-matching pattern in fm-harness.sh that resolves the previously flagged substring false-positive (and the earlier composer-regex finding was explicitly ignored by the user), with no new risk introduced in the pipeline-authored code.

Testing

Exercised the whole omp port: live runs of both opt-in guards against the real omp v18.0.6 binary proved detection precedence, the session-lock identity (comm=omp, CLAUDECODE=1, ancestry-stopping recognition, foreign-pid/marker persistence guards), and the primary supervision stack (session_stop blocked + native continuation, fm_watch_arm_omp discoverable/callable, watcher retire/rearm), plus the fm-spawn omp launch template and real-spawn-generated omp-ext busy-state artifact through the actual writer and classifier; every portable omp-related suite (harness, session-lock-ancestry, extensions, busy-adapter-wiring, composer-lib, turnend-guard, supervision-instructions, subagent-pretool-check, bootstrap) passes with zero failures, both remaining skips being deliberate host-environment or determinism skips, and the worktree was left clean.

Evidence: omp detection matrix and spawn launch template

Source: omp detection matrix and spawn launch template

OMPCODE=1 CLAUDECODE=1 -> omp; CLAUDECODE=1 alone -> claude; PI marker alone -> pi; cursor/grok markers still win; fm-spawn omp template: omp --approval-mode yolo -e __OMPEXT__ ...

\### omp detection matrix (bin/fm-harness.sh), omp v18.0.6 host
OMPCODE=1 CLAUDECODE=1          ->  omp
OMPCODE=1 PI_CODING_AGENT=true  ->  omp
CLAUDECODE=1 alone              ->  claude
PI_CODING_AGENT=true alone      ->  pi
CURSOR_AGENT=1 + OMPCODE=1      ->  cursor
GROK_AGENT=1 + OMPCODE=1        ->  grok

\### fm-spawn omp crewmate launch template
omp --approval-mode yolo -e __OMPEXT__ __MODELFLAG____EFFORTFLAG__"$(__OPINPUT__ encode launch-brief < __BRIEF__)"
Evidence: live omp session-lock identity guard (omp v18.0.6)

Source: live omp session-lock identity guard (omp v18.0.6)

ok - omp session-lock live guard: a real omp session's own process environment carries CLAUDECODE=1; a real omp process's own comm reports base name 'omp'; the real (unfaked) classifier recognizes a live omp process from its own ancestry without extending past it

# omp omp/18.0.6: its own bash tool reported CLAUDECODE=1 in its own environment
ok - omp session-lock live guard: a real omp session's own process environment carries CLAUDECODE=1
# omp omp/18.0.6: live process comm='omp' args='/home/francesco/.local/bin/omp -p --no-session Run this exact bash command using your tool and output only its result, nothing else: echo -n "OMPCLAUDECODE=$CLAUDECODE"; sleep 3'
ok - omp session-lock live guard: a real omp process's own comm reports base name 'omp'
ok - omp session-lock live guard: the real (unfaked) classifier recognizes a live omp process from its own ancestry without extending past it
exit=0
Evidence: live omp primary supervision guard (omp v18.0.6)

Source: live omp primary supervision guard (omp v18.0.6)

ok - omp omp/18.0.6: session_stop blocked and continued natively, and fm_watch_arm_omp was discoverable and callable

ok - omp omp/18.0.6: session_stop blocked and continued natively, and fm_watch_arm_omp was discoverable and callable
exit=0
Evidence: omp-targeted suite results

Source: omp-targeted suite results

fm-omp-harness PASS; fm-omp-primary-extensions 1 ok; fm-omp-primary-types skipped (package not resolvable on host); fm-session-lock-ancestry 13 oks; fm-busy-adapter-wiring 11 oks; fm-composer-lib 34 oks; fm-turnend-guard 70 oks; fm-supervision-instructions 10 oks; fm-subagent-pretool-check 14 oks; fm-bootstrap 28 oks (deterministic ambient-CLI skip)

fm-omp-harness: PASS (silent assert matrix, 8 cases)
fm-omp-primary-extensions: 1 oks; no failures
fm-omp-primary-types: 0 oks; skip: installed @oh-my-pi/pi-coding-agent package not found 
fm-session-lock-ancestry: 13 oks; no failures
fm-busy-adapter-wiring: 11 oks; no failures
fm-composer-lib: 34 oks; no failures
fm-turnend-guard: 70 oks; no failures
fm-supervision-instructions: 10 oks; no failures
fm-subagent-pretool-check: 14 oks; no failures
fm-bootstrap: 28 oks; skip: ambient zellij in the run PATH; absent-CLI fail-closed case not reproducible here 

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-harness.sh:119 - The new *omp*) echo omp arm in the node/python ancestry-args branch matches any argv0/args containing "omp" as a raw substring (e.g. python3 /opt/compare/..., node foo-comp/...). The author's own comment (lines 100-103) establishes that omp's live ps -o comm= is exactly omp (caught by the precise exact-match omp) arm at line 103), so this args substring arm is effectively dead for real omp and only adds false-positive surface. "omp" is a far commoner substring than the sibling patterns (claude/codex/opencode/grok), so a node/python process in the ancestry whose path contains it would be misidentified as omp, which then feeds busy-source, control, and session-lock classification. Consider matching a whole-word omp token (or dropping the args arm since omp never runs under a bare node/python interpreter), consistent with the exact-omp) precedent.
  • ℹ️ bin/fm-composer-lib.sh:327 - FM_DELIVERY_BUSY_REGEX_DEFAULT (the generic, harness-agnostic busy regex used for the &#39;&#39;/unknown-harness case) gained ⟨esc⟩. omp already gets its own dedicated regex (FM_DELIVERY_OMP_BUSY_REGEX_DEFAULT=&#39;⟨esc⟩&#39;) via the omp) arm, so adding the glyph to the shared default broadens busy detection for ALL unknown-harness consumers. Risk is low because the glyph is omp-specific, but any other harness TUI that renders the literal ⟨esc⟩ key-hint while idle would now read busy on the unknown path. Not a blocker; noting the deliberate widening.

🔧 Fix: Tighten omp args detection to whole-word match in fm-harness.sh
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • tests/fm-omp-harness.test.sh - omp detection env-marker matrix (OMPCODE vs CLAUDECODE/PI/cursor/grok precedence, no-marker unknown, crew mirror) - PASS
  • env OMPCODE=1 CLAUDECODE=1 bin/fm-harness.sh -> omp; CLAUDECODE=1 alone -> claude; CURSOR_AGENT=1/GROK_AGENT=1 + OMPCODE -> cursor/grok (live detection matrix)
  • FM_OMP_LOCK_LIVE_E2E=1 tests/fm-omp-session-lock-live-e2e.test.sh - real omp v18.0.6 session reported CLAUDECODE=1 in its own bash-tool env, real ps comm=omp, and the real (unfaked) fm_harness_process_matches recognized its own ancestry without setting FM_HARNESS_IS_CLAUDE - PASS
  • FM_OMP_PRIMARY_LIVE_E2E=1 tests/fm-omp-primary-live-e2e.test.sh - real omp v18.0.6 with the tracked .omp/extensions: session_stop forced bin/fm-turnend-guard.sh twice (initial + native continuation OMP_TURNEND_CONTINUED marker) and fm_watch_arm_omp started/retired the watcher child across session_shutdown - PASS
  • tests/fm-omp-primary-extensions.test.sh - node-driven guard/watcher extension logic: guard returns typed FIRSTMATE_OP continuation and never sendMessage; retire-then-rearm generation ordering - PASS
  • tests/fm-session-lock-ancestry.test.sh - 13 cases incl. omp Claude-identity gated on CLAUDECODE=1 marker (never bare name), foreign-omp pid isolation, persisted .lock.omp-claude record trusted over ambient marker, pid-reuse identity rejection, interrupted marker-write leaves no lock - PASS
  • tests/fm-busy-adapter-wiring.test.sh - runs REAL fm-spawn to generate state/&lt;id&gt;.omp-ext.ts, then drives it with real fm-busy-event.sh + classifier: agent_start -> busy, agent_end willContinue -> busy, agent_end !ctx.isIdle() -> busy, genuine settle -> idle, turn_end stays a notification - PASS
  • launch_template omp &lt;kind&gt; - emits omp --approval-mode yolo -e __OMPEXT__ __MODELFLAG____EFFORTFLAG__&#34;$(__OPINPUT__ encode launch-brief &lt; __BRIEF__)&#34;
  • tests/fm-composer-lib.sh (34 oks, incl. capped omp composer shape), tests/fm-turnend-guard.test.sh (70 oks), tests/fm-supervision-instructions.test.sh (10 oks, omp snippet + repair line + ordinary-wake line), tests/fm-subagent-pretool-check.test.sh (14 oks, delegation-shape guard) - PASS
  • tests/fm-bootstrap.test.sh (28 oks) - MISSING-diagnostic assertions now use faked-only tasks-axi instead of real /usr/bin/node, and absent-CLI rows skip deterministically when an ambient zellij is in the run PATH; no-mistakes floor bumped to v1.46.0 - PASS
  • tests/fm-omp-primary-types.test.sh - SKIP by design: @oh-my-pi/pi-coding-agent not resolvable from npm root or the compiled omp binary on this host; the extension TS was instead type-loaded and executed by node (extensions suite) and by real omp v18.0.6 (live guard)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

kunchenguid and others added 30 commits August 25, 2026 15:14
* fix: delegate no-mistakes PR gate to pinned action

* no-mistakes(document): Document commit-bound no-mistakes attestations
…uid#3028)

* feat(pi): let operators pin a cheaper supervision-branch model

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's model. A new /supervision-model
command opens Pi's own selector over Pi's own catalog of credentialed
models, plus a "Follow main" entry, and persists the pick as one
<provider>/<model-id> line in this home's gitignored
config/supervision-branch-model. Firstmate keeps no model catalog of its
own.

The branch resolves the pin at every branch build - the first wake of a
cold start and the reopen after /new, /resume, /fork, or reload - so the
choice survives all of them, and picking also releases the live branch so
the next wake reopens the same persistent branch conversation under the
new model. An absent, unreadable, or unparseable file means no pin and
keeps today's behavior byte for byte: no model option is passed and Pi
picks the branch's model exactly as before.

A pin naming a model Pi cannot hand back is never silently downgraded
onto main's model: the branch refuses to build and the wake falls back to
the captain-facing main path naming the unusable pin, which is the
extension's existing failure direction.

The choice is home-local and not part of secondmate inherited
configuration, matching the Pi Calm preference precedent.

docs/configuration.md owns the operator-facing schema. Portable
regressions cover pin-present on create and reopen, pin-absent default,
the command's persistence, cancellation, and live rebind, and both
unusable and unparseable pins. The opt-in real-SDK guard proves the
vendor surface the pin reads and that an explicit model wins over the
model a reopened session recorded.

* no-mistakes(review): Fix supervision model runtime and rebind races

* no-mistakes(review): Restrict supervision picker to isolated runtime models

* no-mistakes(document): Document supervision branch model selection

* fix(pi): make the supervision model pin authoritative on every reopen

Clearing the pin with "Follow main" removed the file but the next branch
build reopened the persistent branch session with no explicit model
override, so Pi restored the model that session had recorded - the old
pinned model - while the command reported that the branch now follows
main. The same gap meant an absent pin did not reliably mean
same-model-as-main once a home had pinned once.

The pin file's current state now decides the model on every branch build,
create and reopen alike, overriding Pi's session-state restore. With a
pin, that model. With no pin, main's own current model is applied
explicitly, tracked from the contexts Pi already hands the extension plus
its model_select event, since the branch is built at wake time with no
context of its own. Only when main's model is unknown, or this home's
stored credentials cannot run it in the isolated branch runtime, does a
build fall back to passing no override at all, which is the behavior from
before the pin existed; the branch is never refused over model choice.

The command's notification now reports the model actually applied, and
says plainly when clearing the pin could not apply main's model instead
of claiming a change that did not take effect.

No credential handling changes: the branch still relies entirely on the
stored credentials its own runtime already holds, and the picker stays
restricted to models that runtime can resolve.

Colocated regressions cover pin present on create and reopen, clearing
the pin returning a reopened branch to main's model and specifically not
the old pinned one, an unparseable pin behaving as no pin, and the
unknown-main-model fallback to no override.

* no-mistakes(review): Make unpinned supervision follow main model changes

* no-mistakes(document): Correct supervision model documentation
…nguid#3079)

* feat(pi): let /supervision-model pick the branch's reasoning effort

Supervision is an easier job than the captain's own conversation, so the
Pi supervision branch does not need main's reasoning effort any more than
it needs main's model. /supervision-model now settles both in one flow:
the existing model picker, then a follow-up effort picker built from Pi's
own supported thinking levels for the model just chosen. Firstmate keeps
no effort catalog of its own; the menu, the clamp, and the vocabulary all
come from Pi.

The pick persists as one line in this home's gitignored
config/supervision-branch-effort, independent of the model pin: a captain
may pin a model, an effort, both, or neither. The effort pin's current
state decides the branch effort on every branch build - the first wake of
a cold start and the reopen after /new, /resume, /fork, or reload - and
overrides Pi's restore of whatever level a reopened branch session
recorded, which is what keeps "Follow main" honest. With no pin, main's
own current effort is applied explicitly and followed live through Pi's
thinking_level_select event, the same way an unpinned branch already
follows main's model, and the two selections now share one build revision
so either change invalidates an in-flight build.

The branch is never refused over effort. Pi owns the clamp, so a pinned
level the branch's model cannot run becomes that model's nearest supported
level while the captain's raw pick is kept for a model that supports it,
and the command reports the level the branch will really run at rather
than the raw pin. A token Pi would not recognize at all is treated as no
pin rather than passed to that clamp, which would otherwise collapse a
typo into the model's lowest level. Only when main's effort cannot be read
either does a build pass no effort override at all, which is the behavior
from before this file existed.

Pi's own effort vocabulary is pinned by a bidirectional type assertion
against Pi's getThinkingLevel return type, so the tracked strict typecheck
against the installed package fails the moment Pi adds or removes a level.

docs/configuration.md owns the operator-facing schema for both pins.
Portable regressions cover the pin on create and reopen, model-only and
effort-only pins working independently, clearing a pin returning the
branch to main's effort, live-follow of a mid-session change, the clamp,
an unrecognized token, the unknown-main-effort fallback, and the command's
two-step flow, persistence, cancellation, and honest reporting. The opt-in
real-SDK guard proves the vendor surface all of that rests on, and also
repairs a pre-existing gap that left it unable to load the extension at
all.

* no-mistakes(review): Resolve effective branch effort honestly

* no-mistakes(document): Clarify Pi-owned effort picker behavior
…3093)

* fix(supervision): silence empty board closes and decouple the heartbeat

Two unrelated sources of noise put routine supervision events in the
captain's chat.

An empty Lavish board close - the captain reads a review surface, says
nothing, and closes it - became a check wake whose entire content was
that nothing happened. Suppress it at its source instead of routing it
anywhere: the generic runner gains a `silent` adapter seam mirroring the
existing `terminal` one, and the Lavish adapter answers it for exactly
one positively-determined shape, an `ended` session carrying no queued
content block. A silenced result is recorded durably handled so it does
not return on a later reconcile. Everything else announces unchanged - a
`Send & End` close carrying the captain's real answer, an `ended` result
still carrying content, a waiting or missing session, an unreadable
result, and every adapter that implements no `silent` command at all.
The keyed-answer feed is untouched, so suppressing an announcement never
suppresses the captain's own answer.

A fleet heartbeat was deferred to main merely because some unrelated
check row happened to be sitting unread, which put a routine fleet
review in the chat for a reason that had nothing to do with the fleet. A
check row is permanently main-owned, so it is now excluded from a
heartbeat claim rather than vetoing the scan, exactly as in every other
mode. What all-or-nothing guarantees is unchanged: the branch takes
every branch-ownable unread row or none of them, and an unresolvable
task-local row, an unknown row kind, or an unreadable queue still defers
the whole review to main. Main is still woken for the check on that
check's own triggering close, so nothing starves.

Main-only classes are unchanged and now each covered by a test: Relay
mentions, credential failures, merge confirmations, real board answers,
and watcher-failure repair. The per-actor acknowledgement and
no-cross-swallow properties are untouched.

* no-mistakes(review): Fail closed on all Lavish content headers

* no-mistakes(review): Suppress false unacknowledged status for silenced results
kunchenguid#1967)

* fix(classify): read the decision fold through a correlation token

status_line_verb stripped a trailing [key=...] from a status line's prefix
but left everything else glued to the verb, so a line carrying the
correlation token bin/fm-pending-reply-lib.sh embeds and a secondmate
echoes back matched no arm of _fm_decision_fold_line. Such a line folded as
ordinary status in both directions: a needs-decision or blocked opener never
opened its key, and a resolved or captain-held closer never closed one. The
same glued verb also hid correlated done and blocked lines from
status_is_captain_relevant and status_is_terminal_verb, and let correlated
working and resolved lines leak through the free-text fallback the
nonterminal guard was meant to stop.

The verb parse now walks whole words and drops only a token of the exact
shape a firstmate library writes - corr=<16 hex>, plus the bracketed form
bin/fm-secondmate-report.sh emits - before or after the key token, unkeyed,
or doubled. An arbitrary name=value word is deliberately NOT skipped:
skipping unknown tokens would let free text carrying an equals sign reduce
to a bare verb and impersonate a transition, which is the takeover the
strict parse and _fm_decision_key_transition_allowed exist to prevent. A
prefix with no corr= substring is returned byte-for-byte as before, so every
line without a token keeps its exact historical verb.

FM_OPEN_DECISIONS_FOLD_VERSION goes to 3, because every cursor persisted
under the previous reading carries an open set computed while correlated
lines were invisible and must be rebuilt from byte 0.

Measured over a real 383-line status log: 254 lines keep byte-identical
captain-relevance, pause, terminal-verb and captain-held verdicts, and all
129 changed lines carry a valid token - 14 correlated done/blocked/
needs-decision lines become captain-visible, and 20 correlated
working/resolved lines stop being escalated on prose alone.

* fix(review): Captain, block token-first decision impersonation

* fix(document): Clarify normalized status verb ownership

* fix(classify): reconcile the correlation-token read with the tag-stop parser

Rebasing onto main put this change beside kunchenguid#2280, which made verb parsing
stop at the first "[name=value]" tag. Both edit status_line_verb with
different intents, so the resolution keeps both rules rather than letting
one overwrite the other:

- kunchenguid#2280's tag stop is kept verbatim and now owns every BRACKETED tag,
  including the "[corr=...]" form fm-secondmate-report.sh writes. The
  bracket-unwrapping arm this branch had added to the token test is
  therefore removed as unreachable.
- This branch's token walk is kept and narrowed to the UNBRACKETED token
  fm-pending-reply-lib.sh writes, which the tag stop does not reach.

Two consequences of standing beside kunchenguid#2280 rather than before it:

The fold version had collided at 4: kunchenguid#2280 spent it on the tag-stop
parser and this branch had spent it on the token read. A cursor
persisted under kunchenguid#2280's reading predates this one and must still be
rebuilt, so the version moves to 5.

A bracketed impostor is dropped from the malformed-token list. On main
today "resolved [corr=deadbeef] [key=victim]:" already reads as the bare
verb, as does "resolved [anything at all] [key=victim]:", because the tag
stop ends the parse at the opening bracket regardless of content. That is
kunchenguid#2280's reviewed contract; asserting otherwise here would narrow it. The
unbracketed impostors it owns stay strict and still fold as prose.

Adds a consumer test for the two verb-string case arms that postdate this
branch: fm-supervise-daemon.sh's transient-stale arm and
fm-crew-state.sh's map_log_state.

* fix(review): Captain: Seed cursor migration fixture with version four

* fix(document): Clarify voice status normalization ownership
…unchenguid#3115)

* fix(bin): Cursor-Park unter Pi-Host stilllegen.

pi-cursor-sdk lädt .cursor/hooks.json in die Pi-Sitzung und parkte einen zweiten Watcher; das erzeugte rearm-resurface und brach laufende Rückfragen ab. Bei PI_CODING_AGENT=true beendet der Park sofort, native cursor-agent Primaries bleiben unverändert.

* fix(bin): Cursor-Park trotz PI-Leak nur ohne Cursor-Identität stilllegen.

Stand-down gilt nur bei PI_CODING_AGENT=true ohne CURSOR_AGENT und ohne CURSOR_INVOKED_AS. Handgestartete cursor-agent Primaries mit geerbtem PI-Marker parken weiter.

* no-mistakes(document): Document Cursor park Pi-host stand-down

* fix(bin): no-mistakes-Mindestversion auf 1.46.0 anheben.

Die PR-Attestierung verlangt ab 1.46.0 strukturierte Pipeline-Schritte; der Bootstrap-Floor blieb bei 1.31.2 und ließ zu alte Builds zu.

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

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
…chenguid#3099)

* fix(pi): make /supervision-model's model list bounded and searchable

Pi's generic extension selector renders every option at once with no
search box, so a real eligible catalog ran off the top of the terminal.
The model step now draws the same rows through Pi's own SelectList - the
bounded scrolling primitive behind Pi's /model picker - with Pi's own
Input and fuzzy filter above it for search, keeping 'Follow main' first,
the branch-runtime eligibility filter intact, and the pick branch-only.

Pi's ModelSelectorComponent is deliberately not reused: its selection
handler writes the captain's default model through Pi's settings manager,
which would move main's conversation as a side effect of pinning the
branch. The effort step's menu is a handful of levels and stays on Pi's
plain selector dialog.

* no-mistakes(document): Clarify supervision picker documentation

* no-mistakes: apply CI fixes

* no-mistakes: apply CI fixes

* no-mistakes(document): Document searchable supervision model picker

* no-mistakes: apply CI fixes
* fix: make a landed merge leave a durable outcome

A merge was the one lifecycle event that left no record outside the
merging agent's memory. bin/fm-pr-merge.sh ended at the forge call, and a
home merging under standing authority never waits for the merge poll that
would otherwise confirm it, so three real merges reached the captain as
silence.

bin/fm-merge-outcome-lib.sh is the single owner of that record. A
secondmate home reports the landed PR upward on the same parent reply
channel its terminal-outcome backstop already uses; a main home records
it on the durable wake queue. The record is at most once per task and
canonical PR identity, and only a merge that actually landed produces one.

The merge poll feeds that same channel when it detects a merge this home
did not perform, so the captain's own forge merge and a merge firstmate
performed itself produce one consistent outcome instead of two reporting
paths. No new state file and no second watch path.

Two smaller gaps from the same failure:

- A mate charter listed its report triggers without naming a landed
  merge. Under standing merge authority nothing is ever "ready for
  review", so the enumerated list silently omitted the case that matters.
- A secondmate home seeded without its parent binding failed every
  terminal-outcome report for the same reason, and the diagnostic never
  named the binding. It does now.

* no-mistakes(review): Harden durable merge outcome reporting

* no-mistakes(review): Make merge race regression deterministic

* no-mistakes(review): Make merge outcomes retry-idempotent and forge-confirmed

* no-mistakes(review): Unify merge publication under canonical outcome marker

* no-mistakes(review): Publish merge outcomes before committing dedup markers

* no-mistakes(review): Document at-least-once merge outcome recovery

* no-mistakes(review): Use supported GitHub confirmation and update recovery docs

* no-mistakes(review): Preserve distinct merge wakes by PR identity

* no-mistakes(document): Document durable merge outcome semantics

* no-mistakes(test): Make merge outcome interleaving test deterministic

* no-mistakes(document): Clarify merge outcome documentation ownership

* fix(lint): keep the merge-outcome library an analysis boundary

bin/fm-watch.sh followed the new merge-outcome library's source graph,
which reaches the wake queue, PR identity, and secondmate parent
libraries. Expanding that inside an already-large lint root pushed
ShellCheck's external-source analysis past the bounded CI lint worker:
the Lint job was killed with SIGTERM after five silent minutes, twice,
having emitted no diagnostics at all.

Make it an analysis boundary, exactly as the transition and inbox owners
directly above and below it already are and for the same stated reason.
Coverage is unchanged because the library is a canonical lint root in its
own right and is still linted as one.

Measured locally: the watcher goes from not terminating within 120s to
9s clean, and the library alone lints in 1s clean.
…chenguid#3129)

* fix(bearings): keep an inventory-mismatch home readable, and mark warnings as repairs

A backlog-vs-metadata inventory mismatch inside a secondmate home was being
reported as "we cannot read that home", which discarded that home's open
captain calls, queued work, landed work, and live workers from the whole
Bearings digest. The main home already treats the identical mismatch as a
harmless disclosure; this makes the secondmate path agree.

- fm-fleet-snapshot.sh: the invalidity gate now passes orphan_in_flight,
  unowned_current, and terminal_in_flight through the partial-structured
  carve-out alongside child_current_unavailable, so those homes keep their
  decisions, holds, queued, landed, and live work and leave unreadable[].
  missing_backlog and unstructured_current stay on the discard path, because
  there the backlog itself is untrustworthy.
- fm-fleet-snapshot.sh: the same three kinds no longer collapse the home's own
  classification to "unknown"; the real captain_decision / active_child_work /
  externally_held classification survives and invalidity carries the warning.
  An unavailable child state still collapses it, including when a mismatch
  masks it under strict-invalidity precedence.
- secondmate_landed.partial now keys on partial-structured trust rather than an
  unknown state, so an inventory-mismatch home is still disclosed as partial.

Ask the home that owns the wrong books to fix them:

- bin/fm-secondmate-reconcile.sh sends exactly one reconcile instruction per
  mismatch episode through the ordinary steering transport. A persistent
  mismatch keeps its episode identity and never re-nags; a changed mismatch
  earns one more ask; a repaired one is forgotten so a recurrence is asked
  about again. The parent never touches the mate's own files, and a failed send
  records nothing so the next run retries it.

Give integrity warnings their own look on the board:

- charted rows take an optional kind of "queued" (the default) or "warning".
  A warning badges "needs repair" instead of "waiting" and is excluded from the
  Charted Next count, so alarms stop reading as dispatchable queued work. No
  fifth board section, and every existing payload stays valid.

Tests pin the new policy behaviorally: the retained surfaces and classification
for all three mismatch kinds, the still-discarding unstructured_current and
missing_backlog cases, the once-per-episode reconcile ask through real durable
steering records, and the board rendering exercised through the shipped
template under a minimal DOM shim.

* no-mistakes(review): Make reconcile dedupe atomic and warnings non-dispatchable

* no-mistakes(review): Preserve reconcile identity and reject stale snapshots

* no-mistakes(review): Order snapshots uniquely and canonicalize episode identities

* no-mistakes(review): Add fire-and-forget reconcile and separate warning overflow

* no-mistakes(review): Exclude fire-and-forget from escalation and track reconcile background

* no-mistakes(review): Run reconcile enqueue inline across all adapters

* no-mistakes(review): Track reconcile clears across strict-invalidity homes

* no-mistakes(review): Persist reconcile transitions atomically

* no-mistakes(document): Document reconcile and fire-and-forget contracts

* refactor(bearings): replace the reconcile episode dedupe with a 4-hour cooldown

The reconcile ask needed to fire once per problem without nagging on every
recap. The episode-precise record that tried to do that had to be correct in
every direction at once - order two concurrent snapshots, tell a repair from a
new problem, and never lose a clear - and each direction it got wrong either
swallowed a nudge or sent a duplicate.

A per-home cooldown removes the whole class. One durable timestamp per home,
one nudge per four hours, and nothing to get stale, mis-order, or
mis-classify: a home in mismatch is asked once, later recaps stay silent, and
a mismatch still sitting there after the window earns one gentle re-nudge.

- bin/fm-secondmate-reconcile.sh: state/<id>.reconcile-nudged holds the epoch
  second of the last ask; FM_RECONCILE_COOLDOWN_SECONDS names the window. The
  episode identity, ordering generation, pending/clear transitions, and
  delivery-identity reuse are all gone. A known-undelivered send starts no
  cooldown so the next run retries it; an unconfirmed one does, because a
  duplicate ask is worse than one the mate may already hold.
- bin/fm-fleet-snapshot.sh, bin/fm-bearings-snapshot.sh: drop the snapshot
  `observation` monotonic identity, which existed only to order those records.
- bin/fm-teardown.sh: retire the cooldown record with the endpoint's other
  runtime artifacts, so reseeding a retired id is not silenced by its
  predecessor's window.

The inline durable fire-and-forget send is unchanged, and the projection fix
and the warning surface are untouched.

Tests follow the behavior: the cooldown suite now pins one ask per window, the
re-nudge after it, the four-hour boundary, per-home independence, and that the
ask stays out of a re-ring ladder that still rings an ordinary steer beside it.
The obsolete observation-ordering test is deleted with the machinery it covered.

* no-mistakes(review): Serialize reconcile cooldown commits with mate lifecycle

* no-mistakes(review): Reject stale reconcile snapshots across mate reincarnations

* no-mistakes(review): Start reconcile cooldown after delivery completes

* no-mistakes(review): Keep reconcile sends nonblocking and remove pending residue

* no-mistakes(document): Document reconcile skip and stale-endpoint behavior

* no-mistakes(lint): Fix reconcile test subshell lint warning

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

* fix(bin): stop dropping reconcile nudges for markerless remote secondmates

A persistent remote secondmate's parent-side state/<id>.meta never carries
spawn_gen: bin/fm-spawn.sh's spawn_remote_secondmate() is its sole writer and
never writes one, because that incarnation identity does not apply to a
remote route. fm-secondmate-reconcile.sh's row filter required a non-empty
spawn_gen matching an identifier regex, so every such row was silently
dropped before the per-row loop ever saw it: no sent/stale/failed line, no
cooldown record, nothing sent, and no trace of why.

Give a legitimately markerless persistent remote secondmate a safe substitute
identity - its recorded remote_host - instead of weakening the spawn_gen
check for rows that do have a generation:

- bin/fm-secondmate-reconcile.sh: carry host through the row projection for
  both fm-fleet-snapshot.v1 and fm-bearings.v1 documents, and admit an empty
  spawn_gen instead of filtering the row out. A new revalidate_identity()
  compares the sampled spawn_gen against current metadata when one was
  sampled (unchanged), or the sampled host against the metadata's
  remote_host when none was sampled and the metadata still carries no
  spawn_gen of its own. A row with neither a spawn_gen nor a host has no
  safe identity at all and fails loudly instead of vanishing, exactly the
  visibility the original bug lacked.
- Rows now join on the ASCII unit separator rather than @TSV: bash's
  IFS-whitespace read collapses consecutive tabs, which would have silently
  dropped a legitimately empty field again.
- bin/fm-bearings-snapshot.sh: thread host through the secondmate_reconcile
  projection so the fm-bearings.v1 path (the one bearings itself feeds to
  the reconcile hook) carries the same substitute identity.
- tests/fm-secondmate-reconcile.test.sh: end-to-end coverage through the real
  remote transport (fm-on.sh + fm-remote-secondmate-control.sh against a
  genuinely seeded remote home) for a markerless mate nudged once per
  cooldown window, a stale/replaced remote route refused exactly like the
  existing local spawn_gen case, and a row with no identity at all failing
  loudly rather than being swallowed.

* no-mistakes(review): Enforce markerless remote host identity during final delivery

* no-mistakes(document): Document markerless remote reconciliation safety
…decorated (kunchenguid#3147)

* fix(watch): hand a busy declared pause to the away-mode daemon undecorated

While away mode is active the daemon owns triage and the watcher reverts to
one-shot, handing over plain wake identities the daemon classifies itself. The
busy-turn bound was the one stale path that did not: with afk active it ran the
wedge timer, so the daemon received a wake already decorated as a possible wedge.

That decoration outranks the daemon's own verdict. handle_wake escalates an
enriched wedge reason before its pause classification can apply, so a crew that
declared the wait itself - a `paused:` external wait or a verified captain-held
transfer holding a live foreground call - was wedge-escalated once per
FM_STALE_ESCALATE_SECS for as long as the wait lasted, the escalation count
climbing into demand-deep-inspection on a pane nobody needed to inspect.
Measured on the pre-fix tree, five consecutive re-arms produced five escalations.

busy_turn_bound_check now reads the declaration before the afk branch: away mode
hands off the plain window identity, one-shot per distinct stale hash, leaving
normal-mode pause bookkeeping unwritten because the daemon owns it there. The
daemon then classifies the wait itself and self-handles it on the long cadence.
Normal-mode behavior is unchanged, and lifting the declaration still restores the
busy-pane wedge escalation on the same pane.

The regression covers all three: the undecorated handoff with no wedge timer or
escalation counter, the one-shot on re-arm that the escalation ladder used to
climb, and the restored wedge escalation once the declaration is lifted.

* no-mistakes(review): key afk busy-pause handoff on declaration, clear wedge state

* no-mistakes(document): docs: scope away-mode busy-bound handoff to declared waits

* no-mistakes(document): docs: note afk busy-bound handoff in watcher header

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>
…unchenguid#3121)

* fix(bin): explain a pooled slot's stale submodule refusal

A pool slot whose submodule pin moved is refused with "is not clean; refusing
to discard uncommitted work", while the operator's own `git status` in that slot
reads clean. The message names no submodule, no pin, and no remedy, so the
refusal is unreadable and the slot looks wedged for no reason. That is the
failure that jammed three slots in a row when a submodule pin moved.

The refusal itself was never the bug and is unchanged: the gate still refuses,
and still touches nothing. It now distinguishes the one case it can prove and
says what it found - the submodule, the pin the slot has, the pin the base
records, and the command that clears it.

The diagnosis is deliberately conservative, because ` M <path>` alone cannot
tell a stale pin from real work. An entry is reported as stale only when every
reported entry is a gitlink whose submodule is internally clean and whose
recorded pin actually differs. A submodule holding uncommitted work, untracked
files, or an unpushed commit therefore keeps the original uncommitted-work
refusal, even when its pin is also stale - the remedy command would be wrong
there, and the conservative refusal is the safe answer.

Nothing is converged, synced, initialized, or deleted. There is no new failure
path: a slot that launched before still launches, a slot that refused before
still refuses, and projects that configure a submodule `ignore` are read exactly
as before. Paths are read with core.quotePath=false so a non-ASCII submodule is
named rather than falling back to the unreadable message.

Tests keep the reproductions that prove the message is accurate: the stale-pin
diagnosis (which fails against the previous refusal), work inside a submodule
still refused as uncommitted work, and a stale pin carrying real work refused
conservatively rather than called stale. Each asserts the slot is left untouched.

* no-mistakes(review): require remote containment before calling a submodule pin stale

* fix(bin): stop printing a remedy the containment check cannot stand behind

The stale-pin diagnosis printed `git submodule update --checkout` as the command
that clears the slot. The containment check behind it reads local refs only and
never fetches, because this gate has to stay usable offline. A remote-tracking
ref that has gone stale - its upstream branch deleted or force-pushed, and never
pruned - still reads as containment, so a commit that is really unpushed can look
contained and that command would move the submodule off it.

Naming the submodule and both pins is the whole point of the diagnosis: it turns
"is not clean", on a slot whose own `git status` reads clean, into a statement of
which submodule drifted and where it drifted from. The operator can choose the
remedy from that, seeing the whole picture. Printing an instruction that rests on
a judgement which can be fooled is worse than printing none, so it is dropped.

The limitation is now stated where it applies, in the script header and beside
the check itself, rather than left for a reader to discover.

No fetch is added: the gate stays offline-safe by design. Nothing else changes -
the same conditions are refused, the slot is still never touched, and a submodule
carrying real work or an unpushed commit still keeps the conservative
uncommitted-work refusal.

* no-mistakes(review): bound submodule containment probe to first commit
* fix(pi): type captain supervision outcomes so main relays them

A captain-relevant branch outcome reached main as a bare user message with
no marker of origin or required action, written in main's own captain-facing
voice, landing in a tail that often already held several such notes. Pi keeps
only a custom message's content when it builds the provider request, so
customType and display never reach the model and content was the only place
that identity could live.

Main could not tell an incoming outcome from its own earlier answer and
sometimes re-emitted that answer instead of relaying the outcome, losing it.
Measured against real Pi 0.84.1 on openai-codex/gpt-5.6-sol: 6 failures in 24
turns, rising to 3 in 6 once one stale answer was already in the tail, which
is how one captain conversation saw six identical messages in a row. The same
scenario with the outcome typed failed 0 times in 14 turns.

Wrap only the captain-verdict note in the branch-outcome operational kind
owned by bin/fm-operational-input.sh. Delivery is otherwise unchanged: still
display: false, still one triggerTurn follow-up, so the turn remains the
single captain-visible outcome and no hidden note is ever shown twice.
Routine notes stay plain because their renderer reads the glyph off the front
of that same string. An outcome that cannot be encoded degrades to the same
instruction as plain text rather than being lost, matching this file's stated
failure direction.

The existing assertions could not catch this: they pin the sendMessage
options and never look at what main receives. Add a portable regression that
classifies the delivered payload with the real protocol executable, and a
live guard that runs the real Pi SDK's own convertToLlm to prove content is
the entire model-visible payload.

* no-mistakes(document): Document typed Pi captain outcomes
… or enriched wedge (kunchenguid#3155)

* fix(bin): keep a busy pane from retiring a still-declared wait's window

The away-mode daemon's pause re-surface recheck (housekeeping step 2b) read a
busy pane as "the crew resumed" and dropped the declared-wait marker, without
re-reading that the crew's own latest status line still declared the wait.

That inference is not safe, because a declared wait can legitimately hold a pane
busy: a worker sitting on a long foreground call keeps that call live for as long
as the wait lasts. The marker is then cleared while the declaration still stands,
and migrate_watcher_pause_markers recreates it with a fresh timestamp on the very
next tick, so the window restarts forever and the wait never matures into its one
bounded recheck.

Away mode makes that terminal. Since the watcher half landed, a busy pane under a
declared wait is handed to the daemon exactly once per declaration and never woken
again while the declaration stands (bin/fm-watch.sh, busy_turn_bound_check), so
this recheck is the only thing left that can re-surface the pane at all. Measured
end to end on a throwaway state root, away mode active, a pi pane busy past
FM_BUSY_TURN_MAX_SECS, status still `paused:`, over six PAUSE_RESURFACE_SECS
windows: 0 captain-facing rechecks before this change, 6 after - one per window,
with the marker reset each time.

The fix drops only the busy arm of the 2b probe, leaving it an endpoint-readability
check: exit code 2 still means the capture failed, so the endpoint is gone and the
marker goes. The loop head above already drops the marker the moment the status
line stops declaring the wait, so nothing else is needed to end the routing, and
the reconcile path runs before the probe ever reads a pane.

tests/fm-daemon.test.sh: test_housekeeping_paused_resumed_cleared pinned the old
inference on purpose - its fixture's status line still read `paused:` while the
pane was busy, and its comment read "A pause whose pane became busy again (the
crew resumed)". Its fixture now resumes the way a crew actually resumes, by
appending a non-declaring status line, and it asserts its own busy verdict first
so it cannot silently decay into the idle-pane case that
test_housekeeping_paused_unpaused_cleared already covers. What it pins is now the
inverse guard: a busy pane must not GATE the clear either, so an over-correction
that kept the marker alive whenever the pane is busy would fail it.

test_housekeeping_busy_declared_wait_matures_its_window is the new regression, over
both declaration forms. It asserts the busy verdict, then that ticks inside the
window neither escalate nor let the marker be recreated with a fresh timestamp,
then exactly one recheck past the window named for the right human and never a
wedge, then silence on the next tick inside the reset window. It fails on
unmodified main with "produced 0 escalations past its window, expected exactly
one".

Refs kunchenguid#3149

* fix(bin): let a declared wait outrank an enriched wedge escalation

handle_wake classifies a stale wake through classify_stale, which returns a
`pause` verdict for a crew whose latest status line declares an external wait or
a verified captain-held transfer. It then threw that verdict away whenever the
wake reason matched `idle *s, possible wedge, escalation *`, so the watcher's
enriched wedge decoration outranked the crew's own declaration and a healthy
declared wait was escalated once per FM_STALE_ESCALATE_SECS for as long as the
wait lasted.

The enriched reason earns its precedence over the daemon's cheaper status-log
absorption honestly - it carries the watcher's escalation count and its explicit
"do not re-absorb on the run-step/pane state alone" demand. A `pause` verdict is
not run-step or pane state. It is the crew's own declaration that this pane waits
by design, which is precisely the question the wedge timer cannot answer for
itself, so it is the one verdict that decoration must not override.

The two classifications genuinely disagree in steady state rather than only in a
race: a crew that declares `paused:` while its no-mistakes run is still attributed
to its code reads `working` to the watcher's pause_state_class, so the watcher
takes the wedge timer while the daemon's classify_stale reads the status log and
correctly returns `pause`.

The wait stays bounded, not silenced. Absorbing to the pause action records the
declared-wait marker and drops wedge aging, and housekeeping (2b) then owns the
re-surface, so the pane still reaches the captain - once per PAUSE_RESURFACE_SECS
as an explicit "recheck whether the wait still holds", instead of once per
FM_STALE_ESCALATE_SECS as a possible wedge. Measured on a throwaway state root
over five wedge cadences for one declared wait: 5 escalations climbing to
demand-deep-inspection before this change, 0 after, with the one bounded recheck
still delivered.

tests/fm-daemon.test.sh: test_stale_diagnostic_wedge_survives_busy_housekeeping's
`paused` case pinned the old precedence on purpose, asserting exactly one
escalation carrying the demand-deep-inspection payload. That case now asserts the
pause cadence instead - no escalation inside the window, pause tracking recorded -
while the `working` and `prior-terminal` cases keep asserting the enriched wedge
verbatim, so the override itself is still pinned everywhere it is correct.

test_enriched_wedge_under_declared_wait_uses_pause_cadence is the new regression.
It asserts the fixture's own classifier verdict is a pause first, so the case
cannot go vacuous, then drives four consecutive wedge-cadence deliveries in both
the plain and demand-deep-inspection forms through the real handle_wake and
housekeeping pair, then matures the window for exactly one awaiting-external
recheck, then lifts the declaration and requires the same enriched wedge to
escalate again unchanged. It fails on unmodified main at the first delivery.

Refs kunchenguid#3149

* no-mistakes(review): align afk skill recheck wording with still-declared contract

* no-mistakes(document): daemon doc comments: pause window ages on declaration

---------

Co-authored-by: Talon Stark <talonstark@gmail.com>
* fix(bin): make Claude auto-arm continuity self-heal past a hung claim

On a Claude primary, a Stop-hook auto-arm process that hung mid-arm held
the single-flight owner lock with its epoch ledger frozen at
outcome=arming, and the abandonment proof read any live lock holder in
arming as legitimately deciding forever. Every later Stop firing exited
0 at the lock, the turn-end guard kept deferring to the hung owner as
recovery under way, and the watcher was never auto-re-armed again for
the rest of the session - supervision survived only on manual arms and
lapsed between them (the 2026-08-26 watcher flap).

Corrections layered onto the lock-held-across-arm shape each reopened
the same concurrency class one level down, so this replaces the claim
machinery wholesale with a generation-based optimistic design:

- The epoch ledger's monotonic sequence IS the claim generation; the
  two-line entry (classic epoch record plus the claimant's MANDATORY
  pid-identity) is the claim. Every firing defers to a live OPEN claim:
  outcome arming, owner alive, identity recomputes and matches, and not
  stuck (entry and watcher beacon both older than the guard grace).
- A finished, dead, identity-mismatched, identityless, or stuck claim
  is superseded by simply taking the next generation - no signalling or
  revocation of a steady-state predecessor.
- No mutex is held across arming or output; the owner lock survives
  only as a micro-mutex around individual ledger writes. A superseded
  owner goes completely silent: ownership is re-verified before every
  arm invocation, episode-state mutation, ledger write, and
  continuation.
- The irrevocable commit point of a translation is the exit status
  (the harness delivers the collected stderr only on exit 2), so the
  owned terminal ledger write is the atomic commit: the winning
  generation exits 2 unconditionally after it, a refused one exits 0
  silently even after printing, and the once-per-episode failure notice
  commits in the same owned critical section as the winning failed
  write. Two bounded residuals are documented accepted intent: an owner
  dying between its owned write and its own exit, and a hung old-build
  owner resuming during the one legacy upgrade window.
- The pre-generation lock-holding claim shape keeps defer-or-reclaim
  behavior through a legacy shim: a live identity-verified stuck owner
  is retired via TERM (with a queued TERM sufficient when the owner is
  stopped) before its lock is removed, an unverified or identityless
  pid is never signalled but never blocks a proven-abandoned reclaim,
  and the lock's identity evidence is grafted into the ledger
  (mtime-preserving) so pid-reuse protection survives the lock.
- The guard reads the same predicates for recovery ownership and its
  terminal fail-open (which re-checks for a live open claim under the
  held locks before committing the attended alarm), with ledger reads
  anchored to line 1 so the identity line can never confuse them.

Behavioral regression coverage exercises all three edge classes through
the real hook and guard - a live open claim defers with no lock held, a
stuck claim is superseded and the home re-arms, and an end-to-end run
with a genuinely hung owner shows a concurrent firing deferring
promptly mid-arm, a later firing superseding the stuck owner, and the
superseded owner exiting silently without a second translation - plus
the identityless/reused-pid loopholes, the superseded-owner arm
boundary, and the legacy TERM, SIGSTOP, and signal-free reclaim paths.

* no-mistakes(review): Refuse auto-arm commits when notice marker creation fails

* no-mistakes(review): Make episode reset atomic with generation ownership

* no-mistakes(document): Update auto-arm generation and commit documentation
# Conflicts:
#	AGENTS.md
#	docs/configuration.md
omp (Oh My Pi, @oh-my-pi/pi-coding-agent) sets both OMPCODE=1 and
CLAUDECODE=1 on every bash-tool child process, so checking CLAUDECODE
first silently misidentified omp sessions as claude. Test OMPCODE
first, same precedence-fix pattern already used for the Cursor marker
collision in this file, and add ancestry-based detection as a second
layer. Also record the empirically verified (but partial) Oh My Pi
adapter facts in harness-adapters so dispatch continues to fail closed
until a launch template and busy-state contract land in a follow-up.

(cherry picked from commit 12e8fad585c58e8d3b063f05e5779c76a203c93e)
…-dispatch.json

Verified live 2026-08-24 on omp v18.0.4: interrupt (single Escape),
exit (/exit), and a full real crew dispatch smoke test (spawn, run,
report, exit, teardown) on the openai-codex/gpt-5.6-luna model.

- bin/fm-spawn.sh: omp launch template (--approval-mode yolo,
  --model, --thinking), model/effort flag wiring, and an explicit
  --secondmate refusal on omp (no primary supervision protocol yet,
  same as muse).
- bin/fm-control-lib.sh: omp control-plane mechanics (interrupt key,
  exit command, kind restriction) mirroring the verified facts.
- .agents/skills/harness-adapters/SKILL.md: omp moved from
  partially-verified to VERIFIED CREWMATE/SCOUT, with the live
  evidence and the model-role-mapping rationale.
- AGENTS.md: omp added to the verified-harness list, crewmate/scout
  only.

config/crew-harness and config/crew-dispatch.json (local, gitignored)
now route crew dispatch to omp, with each dispatch rule's model
selected from omp's own configured modelRoles (~/.omp/agent/config.yml)
per captain instruction, superseding the 2026-08-21 claude-only
restriction for omp dispatch.

(cherry picked from commit 16cc445d95fa677ee6a6dd5b647fe51eedd6e60e)
omp had no working busy-state hook and no composer mapping, so Herdr's
agent panel and fm-crew-state.sh could never show a real state for an
omp crewmate/scout once idle. Both gaps are closed:

- bin/fm-spawn.sh: omp's launch template now loads a per-task
  state/<id>.omp-ext.ts extension via an explicit -e path, mirroring
  the pi-ext pattern. omp's extension API is Pi-compatible but its
  settle event is agent_end (not agent_settled) and carries its own
  willContinue flag; the extension goes idle only when willContinue is
  falsy AND ctx.isIdle() also confirms it, the same double-check Pi's
  extension uses. Verified live end-to-end against the real omp v18.0.4
  binary: busy mid-turn, idle only at genuine settle, turn_end staying
  a notification-only touch.
- bin/fm-busy-lib.sh: fm_busy_sources_for_harness now trusts the
  omp-ext source for harness=omp.
- bin/fm-composer-lib.sh: taught the shared screen classifier omp's
  real composer shape (composer.shape=box, live-verified): a top row
  that permanently embeds a status/title line and a bottom row that is
  simultaneously the closing rule and the last content row. This needed
  a new "capped" shape alongside the existing bordered/bare/left-bar/
  separated catalogue, since the generic bordered shape's
  content_rows>0 gate always misses the common zero-interior-row case.
  Also registered omp's busy-indicator delivery regex (a stable
  trailing "esc" cancel-hint token; the spinner's working-message text
  is dynamic and never stable enough to match).
- bin/fm-teardown.sh: clean up the per-task omp-ext.ts file alongside
  the other per-adapter artifacts.
- tests/fm-busy-adapter-wiring.test.sh: drive the real fm-spawn-
  generated omp extension through a plain Node host and the real
  fm-busy-event.sh writer (agent_start, willContinue, ctx.isIdle(),
  turn_end, ordering, stale-incarnation rejection).
- tests/fm-composer-lib.test.sh: matrix coverage for the new capped
  shape (idle, typed, wrapped, extraction fidelity).
- harness-adapters skill: omp's Busy state row and the "remaining
  work" note updated to match the verified reality, with the exact
  verification boundary disclosed (a full nested fm-spawn dispatch
  from inside this task's own isolated worktree was not run, to avoid
  mutating the shared firstmate home; the live binary round-trip and
  the automated real-extension-file tests are what was actually done).

Full existing composer-lib, composer-ghost, busy-state,
busy-adapter-wiring, backend-herdr, tmux-submit-busy, and
spawn-dispatch-profile suites re-run with zero regressions.

(cherry picked from commit bed42b8fe5b00260bfcde2b30141e92804670ffe)
(cherry picked from commit 7e01c77fd97cf94ee295deb3a5745801c3c3546f)
owner.restoring stayed true through the await sendWake(...) call after
a restore attempt concluded (success or exhausted retries), so if the
confirmed successor watcher exited during that window its own close
handler saw restoring still true and silently dropped the failure,
leaving watcher supervision down with no scheduled retry. Clear the
flag as soon as the retry loop's outcome is decided, before the async
notification, so a close event racing with wake delivery is handled
normally.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
omp (Oh My Pi) runs on the same Claude Agent SDK and sets CLAUDECODE=1,
but its process is named "omp" with no separate "claude"-named ancestor,
so fm-session-lock-lib.sh's ancestry walk could never locate it and every
omp-run firstmate primary stayed permanently read-only. Gate the
recognition strictly on the CLAUDECODE=1 marker, never a bare name match,
scoped to session-lock identity only.
… pids

Greptile flagged that fm_harness_process_matches's omp branch read
$CLAUDECODE from the checking process's own environment even when
classifying an unrelated pid pulled from a lock file - a live
non-Claude omp holder checked from a Claude session would be
misclassified as alive, and a genuinely live Claude-backed omp
session checked from an unmarked context would look stale and lose
its lock.

The marker is only sound evidence for a pid that is a verified member
of the checker's own process ancestry, since env vars are inherited
top-down. fm_harness_ancestry_pids now passes self=1 only for pids it
discovers by walking $$'s own kernel-reported ppid chain, and
fm_harness_pid_alive computes self by checking membership in that same
walk before trusting the marker for a lock-file pid, rather than
assuming self from the caller's context.
indrasantosa and others added 8 commits August 27, 2026 13:29
…require

The harness-dependent CLAUDECODE-marker check landed with only a portable
regression against a stubbed ps. Add the required live-harness-optin
counterpart, driving a real installed omp process and its own bash tool to
confirm the two vendor facts the check depends on: that omp's own comm
reports base name "omp", and that a real omp session's own process
environment carries CLAUDECODE=1. Record the dated result and update the
harness-adapters note to reflect the self/foreign-pid distinction already
shipped in bin/fm-session-lock-lib.sh and bin/fm-lock.sh.
…olation

- tests/fm-omp-harness.test.sh (detection matrix, from pr-2935/pr-2503)
- tests/fm-omp-primary-types.test.sh (strict no-emit typecheck of the
  .omp/extensions primary stack, adapted from pr-2503)
- OMPCODE isolation for cursor/kimi/secondmate harness tests and the
  shared test lib, so suites run correctly under an omp parent host
- wire the delegation-shape subagent guard into the omp turn-end
  extension (both upstream OMP PRs omitted it)
…e reviewed designs

- fm-harness.sh: grok marker precedes OMPCODE (cursor > grok > omp >
  claude > pi), matching the documented adapter precedence and the
  detection matrix
- fm-session-lock-lib.sh: omp stays OUT of the harness tables; its lock
  identity is marker-gated (CLAUDECODE=1 on the self ancestry walk)
  only, per the reviewed session-lock design, not a bare name/path match
- fm-omp-harness.test.sh: pin the marker-clean ancestry walk with a fake
  ps so the unknown verdict is deterministic when the suite itself runs
  under omp
- fm-kimi-harness.test.sh: drop ambient OMPCODE in the detection cases
… evidence for omp 18.0.6 (2026-08-27 Linux)
- fm-session-start/fm-bootstrap: assert on MISSING: tasks-axi instead of
  MISSING: node, which a host's own /usr/bin/node silently satisfies
- fm-bootstrap: skip the absent-CLI row for a backend whose real CLI sits
  in the run PATH (presence-only availability check), so hosts with
  /usr/bin/zellij do not fail a scenario they cannot reproduce
@frasdl
frasdl merged commit 7844658 into main Aug 27, 2026
13 checks passed
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.

8 participants