feat(pi): nudge captain on telegram for settled decision waits - #10
Merged
Conversation
morris2spears
force-pushed
the
fm/firstmate-decision-telegram-nudge-pi
branch
from
August 10, 2026 18:27
c23bffa to
bcfadae
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Implement the Pi and pi-signed primary-session half of the captain-attention Telegram nudge. When a settled Pi run's latest non-empty assistant chat text addresses Captain and asks a question or clearly requests a decision, arm a non-blocking 30-second timer and send exactly one content-free message, 'Captain, something's awaiting your attention.', only if Telegram mode is opted in and the session is a genuine primary scope. Skip the exact routine 'Captain, shipshape.' response, empty/tool-noise turns, watcher operational turns without an ask, and all crewmate or scout worktrees. Disarm on genuine captain presence, before a new agent run, and on session shutdown. Reuse the Claude sibling's shared bin/fm-decision-nudge.sh marker/timer/phone-inbox contract through additive --pi-arm and --pi-resolved modes without changing .claude/settings.json. Prefer agent_settled; do not guess at blocking UI overlays because Pi 0.82.1 exposes no global hook around arbitrary ctx.ui confirm/select/input/custom calls. Include deterministic tests, typecheck registration, shellcheck-clean scripts, and maintainer verification documentation.
What Changed
.pi/extensions/fm-primary-decision-nudge.ts, which watches settled Pi turns and - only in a genuine primary scope with Telegram opted in - arms a non-blocking 30s timer that sends one content-free "Captain, something's awaiting your attention." message. It skips the routine "Captain, shipshape." reply, empty/tool-noise turns, watcher turns with no ask, and crewmate/scout worktrees, and disarms on captain presence, before a new agent run, and on session shutdown.bin/fm-decision-nudge.shwith additive--pi-arm/--pi-resolvedmodes reusing the shared marker/timer/phone-inbox contract (no.claude/settings.jsonchanges), and wired the new extension intobin/fm-spawn.sh(-eflag) andbin/fm-session-start.sh(third loaded-marker diagnostic) plus the Pi supervision snippet..pi/extensions/lib/fm-primary-loaded-marker.tsfor all three primary extensions, and addedtests/fm-pi-decision-nudge.test.sh(8 behavior cases) plus session-start/supervision test updates anddocs/verification/pi-decision-nudge.md.Risk Assessment
✅ Low: The final commit only corrects two stale documentation sentences to match the code and removes a now-unused import, and every earlier round's substantive finding (scope-gated disarm, tightened ask heuristic, three-extension load wiring, repaired session-start fixtures, shared marker lib) is implemented with matching deterministic tests.
Testing
Ran the targeted Pi nudge, Pi extension, session-start, supervision-snippet, and spawn-dispatch tests; all change-related assertions pass. Added no source changes. Produced product-level evidence by driving the real Pi extension and the real nudge script against a fixture primary home with a stand-in phone-inbox client: the captain gets exactly one content-free 'Captain, something's awaiting your attention.' for a genuine settled ask, and nothing at all for shipshape replies, watcher turns without an ask, empty turns, answered turns, crew worktrees, or a missing Telegram opt-in. Also confirmed real pi 0.82.1 loads the new extension and writes its loaded marker, and captured the real session-start/supervision CLI text naming it. This change is CLI/notification-facing with no rendered UI surface, so no screenshot applies; the phone-message transcript is the end-user artifact. Two session-start failures on this macOS host (bash 3.2 lacking BASHPID, and a Herdr husk case) reproduce identically on the base commit and are unrelated to the change.
Evidence: End-to-end Pi nudge transcript (what lands on the captain's phone)
=== Pi primary session, Telegram mode ON, nudge delay 3s (30s in production) === [18:10:44] PI CHAT (captain asked a real decision) assistant> Captain, CI is green on both branches. Should I land this on main now? timer armed? yes [18:10:48] [18:10:47] PHONE <- Captain, something's awaiting your attention. === captain answers within the delay -> no page === [18:10:48] PI CHAT (second decision ask) assistant> Captain, please choose: revert the deploy or patch forward? timer armed? yes [18:10:48] captain types in the pane (interactive input) timer armed? no [18:10:52] phone log unchanged: [18:10:47] PHONE <- Captain, something's awaiting your attention. === turns that must never page him === [18:10:52] PI CHAT (routine idle reply) assistant> Captain, shipshape. timer armed? no [18:10:53] PI CHAT (watcher operational turn, no ask) assistant> Captain, PR #7 is merged and CI is green. I'll confirm the deploy once you're back. timer armed? no [18:10:53] PI CHAT (tool-noise / empty turn) assistant> (empty turn) timer armed? no === final phone contents (exactly one content-free message) === [18:10:47] PHONE <- Captain, something's awaiting your attention. === a crewmate/scout task worktree of the same repo stays silent === crew worktree armed a timer? no === Telegram mode OFF (no standing opt-in) -> feature inert === timer armed? no phone log lines: 1Evidence: Evidence driver script used for the transcript
Evidence: Real session-start diagnostic naming the nudge extension
PI_WATCH_EXTENSION: not loaded - approve Pi project trust once per clone, then restart plain pi so <root>/.pi/extensions/fm-primary-turnend-guard.ts, <root>/.pi/extensions/fm-primary-pi-watch.ts, and <root>/.pi/extensions/fm-primary-decision-nudge.ts auto-load for turn-end guard, background wake, and captain-attention nudge coverage; use -e ... only if project hooks are not trustedEvidence: Real pi 0.82.1 loaded the extension and wrote its loaded marker
pi --version: 0.82.1 state/.pi-decision-nudge-extension-loaded -> sha256:9d36b50d085cc4bc80eb82437f16040cb38f1e5308059d70e639340cf5b038c8Evidence: Session-start suite output with Pi diagnostics reached
Evidence: Forced Pi extension typecheck log (typescript 5.9.3)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
🔧 **Review** - 5 issues found → auto-fixed (3) ✅
bin/fm-decision-nudge.sh:82- The--pi-resolved/--claude-resolvedfast path deletes the marker before any scope check, while arming is gated byfm_primary_scope_matches..pi/extensions/fm-primary-decision-nudge.tsis a tracked file, so it is present and auto-discovered in every crewmate/scout worktree of this repo (crew pi panes are launched without--no-extensions, bin/fm-spawn.sh:490), and itsinput/before_agent_start/session_shutdownhandlers fire on every crew turn. fm-spawn.sh deliberately does not pin FM_HOME/FM_STATE_OVERRIDE for crew panes (bin/fm-spawn.sh:1633-1652 explicitly blanks those leaked vars only for secondmate launches), so a crew pane that inherits FM_HOME from the daemon env resolves STATE to the captain's primary home and silentlyrm -fs the pending marker - the captain's armed nudge is cancelled by an unrelated crewmate turn with no captain presence. Gate the disarm path on the same primary-scope predicate (or make the extension register no handlers outside primary scope, as Calm does)..pi/extensions/fm-primary-decision-nudge.ts:24-/\b(?:choose|select|pick|decide|confirm|approve)\b/imatches these verbs anywhere in a Captain-addressed message, with no requirement that the sentence be an ask. A settled watcher/operational turn like "Captain, PR feat(bin): inject Pi Calm into ordinary crew panes #7 is merged and CI is green. I'll confirm the deploy once you're back." contains no question and no pending decision, yet arms the timer and pages the captain's phone. The intent requires skipping "watcher operational turns without an ask". Consider requiring the verb to appear in an imperative/interrogative position (e.g. anchored near the end, or combined with the?/ explicit request patterns) rather than bare verb presence..pi/extensions/fm-primary-decision-nudge.ts:1- Unlike its two tracked primary siblings, the new extension has no-einjection in bin/fm-spawn.sh (__PITURNEND__/__PIWATCH__, lines 488/1609-1621) and no loaded-marker check in bin/fm-session-start.sh:318-331. It works only via project-trust auto-discovery; in the fallback path fm-session-start.sh itself documents ("use -e <turnend> -e <piwatch> only if project hooks are not trusted") and in pi secondmate launches, the nudge extension silently never loads and nothing reports it as missing. Either add it to those two wiring points or state that trust-only discovery is the accepted scope..pi/extensions/fm-primary-decision-nudge.ts:67- The loop looks like a backward scan but returns unconditionally at the first assistant entry, so an assistant entry with no text blocks (tool-call-only, or an aborted/empty final message) yields null instead of falling back to the latest non-empty assistant text the intent describes ("latest non-empty assistant chat text"). The conservative behavior also satisfies the intent's "skip empty/tool-noise turns" clause, so this is a wording divergence rather than a proven miss - confirm which reading is wanted, and if the current one is intended, drop the loop or comment why it stops at the first assistant entry.bin/fm-decision-nudge.sh:10- The header documents Claude hook wiring in.claude/settings.json, citesdocs/verification/decision-nudge.md(line 12) and adocs/configuration.md"Captain-attention nudge" section (line 64) - none of which exist on this branch. Consequently--claude-pending/--claude-resolvedare unreachable and uncovered by the new suite here. Fine if the Claude sibling lands them, but as merged the file documents behavior this repo state does not have; note the dependency or trim the references until the sibling merges.🔧 Fix: scope nudge disarm, tighten ask heuristic, wire pi loading
3 issues (1 error, 1 warning, 1 info) still open:
tests/fm-session-start.test.sh:1331- bin/fm-session-start.sh now requires a third loaded marker and prints a new three-extension sentence, but tests/fm-session-start.test.sh was not updated, so three existing assertions now fail deterministically: (1) line 1331assert_not_contains "PI_WATCH_EXTENSION: not loaded"- its fixturewrite_pi_loaded_markers(line 547) writes only.pi-watch-extension-loadedand.pi-turnend-extension-loaded, and no fixture installs.pi/extensions/fm-primary-decision-nudge.ts, soPI_NUDGE_VERSIONis empty andpi_extension_loadedreturns 1; (2) line 1250 and (3) line 1278 assert the exact substring"... fm-primary-turnend-guard.ts and $root/.pi/extensions/fm-primary-pi-watch.ts auto-load", which the new message ("%s, %s, and %s auto-load for turn-end guard, background wake, and captain-attention nudge coverage") no longer contains. Add aninstall_pi_decision_nudge_extension_fixture+write_pi_decision_nudge_loaded_marker, extendwrite_pi_loaded_markers, and update the two wording assertions.docs/supervision-protocols/pi.md:5- The Pi supervision snippet rendered into every Pi session still documents the trust-free fallback as-e __FM_PI_TURNEND_EXT__ -e __FM_PI_EXT__(line 5) and names only two extensions (lines 21-22), while fm-session-start.sh now demands all three loaded markers. A captain who follows the documented repair relaunches with two extensions and the digest keeps printingPI_WATCH_EXTENSION: not loadedforever, with no instruction that would clear it. Either add the nudge extension to the snippet (bin/fm-supervision-instructions.sh:122 placeholder plus tests/fm-supervision-instructions.test.sh:165's exact-e $turnend -e $watchassertion), or keep the diagnostic gated on the two extensions the snippet can actually restore..pi/extensions/fm-primary-decision-nudge.ts:22-parentPid,pidAlive,lockOwnership, andmarkLoadedare now byte-identical in three tracked Pi extensions (fm-primary-turnend-guard.ts:53, fm-primary-pi-watch.ts:145, and this file)..pi/extensions/lib/already exists for exactly this kind of shared helper (fm-operational-input.ts is imported the same way and is already copied/type-checked alongside the extensions). Optional cleanup, not a blocker: extracting the loaded-marker/lock-ownership contract into one lib module would keep the three sessions' definition of "this session owns the lock" from drifting.🔧 Fix: fix pi extension load tests, snippet, share marker lib
3 issues (1 warning, 2 infos) still open:
.agents/skills/harness-adapters/SKILL.md:103- The Pi adapter facts still describe two primary extensions after this branch made it three. SKILL.md:103 ("...fm-primary-turnend-guard.ts plus ...fm-primary-pi-watch.ts, both project-local extensions"), :298 ("reports when the live Pi-family session has not loaded both the turn-end guard and watcher extensions") and :299 ("fm-spawn.sh --secondmate launches ... with both -e ...turnend-guard.ts and -e ...pi-watch.ts") now contradict bin/fm-spawn.sh:491 (three-eflags) and bin/fm-session-start.sh:331 (three required markers); docs/configuration.md:214 has the same stale two-extension sentence for pi secondmate launches. SKILL.md:104 itself mandates updating these concise facts whenever a primary watcher adapter changes, and docs/supervision-protocols/pi.md was already updated in this commit, so these two are the leftovers..pi/extensions/lib/fm-primary-loaded-marker.ts:43-extensionVersionOfstill hashes only the importing extension file, so a future update that touches just this shared lib changes the runtime behavior of all three primary extensions while every.pi-*-extension-loadedstamp stays valid and bin/fm-session-start.sh keeps reporting the running session as current - no restart prompt. This is the same property the pre-existing lib/fm-operational-input.ts import already has, so the refactor inherits rather than introduces it; noting the tradeoff, no action needed unless the staleness check is meant to cover the lock-ownership contract too..pi/extensions/fm-primary-pi-watch.ts:11-spawnSyncis still imported but no longer referenced anywhere in the file afterparentPidmoved to the shared lib (onlyspawnandChildProcessremain in use). Harmless dead import left by the extraction; drop it from the import list.🔧 Fix: update pi adapter docs, drop dead spawnSync import
✅ Re-checked - no issues remain.
tests/fm-session-start.test.sh:739- tests/fm-session-start.test.sh has two failures on this macOS host that are unrelated to the change and reproduce identically on the base commit 70436b5: the concurrent session-lock case uses $BASHPID (bash >= 4; host bash is 3.2) and the Herdr husk recovery case reports an unexpected SECONDMATE_LIVENESS line. I ran the suite with only those two cases stubbed out to reach the new Pi assertions, which all pass, including 'session start rejects Pi sessions missing the captain-attention nudge marker'.tests/fm-pi-primary-types.test.sh:8- tests/fm-pi-primary-types.test.sh gate-skips locally because tsc is not on PATH. Forcing it with typescript 5.9.3 (the version Pi 0.82.1 pins) surfaces only two pre-existing errors in the untouched .pi/extensions/fm-calm.ts (TS2322 at 253, TS2345 at 294) against the installed Pi type declarations; the newly registered fm-primary-decision-nudge.ts and lib/fm-primary-loaded-marker.ts typecheck clean. Strict typecheck evidence for the new files is therefore established, but the suite as a whole cannot go green on this host../bin/fm-test-run.sh tests/fm-pi-decision-nudge.test.sh- all 8 behavior cases pass (heuristic, arm/disarm wiring, single-claim timer, opt-in gate, worktree scope)./bin/fm-test-run.sh tests/fm-pi-watch-extension.test.sh tests/fm-turnend-guard.test.sh tests/fm-calm-pi-extension.test.sh tests/fm-documentation-audiences.test.sh- pass./bin/fm-test-run.sh tests/fm-supervision-instructions.test.sh tests/fm-spawn-dispatch-profile.test.sh- passbash tests/fm-session-start.test.sh(with the bash4-only lock case and the pre-existing Herdr husk case stubbed) - all Pi extension diagnostics pass, includingsession start rejects Pi sessions missing the captain-attention nudge markergit show 70436b5:tests/fm-session-start.test.sh | bash- confirms the two failures predate the changePATH=<tsc shim> ./bin/fm-test-run.sh tests/fm-pi-primary-types.test.shwith typescript 5.9.3 - new extension and shared marker lib typecheck clean; only pre-existing fm-calm.ts errors remainManual end-to-end:/var/folders/.../no-mistakes-evidence/01KZPBW2KBWV24ACQXDBRW16DH/e2e-pi-nudge.shdrives the real extension + realbin/fm-decision-nudge.sh+ capture tg client and prints the phone transcriptManual real-harness load:pi -p 'say ok' -e .pi/extensions/fm-primary-decision-nudge.tsunder pi 0.82.1 wrotestate/.pi-decision-nudge-extension-loadedManual CLI render:./bin/fm-supervision-instructions.sh --harness piandFM_FAKE_HARNESS=pi ./bin/fm-session-start.shshow the nudge extension in the load diagnostic and-efallbackAGENTS.md:121- AGENTS.md's state/ inventory lists most runtime artifacts but does not list the new private state/.decision-nudge-pending marker, nor the pre-existing .pi-*-extension-loaded markers. I deliberately left it alone: the inventory already omits every Pi extension load marker, so adding only the nudge marker would be inconsistent, and the marker is private volatile state whose contract is owned by bin/fm-decision-nudge.sh's header. A follow-up could add one line covering all Pi/primary extension and nudge markers together.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.