Skip to content

feat(bin): add OMP harness support and share the primary watcher core - #1580

Closed
dnth wants to merge 68 commits into
kunchenguid:mainfrom
dnth:fm/omp-watcher-rebind-fix-v2
Closed

dnth wants to merge 68 commits into
kunchenguid:mainfrom
dnth:fm/omp-watcher-rebind-fix-v2

Conversation

@dnth

@dnth dnth commented Aug 3, 2026

Copy link
Copy Markdown

Intent

Make the already-landed OMP daily-driver branch safe across primary watcher-core factory re-bindings. Preserve one shared active watcher generation and one process-exit listener ownership path across factory re-bindings. A factory re-binding must retire or supersede the old core so it cannot remain live, start a duplicate arm child, or accumulate exit listeners. Preserve current OMP and Pi behavior outside this defect. Extend the existing behavioral test surface in tests/fm-pi-watch-extension.test.sh or the authoritative watcher-core owner; the regression must fail on current main and pass with the correction, covering factory re-binding, duplicate arm prevention, and exit-listener ownership. Run the focused watcher-core, OMP, and Pi tests plus the repository lint required for changed tracked files. Keep the final diff limited to the correction and its behavioral coverage, with no .omc runtime artifacts or unrelated changes. Deliver through the configured no-mistakes PR path against dnth/firstmate:main and do not merge the PR. Use the prior unpublished validation commit only as evidence; do not copy its .omc artifacts, unrelated ignore changes, PR-polish changes, or old task chronology.

What Changed

  • Extracts the Pi primary watcher into a shared bin/fm-primary-watch-core.ts and binds both .pi/extensions/fm-primary-pi-watch.ts and the new .omp/extensions/fm-primary-omp.ts to it. The core keeps exactly one active generation and one process-exit listener across factory re-bindings, and a superseded binding can no longer reclaim ownership, kill the live arm child, or accumulate exit listeners.
  • Adds OMP as a supported harness across the toolbelt: capability and process probes (bin/fm-omp-capabilities.sh, bin/fm-omp-process-lib.sh), harness/backend/spawn/send/session-start wiring for primary, secondmate, and worker roles, and tmux plus Herdr backend support including OMP busy-footer detection.
  • Adds OMP fixture and live e2e suites (tests/fm-omp-*.test.sh), a factory re-bind regression in tests/fm-pi-watch-extension.test.sh, and documents the protocol in docs/supervision-protocols/omp.md plus the backend, verification, and AFK/harness-adapter skill docs.

Risk Assessment

✅ Low: The new work over the already-landed base is a 5-line binding-token guard plus its direct regression coverage, it closes the round-1 ownership gap at the shared boundary without altering any live-binding path, and no residual reachable route for a superseded core to reclaim the active generation or the exit listener remains.

Testing

Ran the focused watcher-core, OMP, and Pi suites plus a targeted red/green proof of the new regression. The new re-bind test fails with the pre-fix watcher core (duplicate process-exit listener, superseded arm child still alive) and passes with the correction; a manual side-by-side demo shows the operator-visible difference — before the fix a factory re-bind leaves two live arm children, two exit listeners, and a superseded binding that still claims ownership, after the fix one arm child, one exit listener, and the superseded binding refuses to arm. OMP primary/secondmate/harness, Pi calm, Pi-compatible family, and watcher-lock suites are green. Two failures in the focused set (the OpenCode session-lock case and the Pi type-check suite) reproduce identically on a clean checkout of upstream main and touch no file in this diff, so they are pre-existing rather than regressions. No visual/UI surface is involved — the change is a CLI/daemon watcher lifecycle fix, so evidence is CLI transcripts of arm-child and exit-listener state.

Evidence: Watcher re-bind before/after transcript (arm children + exit listeners)

=== BEFORE FIX (watcher core at b6fe376 = pre-fix main) === after bind #1: live arm children=[547546] exit listeners=1 --- runtime re-binds the watcher factory (no session_shutdown) --- after bind #2: live arm children=[547546] exit listeners=2 superseded bind arm -> ok=true msg="watcher: unchanged - Pi extension already owns an arm child..." after bind #2 arms: live arm children=[547546,547615] exit listeners=2 === AFTER FIX (watcher core at 9b4e407) === after bind #1: live arm children=[541556] exit listeners=1 --- runtime re-binds the watcher factory (no session_shutdown) --- after bind #2: live arm children=[] exit listeners=1 superseded bind arm -> ok=false msg="watcher: not armed - Pi session is shutting down" after bind #2 arms: live arm children=[541634] exit listeners=1

=== BEFORE FIX (watcher core at b6fe376 = pre-fix main) ===
bind #1 arm  -> ok=true msg="watcher: started Pi extension arm child 1; future ordinary re-arms are automatic; call fm_watch_arm_pi again only after a later notification says the cycle is missing, failed, or unhealthy"
after bind #1: live arm children=[547546] exit listeners=1
--- runtime re-binds the watcher factory (no session_shutdown) ---
after bind #2: live arm children=[547546] exit listeners=2
superseded bind arm -> ok=true msg="watcher: unchanged - Pi extension already owns an arm child; no manual re-arm needed; call fm_watch_arm_pi again only after a later notification says the cycle is missing, failed, or unhealthy"
bind #2 arm  -> ok=true msg="watcher: started Pi extension arm child 1; future ordinary re-arms are automatic; call fm_watch_arm_pi again only after a later notification says the cycle is missing, failed, or unhealthy"
after bind #2 arms: live arm children=[547546,547615] exit listeners=2
=== AFTER FIX (watcher core at 9b4e407) ===
bind #1 arm  -> ok=true msg="watcher: started Pi extension arm child 1; future ordinary re-arms are automatic; call fm_watch_arm_pi again only after a later notification says the cycle is missing, failed, or unhealthy"
after bind #1: live arm children=[541556] exit listeners=1
--- runtime re-binds the watcher factory (no session_shutdown) ---
after bind #2: live arm children=[] exit listeners=1
superseded bind arm -> ok=false msg="watcher: not armed - Pi session is shutting down"
bind #2 arm  -> ok=true msg="watcher: started Pi extension arm child 1; future ordinary re-arms are automatic; call fm_watch_arm_pi again only after a later notification says the cycle is missing, failed, or unhealthy"
after bind #2 arms: live arm children=[541634] exit listeners=1
Evidence: Regression fails on pre-fix watcher core

not ok - A fresh factory bind must supersede the prior binding and keep one exit fallback: expected exit 0, got 1 FM_TEST_SUMMARY total=1 failed=1

FM_TEST_BEGIN 2026-08-03T10:57:17Z tests/fm-pi-watch-extension.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - Pi extension reports external healthy watcher output
ok - Pi custom tool exposes repair-only metadata and returns automatic-continuation guidance
ok - Pi redundant tool call returns ownership guidance and spawns no second child
ok - Pi scheduled retry remains extension-owned after another tool call
ok - Pi actionable close starts one successor before wake delivery settles
ok - Pi hung successor falls back to one typed actionable wake
ok - Pi unretired successor falls back without an overlapping retry
ok - Pi late unretired closes resume classified supervision
ok - Pi clean empty close triggers a bounded continuity retry
ok - Pi established clean closes stop at the configured retry limit
ok - Pi close handler verifies session-lock ownership before successor launch
ok - Pi watcher arm distinguishes all session lock ownership states
ok - Pi session transitions use a generation owner across /new /resume /fork, stale callbacks, and quit
not ok - A fresh factory bind must supersede the prior binding and keep one exit fallback: expected exit 0, got 1
FM_TEST_END 2026-08-03T10:57:28Z tests/fm-pi-watch-extension.test.sh exit=1 duration_ms=10306 gate_skip=false
FM_TEST_SUMMARY total=1 failed=1 skipped_gate=0 duration_ms=10353
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=10306 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-pi-watch-extension.test.sh duration_ms=10306
Evidence: Pre-fix scenario detail (listener count + un-retired child)

PREFIX-NOTE: exit listeners after 2nd bind = 2 (expected 1) Error: timeout waiting for superseded bind child exit

PREFIX-SCENARIO-OUTPUT: PREFIX-NOTE: exit listeners after 2nd bind = 2 (expected 1)
file:///tmp/fm-prefix-check/[eval1]:35
  throw new Error(`timeout waiting for ${label}`);
        ^

Error: timeout waiting for superseded bind child exit
    at waitFor (file:///tmp/fm-prefix-check/[eval1]:35:9)
Evidence: Focused watcher-core / OMP / Pi suite run at 9b4e407

ok - Pi factory re-bind without shutdown retires the prior generation and exit fallback stays singular ok - Pi process-exit cleanup listener remains singular across session replacement

FM_TEST_BEGIN 2026-08-03T10:53:22Z tests/fm-pi-watch-extension.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - Pi extension reports external healthy watcher output
ok - Pi custom tool exposes repair-only metadata and returns automatic-continuation guidance
ok - Pi redundant tool call returns ownership guidance and spawns no second child
ok - Pi scheduled retry remains extension-owned after another tool call
ok - Pi actionable close starts one successor before wake delivery settles
ok - Pi hung successor falls back to one typed actionable wake
ok - Pi unretired successor falls back without an overlapping retry
ok - Pi late unretired closes resume classified supervision
ok - Pi clean empty close triggers a bounded continuity retry
ok - Pi established clean closes stop at the configured retry limit
ok - Pi close handler verifies session-lock ownership before successor launch
ok - Pi watcher arm distinguishes all session lock ownership states
ok - Pi session transitions use a generation owner across /new /resume /fork, stale callbacks, and quit
ok - Pi factory re-bind without shutdown retires the prior generation and exit fallback stays singular
ok - Pi process-exit cleanup listener remains singular across session replacement
ok - Pi process-exit cleanup stops the attached arm child
ok - OpenCode plugins have an explicit ESM boundary even under a typeless parent package
ok - OpenCode watcher plugin uses the effective FM_HOME state
ok - OpenCode watcher plugin sources the effective config
not ok - OpenCode watch plugin must arm only when this session owns the fleet lock: expected exit 0, got 1
FM_TEST_END 2026-08-03T10:53:40Z tests/fm-pi-watch-extension.test.sh exit=1 duration_ms=18072 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:53:40Z tests/fm-omp-primary.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - OMP path resolution stays canonical when readlink -f is unavailable
ok - OMP primary identity requires launch-bound Bun and OMP realpaths plus the exact argv boundary
ok - exact-OMP ancestry stops at the innermost foreign harness ancestor
ok - OMP fresh primary lifecycle creates canonical state and atomically replaces a marker symlink without following it
ok - native OMP alone admits a fresh plain checkout and delivers one startup instruction
ok - OMP primary refuses whitespace-bearing identity before marker publication
ok - OMP native extension binds startup, guarded stop, watcher, safety, marker, and shutdown surfaces
FM_TEST_END 2026-08-03T10:53:44Z tests/fm-omp-primary.test.sh exit=0 duration_ms=4299 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:53:44Z tests/fm-omp-harness.test.sh family=pure-contract-unit expected_gate_skip=none
ok - OMP worker tools preserve the exact launch-boundary harness identity
ok - OMP capability probe accepts the required launch and recovery surface
ok - OMP capability probe enforces the Bun process identity used by session ownership
ok - OMP capability probe names each missing launch or recovery requirement
ok - selected OMP refuses instead of falling back to another harness
FM_TEST_END 2026-08-03T10:53:45Z tests/fm-omp-harness.test.sh exit=0 duration_ms=400 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:53:45Z tests/fm-calm-pi-extension.test.sh family=pure-contract-unit expected_gate_skip=none
ok - Pi calm resolves its persistent home independently of Pi's launch directory
ok - Pi calm compatibility evidence never rejects a Pi version for being newer than 0.82.0, and still fails closed on a missing or malformed version
ok - a missing collapsed-thinking presentation API degrades only that Calm adapter with a clear skip reason, while the rest of Calm still registers
ok - missing Pi presentation class exports reach the independent adapter degradation path
ok - Pi calm centralizes transcript visibility, preserves execution/export data, keeps Pi's stock working row visible while no run is active, and persists its choice across session starts
ok - Pi operational follow-up E2E processes exact user-role notifications once while Calm hides current and adjacent rows, Calm off and absent render them, and restart preserves semantics
ok - Pi Calm native /skill:ahoy geometry keeps every collapsed thinking and tool block at zero height while preserving expansion, history, restart, and Calm-off rendering
ok - Pi Calm working ship moves on a slow independent cadence over faster fixed-cell blue water, paints the complete boat standard yellow with balanced resets, keeps ANSI-stripped width exact, flips the directional sail on the exact bounce at both edges and every width, clamps visible and hidden resizes, falls back deterministically when narrow, freezes and resumes column/direction across settle/start without hidden-time jumps or duplicate timers, resets only on a fresh session, and installs and removes one scheduler-owning widget across starts, settle, abort, failure, shutdown, reload, replacement, and Calm toggles while leaving Calm-off visibility untouched
ok - Pi calm native E2E replaces the stock working row with a moving, resize-clamped working ship that freezes and resumes across two working periods in one Pi session, clears on abort, keeps captain turns visible, hides exact operational user rows without changing persistence, restores stock rendering Calm-off, survives restart, and preserves export plus Ctrl+O behavior
FM_TEST_END 2026-08-03T10:55:00Z tests/fm-calm-pi-extension.test.sh exit=0 duration_ms=75614 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:55:00Z tests/fm-pi-primary-types.test.sh family=pure-contract-unit expected_gate_skip=none
../../../../../../tmp/fm-pi-primary-types.gnWahp/fm-calm.ts(248,7): error TS2322: Type '(result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext<TState, StaticType<[], "Encode", {}, {}, TParams>>) => Container | Component' is not assignable to type '(result: AgentToolResult<unknown>, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext<TState, StaticType<[], "Encode", {}, {}, TParams>>) => Component'.
  Types of parameters 'result' and 'result' are incompatible.
    Type 'AgentToolResult<unknown>' is not assignable to type 'AgentToolResult<TDetails>'.
      Type 'unknown' is not assignable to type 'TDetails'.
        'TDetails' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.
../../../../../../tmp/fm-pi-primary-types.gnWahp/fm-calm.ts(290,57): error TS2345: Argument of type '(data: string) => void' is not assignable to parameter of type 'TerminalInputHandler'.
  Type 'void' is not assignable to type '{ consume?: boolean | undefined; data?: string | undefined; } | undefined'.
FM_TEST_END 2026-08-03T10:55:12Z tests/fm-pi-primary-types.test.sh exit=1 duration_ms=11639 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:55:12Z tests/fm-pi-compatible-family.test.sh family=pure-contract-unit expected_gate_skip=none
ok - Pi-compatible family membership is the exact pi/omp allowlist
FM_TEST_END 2026-08-03T10:55:12Z tests/fm-pi-compatible-family.test.sh exit=0 duration_ms=124 gate_skip=false
FM_TEST_SUMMARY total=6 failed=2 skipped_gate=0 duration_ms=110429
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=4 duration_ms=87777 failed=1
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=2 duration_ms=22371 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-calm-pi-extension.test.sh duration_ms=75614
FM_TEST_SLOWEST rank=2 script=tests/fm-pi-watch-extension.test.sh duration_ms=18072
FM_TEST_SLOWEST rank=3 script=tests/fm-pi-primary-types.test.sh duration_ms=11639
FM_TEST_SLOWEST rank=4 script=tests/fm-omp-primary.test.sh duration_ms=4299
FM_TEST_SLOWEST rank=5 script=tests/fm-omp-harness.test.sh duration_ms=400
FM_TEST_SLOWEST rank=6 script=tests/fm-pi-compatible-family.test.sh duration_ms=124
Evidence: Upstream main (33a4287) baseline showing the two failures pre-date this branch

not ok - OpenCode watch plugin must arm only when this session owns the fleet lock: expected exit 0, got 1 fm-calm.ts(248,7): error TS2322 ... fm-calm.ts(290,57): error TS2345 FM_TEST_SUMMARY total=2 failed=2

FM_TEST_BEGIN 2026-08-03T10:56:33Z tests/fm-pi-watch-extension.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - Pi extension reports external healthy watcher output
ok - Pi custom tool exposes repair-only metadata and returns automatic-continuation guidance
ok - Pi redundant tool call returns ownership guidance and spawns no second child
ok - Pi scheduled retry remains extension-owned after another tool call
ok - Pi actionable close starts one successor before wake delivery settles
ok - Pi hung successor falls back to one typed actionable wake
ok - Pi unretired successor falls back without an overlapping retry
ok - Pi late unretired closes resume classified supervision
ok - Pi clean empty close triggers a bounded continuity retry
ok - Pi established clean closes stop at the configured retry limit
ok - Pi close handler verifies session-lock ownership before successor launch
ok - Pi watcher arm distinguishes all session lock ownership states
ok - Pi session transitions use a generation owner across /new /resume /fork, stale callbacks, and quit
ok - Pi process-exit cleanup listener remains singular across session replacement
ok - Pi process-exit cleanup stops the attached arm child
ok - OpenCode plugins have an explicit ESM boundary even under a typeless parent package
ok - OpenCode watcher plugin uses the effective FM_HOME state
ok - OpenCode watcher plugin sources the effective config
not ok - OpenCode watch plugin must arm only when this session owns the fleet lock: expected exit 0, got 1
FM_TEST_END 2026-08-03T10:56:51Z tests/fm-pi-watch-extension.test.sh exit=1 duration_ms=17910 gate_skip=false
FM_TEST_BEGIN 2026-08-03T10:56:51Z tests/fm-pi-primary-types.test.sh family=pure-contract-unit expected_gate_skip=none
../fm-pi-primary-types.u2q5uu/fm-calm.ts(248,7): error TS2322: Type '(result: AgentToolResult<TDetails>, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext<TState, StaticType<[], "Encode", {}, {}, TParams>>) => Container | Component' is not assignable to type '(result: AgentToolResult<unknown>, options: ToolRenderResultOptions, theme: Theme, context: ToolRenderContext<TState, StaticType<[], "Encode", {}, {}, TParams>>) => Component'.
  Types of parameters 'result' and 'result' are incompatible.
    Type 'AgentToolResult<unknown>' is not assignable to type 'AgentToolResult<TDetails>'.
      Type 'unknown' is not assignable to type 'TDetails'.
        'TDetails' could be instantiated with an arbitrary type which could be unrelated to 'unknown'.
../fm-pi-primary-types.u2q5uu/fm-calm.ts(290,57): error TS2345: Argument of type '(data: string) => void' is not assignable to parameter of type 'TerminalInputHandler'.
  Type 'void' is not assignable to type '{ consume?: boolean | undefined; data?: string | undefined; } | undefined'.
FM_TEST_END 2026-08-03T10:57:00Z tests/fm-pi-primary-types.test.sh exit=1 duration_ms=9723 gate_skip=false
FM_TEST_SUMMARY total=2 failed=2 skipped_gate=0 duration_ms=27720
FM_TEST_SUMMARY_FAMILY family=pure-contract-unit count=1 duration_ms=9723 failed=1
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=17910 failed=1
FM_TEST_SLOWEST rank=1 script=tests/fm-pi-watch-extension.test.sh duration_ms=17910
FM_TEST_SLOWEST rank=2 script=tests/fm-pi-primary-types.test.sh duration_ms=9723
Evidence: Manual re-bind demo script (reusable)
#!/usr/bin/env bash
# Manual watcher re-bind demo: binds the Pi primary watcher extension twice in one
# process (as a runtime factory re-bind does) and reports what an operator would see.
# Usage: demo.sh <tree-with-core-and-extension> <label>
set -u
tree=$1
label=$2
work=$(mktemp -d /tmp/fm-rebind-demo.XXXXXX)
repo="$work/repo"
home="$work/home"
mkdir -p "$repo" "$home/state" "$home/config"
ROOT="$tree"
EXT="$tree/.pi/extensions/fm-primary-pi-watch.ts"
. /tmp/fm-rebind-demo/fixture.sh
install_pi_watch_extension_fixture "$repo"
cat > "$repo/bin/fm-watch-arm.sh" <<'SH'
#!/usr/bin/env bash
printf 'watcher: started pid=%s\n' "$$"
printf '%s\n' "$$" > "${FM_CHILD_PID_FILE:?}"
printf 'arm pid=%s\n' "$$" >> "${FM_ARM_LOG:?}"
trap 'exit 0' TERM INT
while :; do sleep 0.2; done
SH
chmod +x "$repo/bin/fm-watch-arm.sh"
printf '%s\n' "$$" > "$home/state/.lock"

echo "=== $label ==="
NODE_NO_WARNINGS=1 PLUGIN="$repo/.pi/extensions/fm-primary-pi-watch.ts" FM_HOME="$home" FM_ROOT_OVERRIDE="$repo" \
  FM_CHILD_PID_FILE="$work/child.pid" FM_ARM_LOG="$work/arm.log" \
  node --input-type=module <<'EOF'
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { pathToFileURL } from "node:url";
function makePi() {
  const handlers = new Map();
  let tool = null;
  return { pi: { on: (e, h) => handlers.set(e, h), registerCommand() {}, registerTool(c) { if (c.name === "fm_watch_arm_pi") tool = c; }, sendUserMessage: async () => {}, events: { on() {} } }, handlers, getTool: () => tool };
}
const alive = (pid) => { try { process.kill(Number(pid), 0); return true; } catch { return false; } };
const liveArms = () => existsSync(process.env.FM_ARM_LOG)
  ? readFileSync(process.env.FM_ARM_LOG, "utf8").trim().split(/\n/).filter(Boolean)
      .map((l) => (/pid=(\d+)/.exec(l) || [])[1]).filter(Boolean).filter(alive)
  : [];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
writeFileSync(`${process.env.FM_HOME}/state/.lock`, `${process.pid}\n`);
const base = process.listenerCount("exit");
const mod = await import(pathToFileURL(process.env.PLUGIN).href);

const first = makePi();
mod.default(first.pi);
await first.handlers.get("session_start")?.({ type: "session_start", reason: "startup" }, {});
const a = await first.getTool().execute("bind-1", {}, undefined, undefined, {});
console.log(`bind #1 arm  -> ok=${a.details?.ok} msg="${String(a.details?.message).split("\n")[0]}"`);
for (let i = 0; i < 100 && !existsSync(process.env.FM_CHILD_PID_FILE); i += 1) await wait(20);
console.log(`after bind #1: live arm children=[${liveArms().join(",")}] exit listeners=${process.listenerCount("exit") - base}`);
const staleTool = first.getTool();

console.log("--- runtime re-binds the watcher factory (no session_shutdown) ---");
const second = makePi();
mod.default(second.pi);
await wait(400);
console.log(`after bind #2: live arm children=[${liveArms().join(",")}] exit listeners=${process.listenerCount("exit") - base}`);
const s = await staleTool.execute("stale", {}, undefined, undefined, {});
console.log(`superseded bind arm -> ok=${s.details?.ok} msg="${String(s.details?.message).split("\n")[0]}"`);
const b = await second.getTool().execute("bind-2", {}, undefined, undefined, {});
console.log(`bind #2 arm  -> ok=${b.details?.ok} msg="${String(b.details?.message).split("\n")[0]}"`);
await wait(400);
console.log(`after bind #2 arms: live arm children=[${liveArms().join(",")}] exit listeners=${process.listenerCount("exit") - base}`);
liveArms().forEach((pid) => { try { process.kill(Number(pid), "SIGTERM"); } catch {} });
process.exit(0);
EOF
rm -rf "$work"
- Outcome: ⚠️ 2 warnings across 1 run (10m24s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

⚠️ **Rebase** - 1 warning
  • ⚠️ .agents/skills/afk/SKILL.md - branch carries 65 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (113 file(s)) into the PR:
  • b6fe376 Merge pull request feat(bin): guard watcher liveness across supervision scripts #8 from dnth/fm/omp-harness-support-e1
  • c74d7c7 chore(merge): reconcile upstream supervision with OMP
  • 29fe743 no-mistakes(document): document OMP toolbelt scripts and AFK harness binding
  • 7a7c353 fix(ci): align snapshot and Herdr fixtures
  • b0ea297 docs(omp): bind lifecycle evidence to corrected head
  • 491bc80 no-mistakes(document): document OMP marker version, herdr ask proof, tmux gate
  • c5b2efb no-mistakes(review): bind watcher marker version to core, re-resolve OMP bun, gate tmux OMP probe
  • 1c38316 docs(omp): refresh final lifecycle evidence
  • 6b5d6bc no-mistakes(lint): omit explicit deliverAs for OMP watcher notifications
  • 0dac9ab no-mistakes(lint): add deterministic OMP offset TOCTOU regression test
  • 98e4550 no-mistakes(lint): bind OMP session offset newline check to recorded size
  • a2f23e6 no-mistakes(lint): order watcher signal signature as inode:size:mtime
  • 84be3a4 no-mistakes(lint): add same-second turn-end marker regression test
  • d136e7a no-mistakes(lint): distinguish same-second turn-end markers in watcher signal scan
  • 1819fee no-mistakes(lint): require exact structured ask result for blocked OMP answers
  • 165d256 no-mistakes(lint): confirm OMP blocked-ask answers via structured selectedOptions
  • 308970d no-mistakes(lint): replace fixture wake drain with primary-owned settle wait
  • eef42c6 no-mistakes(lint): drop duplicate secondmate status wake requirement
  • 840949a no-mistakes(lint): reuse production JSONL offset owner and tolerate ask-close race
  • 822e344 no-mistakes(lint): harden Herdr matrix JSONL offsets and event predicates
  • d2ad0ae no-mistakes(lint): accept idle-captain path for blocked worker answer
  • f0141f5 no-mistakes(lint): accept structured ask selectedOptions for routed worker choice
  • 6cc0841 no-mistakes(lint): bind primary wake drain and route blocked answer via captain
  • 1b53b61 no-mistakes(lint): quarantine native OMP agent read probe form
  • cb2037d no-mistakes(lint): bind OMP submit snapshot offset to complete JSONL boundary
  • ca7fca1 no-mistakes(lint): bind and drain secondmate recovery wake before resumed exit
  • e719b4a no-mistakes(document): point Pi generation-owner docs at shared watcher core
  • 64e2062 no-mistakes(document): correct stale OMP head-bound evidence and arm readiness contract
  • 15a09a8 no-mistakes(review): make herdr spawn fixture pane presence stateful
  • 5df7a43 no-mistakes(review): confirm OMP abort endpoint stop before destructive cleanup
  • 6633e9d docs(omp): refresh final lifecycle evidence
  • ce9ad11 fix(omp): await watcher readiness before arm success
  • 2b2e991 test(herdr): require fresh watcher before role matrix
  • 77c976a Merge remote-tracking branch 'origin/main' into fm/omp-herdr-matrix-fix-k3
  • 7720928 fix(tmux): recognize OMP waiting busy footer
  • d6133a4 test(composer): cover Unicode prompts under C locale
  • a73484c fix(composer): strip prompt glyphs independent of locale
  • 9b5e331 test(herdr): model session inventory and native reads
  • fe672a1 test(herdr): advance session-list fixture responses
  • 5cb73ea test: align Herdr fixtures with merged cleanup calls
  • e38f161 Merge origin/main into OMP Herdr matrix branch
  • 396e395 no-mistakes(document): record green corrected-head combined OMP runner evidence
  • 0970f85 no-mistakes(document): drop one-off draft-PR CI chronology from verification doc
  • ba12290 no-mistakes(document): correct stale OMP Herdr verification evidence provenance and pointers
  • f73ce10 no-mistakes(review): record green corrected OMP Herdr matrix rerun evidence
  • a8d124f no-mistakes(review): log every fixture Herdr refusal through shared refuse path; matrix rerun still in flight, doc transcript not yet replaced
  • f911a60 no-mistakes(review): admit bare Herdr client reads; assert refusal log
  • 246b0f4 test: correct OMP Herdr live matrix fixture
  • 0733c56 no-mistakes(document): document OMP scope, marker ancestry, and tmux submit exception
  • be1a26f no-mistakes(review): bind OMP harness marker to ancestry; scope tmux unknown branch; dedupe identity
  • 3b9b31d fix: correct OMP branch regressions
  • 9a6f58e no-mistakes(document): document OMP session-start, watcher continuity, and state artifacts
  • 88d3a72 no-mistakes(review): untrack .omc runtime artifacts and ignore them
  • 154be48 no-mistakes(review): bound exact-OMP ancestry probe to innermost harness
  • f44c239 test: stabilize guarded OMP Herdr evidence (feat: add scout task lifecycle #7)
  • f526a46 fix: close OMP harness verification gaps (feat: add scout task lifecycle #7)
  • 99010ba docs: close OMP branch standards review (feat: add scout task lifecycle #7)
  • e3584df test: harden OMP branch regression fixtures (feat: add scout task lifecycle #7)
  • c0cfd41 fix: address OMP branch review findings (feat: add scout task lifecycle #7)
  • 83160dc test: verify OMP support contract (feat: add scout task lifecycle #7)
  • 351ba31 feat: verify OMP lifecycle on Herdr (fix: reduce crewmate status wake noise #6)
  • c3921a3 feat: add OMP secondmate tmux support (ci: enforce contributor guardrails #5)
  • 8057eee feat: add OMP primary tmux support (fix(bin): coalesce watcher signals into one wake #4)
  • 63c86fe feat: add OMP worker harness support (docs: align tmux and harness guidance #3)
  • 899830b refactor: extract Pi-compatible watcher core (docs: polish README banner and repo housekeeping #2)

Push main to origin, or rebase your branch onto origin/main, before gating.

🔧 Fix applied.
1 warning still open:

  • ⚠️ .agents/skills/afk/SKILL.md - branch carries 65 commit(s) that exist on your local main branch but were never pushed to origin/main; rebasing would bundle this unrelated work (113 file(s)) into the PR:
  • b6fe376 Merge pull request feat(bin): guard watcher liveness across supervision scripts #8 from dnth/fm/omp-harness-support-e1
  • c74d7c7 chore(merge): reconcile upstream supervision with OMP
  • 29fe743 no-mistakes(document): document OMP toolbelt scripts and AFK harness binding
  • 7a7c353 fix(ci): align snapshot and Herdr fixtures
  • b0ea297 docs(omp): bind lifecycle evidence to corrected head
  • 491bc80 no-mistakes(document): document OMP marker version, herdr ask proof, tmux gate
  • c5b2efb no-mistakes(review): bind watcher marker version to core, re-resolve OMP bun, gate tmux OMP probe
  • 1c38316 docs(omp): refresh final lifecycle evidence
  • 6b5d6bc no-mistakes(lint): omit explicit deliverAs for OMP watcher notifications
  • 0dac9ab no-mistakes(lint): add deterministic OMP offset TOCTOU regression test
  • 98e4550 no-mistakes(lint): bind OMP session offset newline check to recorded size
  • a2f23e6 no-mistakes(lint): order watcher signal signature as inode:size:mtime
  • 84be3a4 no-mistakes(lint): add same-second turn-end marker regression test
  • d136e7a no-mistakes(lint): distinguish same-second turn-end markers in watcher signal scan
  • 1819fee no-mistakes(lint): require exact structured ask result for blocked OMP answers
  • 165d256 no-mistakes(lint): confirm OMP blocked-ask answers via structured selectedOptions
  • 308970d no-mistakes(lint): replace fixture wake drain with primary-owned settle wait
  • eef42c6 no-mistakes(lint): drop duplicate secondmate status wake requirement
  • 840949a no-mistakes(lint): reuse production JSONL offset owner and tolerate ask-close race
  • 822e344 no-mistakes(lint): harden Herdr matrix JSONL offsets and event predicates
  • d2ad0ae no-mistakes(lint): accept idle-captain path for blocked worker answer
  • f0141f5 no-mistakes(lint): accept structured ask selectedOptions for routed worker choice
  • 6cc0841 no-mistakes(lint): bind primary wake drain and route blocked answer via captain
  • 1b53b61 no-mistakes(lint): quarantine native OMP agent read probe form
  • cb2037d no-mistakes(lint): bind OMP submit snapshot offset to complete JSONL boundary
  • ca7fca1 no-mistakes(lint): bind and drain secondmate recovery wake before resumed exit
  • e719b4a no-mistakes(document): point Pi generation-owner docs at shared watcher core
  • 64e2062 no-mistakes(document): correct stale OMP head-bound evidence and arm readiness contract
  • 15a09a8 no-mistakes(review): make herdr spawn fixture pane presence stateful
  • 5df7a43 no-mistakes(review): confirm OMP abort endpoint stop before destructive cleanup
  • 6633e9d docs(omp): refresh final lifecycle evidence
  • ce9ad11 fix(omp): await watcher readiness before arm success
  • 2b2e991 test(herdr): require fresh watcher before role matrix
  • 77c976a Merge remote-tracking branch 'origin/main' into fm/omp-herdr-matrix-fix-k3
  • 7720928 fix(tmux): recognize OMP waiting busy footer
  • d6133a4 test(composer): cover Unicode prompts under C locale
  • a73484c fix(composer): strip prompt glyphs independent of locale
  • 9b5e331 test(herdr): model session inventory and native reads
  • fe672a1 test(herdr): advance session-list fixture responses
  • 5cb73ea test: align Herdr fixtures with merged cleanup calls
  • e38f161 Merge origin/main into OMP Herdr matrix branch
  • 396e395 no-mistakes(document): record green corrected-head combined OMP runner evidence
  • 0970f85 no-mistakes(document): drop one-off draft-PR CI chronology from verification doc
  • ba12290 no-mistakes(document): correct stale OMP Herdr verification evidence provenance and pointers
  • f73ce10 no-mistakes(review): record green corrected OMP Herdr matrix rerun evidence
  • a8d124f no-mistakes(review): log every fixture Herdr refusal through shared refuse path; matrix rerun still in flight, doc transcript not yet replaced
  • f911a60 no-mistakes(review): admit bare Herdr client reads; assert refusal log
  • 246b0f4 test: correct OMP Herdr live matrix fixture
  • 0733c56 no-mistakes(document): document OMP scope, marker ancestry, and tmux submit exception
  • be1a26f no-mistakes(review): bind OMP harness marker to ancestry; scope tmux unknown branch; dedupe identity
  • 3b9b31d fix: correct OMP branch regressions
  • 9a6f58e no-mistakes(document): document OMP session-start, watcher continuity, and state artifacts
  • 88d3a72 no-mistakes(review): untrack .omc runtime artifacts and ignore them
  • 154be48 no-mistakes(review): bound exact-OMP ancestry probe to innermost harness
  • f44c239 test: stabilize guarded OMP Herdr evidence (feat: add scout task lifecycle #7)
  • f526a46 fix: close OMP harness verification gaps (feat: add scout task lifecycle #7)
  • 99010ba docs: close OMP branch standards review (feat: add scout task lifecycle #7)
  • e3584df test: harden OMP branch regression fixtures (feat: add scout task lifecycle #7)
  • c0cfd41 fix: address OMP branch review findings (feat: add scout task lifecycle #7)
  • 83160dc test: verify OMP support contract (feat: add scout task lifecycle #7)
  • 351ba31 feat: verify OMP lifecycle on Herdr (fix: reduce crewmate status wake noise #6)
  • c3921a3 feat: add OMP secondmate tmux support (ci: enforce contributor guardrails #5)
  • 8057eee feat: add OMP primary tmux support (fix(bin): coalesce watcher signals into one wake #4)
  • 63c86fe feat: add OMP worker harness support (docs: align tmux and harness guidance #3)
  • 899830b refactor: extract Pi-compatible watcher core (docs: polish README banner and repo housekeeping #2)

Push main to origin, or rebase your branch onto origin/main, before gating.

🔧 **Review** - 2 issues found → auto-fixed ✅
  • ⚠️ bin/fm-primary-watch-core.ts:564 - sessionStart() is the only retained core callback with no supersession guard. After a factory re-bind, the old core's generation.stopping is true, so sessionStart() mints a fresh generation and activateGeneration (line 143) SIGTERMs the currently live binding's arm child and installs the dead binding as activeGeneration. Concrete path: OMP binds core C1; host re-binds -> C2 active, C1 retired; the OMP adapter's retained session_switch handler on C1 (.omp/extensions/fm-primary-omp.ts:210-216) fires -> C1.sessionShutdown(); C1.sessionStart(); C1.arm() -> C2's arm child is killed and C2's fm_watch_arm_omp tool now answers watcher: not armed - OMP session is shutting down while the retired binding owns the watcher. This contradicts the invariant this commit added at lines 10-12 ("Stale callbacks from an earlier generation, including callbacks retained by a superseded core instance, are no-ops against the active replacement") and the intent's "a factory re-binding must retire or supersede the old core so it cannot remain live". Reachability requires the host to keep delivering session events to the superseded binding; the new test gives each bind a fresh API object (makePi() at tests/fm-pi-watch-extension.test.sh:1140/1153/1183), so this path is never exercised. Earliest shared boundary that closes it: record a module-level active-binding token in createPrimaryWatchCore alongside activeGeneration, and make sessionStart (and sessionShutdown) no-ops when the caller's binding token is not the active one - then extend the new test to call the superseded binding's session_start handler and assert the live child and live-binding tool are untouched.
  • ℹ️ bin/fm-primary-watch-core.ts:126 - The single-active-generation and single-exit-listener guarantees are anchored in module scope, so they hold only while the module cache is retained across re-binds. A re-bind that re-imports the extension through a cache-busting specifier would give each module instance its own activeGeneration/exitFallbackInstalled and reintroduce both the duplicate arm child and the accumulating exit listener. docs/verification/supervision.md:281 states the modeled re-bind is "module cache retained", so this is a documented boundary rather than a defect - noting it because the header comment claims the ownership is "process-wide", which is only true under that assumption. A globalThis symbol-keyed registry would make the claim literal if the assumption ever changes.

🔧 Fix: guard superseded watcher bindings from reclaiming ownership
✅ Re-checked - no issues remain.

⚠️ **Test** - 2 warnings
  • ⚠️ tests/fm-pi-watch-extension.test.sh:1501 - tests/fm-pi-watch-extension.test.sh: "OpenCode watch plugin must arm only when this session owns the fleet lock" fails locally (arm never runs after the lock is re-acquired). Not caused by this change: .opencode/plugins/fm-primary-watch-arm.js and this test body are byte-identical to upstream main, and the same failure reproduces from a clean git archive 33a4287 checkout. Pre-existing/environmental; flagged because it will also fail the branch's CI lane.
  • ⚠️ tests/fm-pi-primary-types.test.sh - tests/fm-pi-primary-types.test.sh fails with two TS errors in fm-calm.ts (TS2322 at 248:7, TS2345 at 290:57) against the locally installed Pi type definitions. Reproduces identically on upstream main (33a4287); unrelated to the watcher-core rebind fix.
  • bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh tests/fm-omp-primary.test.sh tests/fm-omp-harness.test.sh tests/fm-calm-pi-extension.test.sh tests/fm-pi-primary-types.test.sh tests/fm-pi-compatible-family.test.sh
  • bin/fm-test-run.sh tests/fm-omp-secondmate.test.sh tests/fm-watcher-lock.test.sh (all pass)
  • Red-state proof: extracted the target tree to /tmp, replaced only bin/fm-primary-watch-core.ts with the pre-fix b6fe376 version (sha256 verified), then ran bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh — new test Pi factory re-bind without shutdown retires the prior generation and exit fallback stays singular fails there and passes at 9b4e407
  • Pre-fix scenario probe with the exit-listener assertions relaxed: showed exit listeners after 2nd bind = 2 (expected 1) and timeout waiting for superseded bind child exit
  • Manual end-user demo /tmp/no-mistakes-evidence/01KZ3J4X5PMS0WXFS815SEDQRE/demo.sh &lt;tree&gt; &lt;label&gt; run against both the pre-fix and fixed trees, printing live arm-child PIDs, process-exit listener count, and the arm-tool response of the superseded binding
  • Baseline check: git archive 33a4287 | tar -x then bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh tests/fm-pi-primary-types.test.sh to confirm the two failures pre-date this branch
  • Standalone repro of the OpenCode session-lock scenario against the unchanged .opencode/plugins/fm-primary-watch-arm.js
  • git status --short --branch — worktree clean, no stray test artifacts
✅ **Document** - passed

✅ No issues found.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 127)
✅ **Push** - passed

✅ No issues found.

dnth added 30 commits July 31, 2026 14:55
…efuse path; matrix rerun still in flight, doc transcript not yet replaced
dnth added 28 commits August 1, 2026 21:22
Preserve launch-bound OMP identity inside the generalized session-lock matcher, combine test-lane and supervision ownership, and carry current upstream main without rewriting history.
Enable OMP as the native Firstmate harness
@dnth

dnth commented Aug 3, 2026

Copy link
Copy Markdown
Author

Closing because this fork-specific watcher correction was opened against upstream by the local delivery configuration. The validated branch belongs in dnth/firstmate.

@dnth dnth closed this Aug 3, 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.

1 participant