Skip to content

fix(pi): stop nested Pi CLI from replacing a live session binding - #60

Merged
tiago-peixoto merged 5 commits into
mainfrom
fm/firstmate-pi-marker-binding-repair
Sep 21, 2026
Merged

tiago-peixoto merged 5 commits into
mainfrom
fm/firstmate-pi-marker-binding-repair

Conversation

@tiago-peixoto

@tiago-peixoto tiago-peixoto commented Sep 21, 2026 •

Copy link
Copy Markdown
Owner

Intent

Authorize repair of the verified false Pi supervision alarms caused by short-lived nested Pi commands replacing a live session binding, with regression coverage.

What Changed

  • fm-primary-pi-watch.ts and fm-primary-turnend-guard.ts now write their loaded markers only when the state lock names this exact process, or when the lock is missing, invalid-free, or held by a dead pid. A short-lived Pi CLI child of a live session (such as fm-spawn's pi --help probe) no longer overwrites the markers with its own pid, which previously died and caused false PI_WATCH_EXTENSION: not loaded and supervision-off alarms. Watcher arming in the watch extension still treats ancestry as ownership; the turn-end guard's now-unused ancestry walk (lockOwnership/parentPid) is removed.
  • Adds regression coverage: tests/fm-pi-watch-extension.test.sh loads both extensions as a stubbed Node child and checks that a live ancestor binding is kept, that a free or dead lock is still claimed, and that a live lock naming the loading process still binds both markers. The new token-free live guard tests/fm-pi-nested-probe-marker-live-e2e.test.sh (registered in bin/fm-test-run.sh) runs the real pi --help and a refused Pi-harness spawn against a live binding, and fails naming the installed Pi version.
  • tests/fm-pi-primary-live-e2e.test.sh now writes the lock from a subshell that execs pi, so the lock names the Pi process itself under the stricter rule, and docs/verification/runtime-backends.md records the fix and its verification against Pi 0.86.1.

Risk Assessment

✅ Low: The change narrows one marker-writing condition, duplicated in the two Pi extensions, to match what session start already requires (marker pid equals lock pid, and fm-lock.sh records the Pi process itself), and it adds regression tests that fail against the old ancestry rule.

Testing

I ran the committed live guard against the real Pi 0.86.1 CLI; it passes on this branch and fails on the base code at the reported bug. The Pi watch-extension regression suite also passes on this branch, and its new nested-child test fails on the base code. I then drove a real Pi terminal session in a private tmux lab for both base and fix. Every step ran from inside the session with Pi's !! shell command, so no model was involved and no tokens were spent. On base, the false alarm reproduced; on the fix, it did not across nested pi --help, a refused Pi-harness spawn, and /reload. Evidence is CLI and terminal transcripts (this change has no visual UI surface). The lab was torn down and the worktree is clean.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Nested pi --help run inside a live Pi primary session leaves the session's markers in place, and the next session start prints no PI_WATCH_EXTENSION alarm ✅ pass live Fix lab: markers stayed at live Pi pid 91873 after !!pi --help, and !!bin/fm-session-start.sh printed no PI_WATCH_EXTENSION alarm (tui-fix-pi-session.txt, live-pi-tui-before-after.md)
Regression reproduced on base: the same nested pi --help replaces both markers with its dead pid and session start prints the false alarm ✅ pass live Base lab: markers became dead pid 15305 while Pi 81527 was live; session start printed PI_WATCH_EXTENSION: not loaded; fm_pi_extension_owns_supervision returned false (tui-base-pi-session.txt, base-…
A Pi-harness fm-spawn refused for a missing project (its pi --help probe still runs) leaves the live binding in place and session start stays quiet ✅ pass live Fix lab: spawn refused at fm-spawn.sh:2779 after the probe, no tmux window created, markers still 91873, no alarm. The same step on base replaced the markers with probe pid 19394 (tui-base-spawn-probe…
A real session whose lock names the Pi process itself still writes its markers after /reload, and the watcher still arms ✅ pass live Fix lab: after /reload with lock=91873, both marker mtimes advanced and they still name 91873; the stub arm log shows arm calls from ppid 91873; session start afterward printed no alarm
A fresh Pi start on a free lock still binds both markers, then session start takes the lock without an alarm ✅ pass live Fix lab: markers named 91873 immediately on start with no lock; !!bin/fm-session-start.sh printed lock acquired: harness pid 91873 and primary harness: pi, with no alarm. The live guard's positi…
Committed regression coverage catches the bug: the live guard and the new unit tests pass on the fix and fail on the base code ✅ pass live live-guard-fix.txt (3/3 ok), live-guard-base.txt (exit=1, pi --help replaced a live watch binding), unit-suite-fix.txt (exit 0), unit-suite-base.txt (`watch marker pid was replaced by a nested Pi pr…
Evidence: Before/after table of the live Pi TUI lab (markers, lock, session-start alarm per step)

Source: Before/after table of the live Pi TUI lab (markers, lock, session-start alarm per step)

# Live Pi primary: nested Pi CLI vs. session binding (before/after)

Real Pi 0.86.1 interactive TUI, started with plain `pi` in a private tmux server (`tmux -L fm-lab-pimarker-<variant> -f /dev/null`).
Each variant used a throwaway FM_HOME and Pi HOME (`defaultProjectTrust: "always"`) and a `git archive` copy of the tree.
No model was logged in, so no tokens were spent.
Every command ran from inside the live Pi session with Pi's `!!command` (shell only, not sent to a model), the same way the primary runs repo scripts.
`bin/fm-watch-arm.sh` was stubbed in the lab copies because watcher arming is not under test.

## Base 09dc7b39 (before the fix)

| Step | watch marker pid | turn-end marker pid | lock pid | session start |
|---|---|---|---|---|
| Pi 81527 starts (lock free) | 81527 | 81527 | - | - |
| `!!bin/fm-session-start.sh` | 81527 | 81527 | 81527 | no alarm |
| `!!pi --help` | 15305 (dead) | 15305 (dead) | 81527 | - |
| `!!bin/fm-session-start.sh` | 15305 | 15305 | 81527 | **`PI_WATCH_EXTENSION: not loaded`** (false: Pi 81527 is live with both extensions loaded) |

`fm_pi_extension_owns_supervision` then returned false, so the watcher hand-off loses its extension-owned tolerance.
A restarted base session (Pi 74076) showed the same replacement from the refused `bin/fm-spawn.sh probe-x /nonexistent-project --scout --harness pi --backend tmux` probe: both markers became 19394.

## Fix ba3125f4 (this branch)

| Step | watch marker pid | turn-end marker pid | lock pid | session start |
|---|---|---|---|---|
| Pi 91873 starts (lock free) | 91873 | 91873 | - | - |
| `!!bin/fm-session-start.sh` | 91873 | 91873 | 91873 | no alarm |
| `!!pi --help` | 91873 | 91873 | 91873 | - |
| `!!bin/fm-session-start.sh` | 91873 | 91873 | 91873 | no alarm |
| `!!bin/fm-spawn.sh ... --harness pi` (refused at line 2779, after the probe) | 91873 | 91873 | 91873 | - |
| `!!bin/fm-session-start.sh` | 91873 | 91873 | 91873 | no alarm |
| `/reload` (lock names this Pi process) | 91873 (rewritten) | 91873 (rewritten) | 91873 | - |
| `!!bin/fm-session-start.sh` | 91873 | 91873 | 91873 | no alarm |

After `/reload`, both marker mtimes advanced, so the self-lock branch still writes the marker, and the stub arm log shows the owning session (ppid 91873) still armed the watcher.
`fm_pi_extension_owns_supervision` returned true throughout.
Full TUI transcripts: `tui-fix-pi-session.txt`, `tui-base-pi-session.txt`, `tui-base-spawn-probe.txt`.
Evidence: Fix: full Pi TUI transcript (session start, nested pi --help, refused fm-spawn probe, /reload, no alarm)

Source: Fix: full Pi TUI transcript (session start, nested pi --help, refused fm-spawn probe, /reload, no alarm)

 pi v0.86.1
 escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more
 Press ctrl+o to show full startup help and loaded resources.
 Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.
[Context]
  AGENTS.md
[Skills]
  afk, ahoy, ask-user-authority, bearings, bootstrap-diagnostics, captain-hold-lifecycle, decision-hold-lifecycle, diagnostic-reasoning,
firstmate-codexapp, firstmate-coding-guidelines, firstmate-orca, fmx-respond, harness-adapters, process-event-sources, project-management, quiet,
quota-array-dispatch, secondmate-provisioning, stow, stuck-crewmate-recovery, updatefirstmate
[Extensions]
  fm-branch-supervision.ts, fm-calm.ts, fm-primary-pi-watch.ts, fm-primary-turnend-guard.ts
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ echo PI_CODING_AGENT=$PI_CODING_AGENT; bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/fix-ss1.txt 2>&1; echo session-start-exit=$?; grep -c
 PI_WATCH_EXTENSION /tmp/fm-lab-pimarker.rSSBqs/fix-ss1.txt; bin/fm-lock.sh status
 PI_CODING_AGENT=true
 session-start-exit=0
 0
 lock: held by live harness pid 91873
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ pi --help >/dev/null; echo nested-pi-help-exit=$?
 nested-pi-help-exit=0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/fix-ss2.txt 2>&1; echo session-start-exit=$?; grep PI_WATCH_EXTENSION
 /tmp/fm-lab-pimarker.rSSBqs/fix-ss2.txt || echo 'no PI_WATCH_EXTENSION alarm'
 session-start-exit=0
 no PI_WATCH_EXTENSION alarm
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-spawn.sh probe-x /nonexistent-project --scout --harness pi --backend tmux 2>&1 | tail -2; echo fm-spawn-exit=${PIPESTATUS[0]}
 bin/fm-spawn.sh: line 2779: cd: /nonexistent-project: No such file or directory
 fm-spawn-exit=1
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/fix-ss3.txt 2>&1; echo session-start-exit=$?; grep PI_WATCH_EXTENSION
 /tmp/fm-lab-pimarker.rSSBqs/fix-ss3.txt || echo 'no PI_WATCH_EXTENSION alarm'
 session-start-exit=0
 no PI_WATCH_EXTENSION alarm
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Reloaded keybindings, extensions, skills, prompts, themes, and context files
 Extension "<runtime>" error: No API key found for the selected model.
 Use /login to log into a provider via OAuth or API key. See:
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/fix-ss4.txt 2>&1; echo session-start-exit=$?; grep PI_WATCH_EXTENSION
 /tmp/fm-lab-pimarker.rSSBqs/fix-ss4.txt || echo 'no PI_WATCH_EXTENSION alarm'
 session-start-exit=0
 no PI_WATCH_EXTENSION alarm
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
/private/tmp/fm-lab-pimarker.rSSBqs/fix-tree
0.0%/0 (auto)                                                                                                                                  unknown
Evidence: Base: full Pi TUI transcript showing the false PI_WATCH_EXTENSION alarm after nested pi --help

Source: Base: full Pi TUI transcript showing the false PI_WATCH_EXTENSION alarm after nested pi --help

 pi v0.86.1
 escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more
 Press ctrl+o to show full startup help and loaded resources.
 Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.
[Context]
  AGENTS.md
[Skills]
  afk, ahoy, ask-user-authority, bearings, bootstrap-diagnostics, captain-hold-lifecycle, decision-hold-lifecycle, diagnostic-reasoning,
firstmate-codexapp, firstmate-coding-guidelines, firstmate-orca, fmx-respond, harness-adapters, process-event-sources, project-management, quiet,
quota-array-dispatch, secondmate-provisioning, stow, stuck-crewmate-recovery, updatefirstmate
[Extensions]
  fm-branch-supervision.ts, fm-calm.ts, fm-primary-pi-watch.ts, fm-primary-turnend-guard.ts
 ripgrep not found. Downloading...
 ripgrep installed to /tmp/fm-lab-pimarker.rSSBqs/base-pihome/.pi/agent/bin/rg
 Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md
 Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Update Available
 New version 0.87.0 is available. Run pi update
 Changelog: https://pi.dev/changelog
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/base-ss1.txt 2>&1; echo session-start-exit=$?; grep PI_WATCH_EXTENSION
 /tmp/fm-lab-pimarker.rSSBqs/base-ss1.txt || echo 'no PI_WATCH_EXTENSION alarm'; bin/fm-lock.sh status
 session-start-exit=0
 no PI_WATCH_EXTENSION alarm
 lock: held by live harness pid 81527
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ pi --help >/dev/null; echo nested-pi-help-exit=$?
 nested-pi-help-exit=0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/base-ss2.txt 2>&1; echo session-start-exit=$?; grep PI_WATCH_EXTENSION
 /tmp/fm-lab-pimarker.rSSBqs/base-ss2.txt || echo 'no PI_WATCH_EXTENSION alarm'
 session-start-exit=0
 PI_WATCH_EXTENSION: not loaded - approve Pi project trust once per clone, then restart plain pi so
 /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-turnend-guard.ts and
 /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-pi-watch.ts auto-load for turn-end guard and background wake coverage; use -e
 /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-turnend-guard.ts -e
 /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-pi-watch.ts only if project hooks are not trusted
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
/private/tmp/fm-lab-pimarker.rSSBqs/base-tree
0.0%/0 (auto)                                                                                                                                  unknown
Evidence: Base: refused fm-spawn Pi probe transcript (markers replaced by the probe pid)

Source: Base: refused fm-spawn Pi probe transcript (markers replaced by the probe pid)

 pi v0.86.1
 escape interrupt · ctrl+c/ctrl+d clear/exit · / commands · ! bash · ctrl+o more
 Press ctrl+o to show full startup help and loaded resources.
 Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.
[Context]
  AGENTS.md
[Skills]
  afk, ahoy, ask-user-authority, bearings, bootstrap-diagnostics, captain-hold-lifecycle, decision-hold-lifecycle, diagnostic-reasoning,
firstmate-codexapp, firstmate-coding-guidelines, firstmate-orca, fmx-respond, harness-adapters, process-event-sources, project-management, quiet,
quota-array-dispatch, secondmate-provisioning, stow, stuck-crewmate-recovery, updatefirstmate
[Extensions]
  fm-branch-supervision.ts, fm-calm.ts, fm-primary-pi-watch.ts, fm-primary-turnend-guard.ts
 Warning: No models available. Use /login to log into a provider via OAuth or API key. See:
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/providers.md
   ~/.nvm/versions/node/v22.23.1/lib/node_modules/@earendil-works/pi-coding-agent/docs/models.md
 Warning: tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 Update Available
 New version 0.87.0 is available. Run pi update
 Changelog: https://pi.dev/changelog
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-session-start.sh > /tmp/fm-lab-pimarker.rSSBqs/base-ss3.txt 2>&1; echo session-start-exit=$?
 session-start-exit=0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
 $ bin/fm-spawn.sh probe-x /nonexistent-project --scout --harness pi --backend tmux 2>&1 | tail -1
 bin/fm-spawn.sh: line 2779: cd: /nonexistent-project: No such file or directory
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
/private/tmp/fm-lab-pimarker.rSSBqs/base-tree
0.0%/0 (auto)                                                                                                                                  unknown
Evidence: Base: the false session-start alarm line

Source: Base: the false session-start alarm line

PI_WATCH_EXTENSION: not loaded - approve Pi project trust once per clone, then restart plain pi so /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-turnend-guard.ts and /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-pi-watch.ts auto-load for turn-end guard and background wake coverage; use -e /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-turnend-guard.ts -e /tmp/fm-lab-pimarker.rSSBqs/base-tree/.pi/extensions/fm-primary-pi-watch.ts only if project hooks are not trusted
Evidence: Live guard on fix (3/3 ok)

Source: Live guard on fix (3/3 ok)

FM_TEST_BEGIN 2026-09-21T20:31:45Z tests/fm-pi-nested-probe-marker-live-e2e.test.sh family=live-harness-optin expected_gate_skip=live-capability
ok - pi --help still loads project extensions onto a free lock
ok - pi --help leaves a live ancestor binding in place
ok - refused Pi-harness spawn leaves a live ancestor binding in place
FM_TEST_END 2026-09-21T20:31:47Z tests/fm-pi-nested-probe-marker-live-e2e.test.sh exit=0 duration_ms=1604 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=1946
FM_TEST_SUMMARY_FAMILY family=live-harness-optin count=1 duration_ms=1604 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-pi-nested-probe-marker-live-e2e.test.sh duration_ms=1604
Evidence: Live guard on base (fails: pi --help replaced a live watch binding)

Source: Live guard on base (fails: pi --help replaced a live watch binding)

# tests/fm-pi-nested-probe-marker-live-e2e.test.sh run against base 09dc7b39 extensions (Pi 0.86.1)
ok - pi --help still loads project extensions onto a free lock
not ok - pi --help replaced a live watch binding (Pi 0.86.1)
exit=1
Evidence: Regression suite on fix

Source: Regression suite on fix

FM_TEST_BEGIN 2026-09-21T20:32:13Z 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 dispatcher branch offer owns accepted wakes and falls back to main
ok - Pi dispatcher flags a fleet-wide heartbeat offer as branch-eligible
ok - a co-present check row neither vetoes nor rides a heartbeat into main
ok - every main-only check class still reaches main, never the supervision branch
ok - a captain-held signal trigger reaches main with routine rows present
ok - an unread pending-reply escalation keeps later stale aliases on main
ok - a mixed batch of two distinct files - one routine, one needs-decision - routes wholly to main
ok - a co-present needs-decision row neither vetoes nor rides a heartbeat into main
ok - heartbeat restoration failure stays on main
ok - watcher-failure repair stays with main even with a live, accepting branch listener
ok - under the away-posture record every actionable row is offered to the branch while broken-queue wakes and watcher-failure alarms still reach main
ok - Pi refused handling handshake is classified and not swallowed
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 auto-arm through a generation owner across /new /resume /fork/reload, stale callbacks, and quit
ok - Pi session replacement auto-arms and carries its in-flight actionable close
ok - Pi replacement replays a streaming follow-up before consumption
ok - Pi streaming-time wake delivery keeps the successor chain and replays only unconsumed wakes
ok - Pi retries a verified successor that failed during wake delivery once that delivery settles
ok - Pi replacement receives actionable closes after retirement timeout
ok - Pi replacement handoff tokens stay unique across fresh modules
ok - Pi replacement persistence failure still stops its arm child
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
ok - OpenCode watcher plugin requires session lock ownership
ok - OpenCode watcher coordinator respects primary scope
ok - OpenCode watcher plugin starts one successor before wake prompt delivery settles
ok - OpenCode pre-ready actionable close preserves its successor
ok - OpenCode hung successor falls back to one typed actionable wake
ok - OpenCode unretired successor falls back without an overlapping retry
ok - OpenCode late unretired closes resume classified supervision
ok - OpenCode clean empty close triggers a bounded continuity retry
ok - OpenCode established clean closes stop at the configured retry limit
ok - OpenCode close handler verifies session-lock ownership before successor launch
ok - OpenCode watcher plugin coordinates with the turn-end guard
ok - OpenCode healthy arm output does not suppress the turn-end guard
ok - nested Pi load leaves a live session binding in place
ok - free or dead lock still produces a marker for the loading process
ok - live lock naming the loading process still binds both markers
FM_TEST_END 2026-09-21T20:33:22Z tests/fm-pi-watch-extension.test.sh exit=0 duration_ms=69288 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=69594
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=69288 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-pi-watch-extension.test.sh duration_ms=69288
exit=0
Evidence: Base vs fix session-start alarm
base (after !!pi --help inside live Pi 81527): PI_WATCH_EXTENSION: not loaded - approve Pi project trust once per clone, then restart plain pi ...
base: fm_pi_extension_owns_supervision = FALSE although pi 81527 is live with both extensions loaded
fix (after !!pi --help, refused fm-spawn probe, /reload inside live Pi 91873): no PI_WATCH_EXTENSION alarm (0 of 4 session starts)
fix: fm_pi_extension_owns_supervision = true (live pi 91873 owns supervision)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (3) ✅
  • ⚠️ tests/fm-turnend-guard.test.sh:1203 - Simplification: the two new turn-end tests (test_pi_turnend_nested_cli_does_not_replace_live_binding and test_pi_turnend_mark_loaded_claims_free_or_dead_lock) and their helpers (pi_turnend_extension_version, load_pi_turnend_extension_as_child, about 80 lines) repeat coverage the change already adds. tests/fm-pi-watch-extension.test.sh has test_pi_nested_cli_does_not_replace_live_binding and test_pi_mark_loaded_claims_free_or_dead_lock. Both load the same fm-primary-turnend-guard.ts in a child Node process, and both assert the .pi-turnend-extension-loaded pid for the nested case and for the free/dead-lock case. The intent asks for regression coverage, and one copy of that coverage is enough. The second copy doubles the fixture code that must change whenever the writer rule changes. Recommended remedy: remove these additions from fm-turnend-guard.test.sh, or keep them only if you deliberately want per-file ownership of the turn-end writer tests.
  • ℹ️ tests/fm-pi-watch-extension.test.sh:96 - fm_test_pi_extension_version is a copy of fm_pi_extension_version from bin/fm-wake-lib.sh (the shasum/sha256sum/cksum ladder), and pi_turnend_extension_version in tests/fm-turnend-guard.test.sh:1177 is a third copy. This same file already sources fm-wake-lib.sh inside assert_pi_supervision_bound, and the new live e2e calls the library function directly. Separately, assert_pi_supervision_bound (line 129) calls fm_pi_extension_loaded for both markers right after fm_pi_extension_owns_supervision, which already runs those exact checks against the same versions and lock. Suggested fix: seed the markers using the sourced fm_pi_extension_version and drop the two repeated fm_pi_extension_loaded calls. No behavior changes: if the hashing rule ever drifts, the tests fail loudly either way, so this only removes a parallel copy.

🔧 Fix applied.
1 warning still open:

  • ⚠️ docs/verification/runtime-backends.md:2182 - This line says tests/fm-pi-watch-extension.test.sh and tests/fm-turnend-guard.test.sh both cover the new marker-writing rule with a stubbed Node child. That is no longer true. The previous fix round (commit 1f0b9ca) removed the turn-end copies of those tests from fm-turnend-guard.test.sh and did not update this line. The file now has no nested-child or free/dead-lock marker test. What still covers the turn-end marker is test_pi_nested_cli_does_not_replace_live_binding and test_pi_mark_loaded_claims_free_or_dead_lock in fm-pi-watch-extension.test.sh, which load both extensions and check both marker files. Anyone reading this verification record would believe the rule is pinned in a second suite that no longer has those tests. Fix: remove and tests/fm-turnend-guard.test.sh so the line names only fm-pi-watch-extension.test.sh, and say that its tests check both the watch marker and the turn-end marker.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ tests/fm-pi-watch-extension.test.sh:4244 - No test that runs by default checks the branch every real Pi session depends on: the lock names this process and that process is alive, so the marker must be written. The new rule in both extensions only allows that write because of the new lockPid !== String(process.pid) check. The new tests cover the other cases. The nested test puts the parent shell's pid in the lock, so nothing is written. The free/dead test uses a missing or dead lock, so the marker is written. The live guard's positive control also uses a free lock. The only test that runs a real session with a lock naming the Pi process is the edited tests/fm-pi-primary-live-e2e.test.sh, and it only runs on request (fm_live_gate opt-in FM_PI_LIVE_E2E). Failure case: a later edit drops or inverts the self-pid check. Every live Pi primary then stops writing its markers once fm-lock.sh records its pid (pidAlive(self) is true), and session start prints the same false PI_WATCH_EXTENSION: not loaded alarm this change fixes. All default tests would still pass. The verification line in docs/verification/runtime-backends.md:2182 says this suite "pins the same writer rule", so it overstates what is covered. Fix: add a case in which the Node child first writes ${process.pid} to state/.lock, the way the existing tests at line 178 and nearby already do. Then import both extensions and assert that both markers name that pid. For example, add an env switch to load_pi_extensions_as_child or a third step in test_pi_mark_loaded_claims_free_or_dead_lock.
  • ℹ️ .pi/extensions/fm-primary-turnend-guard.ts:40 - This change leaves dead code in the turn-end guard. markLoaded was the only caller of lockOwnership(), and it now reads the lock itself. So lockOwnership() (lines 40-55), its helper parentPid() (lines 25-29, which runs ps to walk parent pids), and type LockOwnership (line 15) are now called only by each other. The watch extension still needs its own copy for arming, but this file no longer uses any of it. A reader will assume the turn-end guard still makes ancestry-based ownership decisions. Fix: delete those three definitions. Keep pidAlive and the spawnSync import, which the taskkill path at line 166 still uses. Behavior does not change.

🔧 Fix applied.
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
Nested pi --help run inside a live Pi primary session leaves the session's markers in place, and the next session start prints no PI_WATCH_EXTENSION alarm ✅ pass live Fix lab: markers stayed at live Pi pid 91873 after !!pi --help, and !!bin/fm-session-start.sh printed no PI_WATCH_EXTENSION alarm (tui-fix-pi-session.txt, live-pi-tui-before-after.md)
Regression reproduced on base: the same nested pi --help replaces both markers with its dead pid and session start prints the false alarm ✅ pass live Base lab: markers became dead pid 15305 while Pi 81527 was live; session start printed PI_WATCH_EXTENSION: not loaded; fm_pi_extension_owns_supervision returned false (tui-base-pi-session.txt, base-…
A Pi-harness fm-spawn refused for a missing project (its pi --help probe still runs) leaves the live binding in place and session start stays quiet ✅ pass live Fix lab: spawn refused at fm-spawn.sh:2779 after the probe, no tmux window created, markers still 91873, no alarm. The same step on base replaced the markers with probe pid 19394 (tui-base-spawn-probe…
A real session whose lock names the Pi process itself still writes its markers after /reload, and the watcher still arms ✅ pass live Fix lab: after /reload with lock=91873, both marker mtimes advanced and they still name 91873; the stub arm log shows arm calls from ppid 91873; session start afterward printed no alarm
A fresh Pi start on a free lock still binds both markers, then session start takes the lock without an alarm ✅ pass live Fix lab: markers named 91873 immediately on start with no lock; !!bin/fm-session-start.sh printed lock acquired: harness pid 91873 and primary harness: pi, with no alarm. The live guard's positi…
Committed regression coverage catches the bug: the live guard and the new unit tests pass on the fix and fail on the base code ✅ pass live live-guard-fix.txt (3/3 ok), live-guard-base.txt (exit=1, pi --help replaced a live watch binding), unit-suite-fix.txt (exit 0), unit-suite-base.txt (`watch marker pid was replaced by a nested Pi pr…
  • bin/fm-test-run.sh tests/fm-pi-nested-probe-marker-live-e2e.test.sh on this branch (real Pi 0.86.1 CLI): 3/3 ok
  • Same live guard run against a git archive 09dc7b39 tree: fails with not ok - pi --help replaced a live watch binding (Pi 0.86.1)
  • bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh on this branch: exit 0, including the nested-child, free/dead-lock and self-lock tests
  • The new tests/fm-pi-watch-extension.test.sh run against base extensions: fails with not ok - watch marker pid was replaced by a nested Pi process
  • Live lab, fix tree: plain pi TUI in private tmux socket fm-lab-pimarker-fix, then !!bin/fm-session-start.sh, !!pi --help, !!bin/fm-session-start.sh, !!bin/fm-spawn.sh probe-x /nonexistent-project --scout --harness pi --backend tmux, !!bin/fm-session-start.sh, /reload, !!bin/fm-session-start.sh; marker, lock and mtime checks after each step
  • Live lab, base tree: same flow in fm-lab-pimarker-base; the false PI_WATCH_EXTENSION alarm appears after the nested pi --help; a restarted base session shows the fm-spawn probe also replacing both markers
  • fm_pi_extension_owns_supervision (bin/fm-wake-lib.sh) run against the live lab state for fix and base
  • Teardown: both lab tmux servers killed, lab Pi processes confirmed exited, /tmp lab dir removed, worktree clean
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

A short-lived child such as fm-spawn's pi --help probe was treated as
lock-owned through ancestry and overwrote both markers with a pid that
died immediately, causing false supervision alarms.
…s Pi change. They fail with the same tests on main at the base commit 09dc7b3 (CI run 35613893238). Two tests that failed here but not visibly on main (fm-kimi-harness and fm-spawn-compact-adviser-disable-remote) were in main's shard 9, which stopped at an actionlint download error before any test ran. Two regressions came in when the fork's own commits were rebased onto upstream on Sep 21. As you asked, I kept the previous agent's two partial fixes. I checked each one against the logs and locally, and made no other changes. 1. bin/fm-spawn.sh: restored the short staged launch line. Upstream kunchenguid#4994 (a452a79) writes the full launch command to a private file and types only `. <launch-file>` into the pane, because typed lines over about 1,024 bytes get cut off. The fork's #57 (a32fce8) put the old `spawn_send_literal "$T" "$LAUNCH"` back while resolving a merge, so it typed the whole command again. That broke fm-claude-trust, fm-backend-orca, fm-kimi-harness, fm-spawn-dispatch-profile, both fm-spawn-compact-adviser-disable suites, fm-remote-secondmate-trace-context and fm-remote-secondmate-parent-binding. The fix is one line that restores kunchenguid#4994's `spawn_send_literal "$T" ". $(shell_quote "$LAUNCH_FILE")"` and keeps #57's `SPAWN_LAUNCH_SENT=1`. 2. tests/fm-remote-reply.test.sh: the fixture also resolves the `default` key. Upstream kunchenguid#3764 added two decision lines with no key (`needs-decision [at=...]: which base branch?`), and these count under the key `default`. The fork's #48 made the automatic recovery repost wait while the mate has any open decision, so "the one automatic recovery repost was not sent". I confirmed this by printing the open decisions at that point: only `default needs-decision which base branch?` was open. Only the fixture's setup changed; every assertion is unchanged, and #48's wait-while-open rule still applies. Verification on macOS: all 8 spawn test files and fm-remote-reply pass through bin/fm-test-run.sh. With the spawn line reverted, fm-claude-trust fails with the same message as CI ("the launch command did not carry the brief the worker must read"). bash -n and shellcheck -S warning pass on both files. No Pi code was touched
@tiago-peixoto
tiago-peixoto marked this pull request as ready for review September 21, 2026 22:28
@tiago-peixoto
tiago-peixoto merged commit e03c32a into main Sep 21, 2026
19 checks passed
tiago-peixoto added a commit that referenced this pull request Sep 22, 2026
A short-lived child such as fm-spawn's pi --help probe was treated as
lock-owned through ancestry and overwrote both markers with a pid that
died immediately, causing false supervision alarms. Only the process the
session lock names, or one about to claim a free or dead lock, may now
publish the marker.
tiago-peixoto added a commit that referenced this pull request Sep 22, 2026
A short-lived child such as fm-spawn's pi --help probe was treated as
lock-owned through ancestry and overwrote both markers with a pid that
died immediately, causing false supervision alarms. Only the process the
session lock names, or one about to claim a free or dead lock, may now
publish the marker.
tiago-peixoto added a commit that referenced this pull request Sep 25, 2026
A short-lived child such as fm-spawn's pi --help probe was treated as
lock-owned through ancestry and overwrote both markers with a pid that
died immediately, causing false supervision alarms. Only the process the
session lock names, or one about to claim a free or dead lock, may now
publish the marker.
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