Skip to content

CI mirror: fix(pi): block the turn end from agent_end instead of reacting after settle - #3

Open
sbracewell64 wants to merge 5 commits into
mainfrom
fm/pi-only-harness-plan
Open

CI mirror: fix(pi): block the turn end from agent_end instead of reacting after settle#3
sbracewell64 wants to merge 5 commits into
mainfrom
fm/pi-only-harness-plan

Conversation

@sbracewell64

Copy link
Copy Markdown
Owner

CI mirror of upstream PR kunchenguid#1082, opened inside this fork solely to run the repository's own workflows on the identical commit. Upstream cross-fork PRs do not run workflows without maintainer approval. Not intended to merge; the upstream PR is the real submission. Body reproduced verbatim below so the compliance check tests the real thing.

Intent

Ship stage 0 of a scout investigation into making Pi the single harness: fix Firstmate's own Pi primary turn-end guard so it genuinely blocks a turn end instead of reacting after one.

Background the diff cannot show: an investigation established that Firstmate previously believed Pi could not block a turn end and could only force one bounded follow-up, which was a main reason Pi-for-Claude was rated a downgrade. That belief was wrong. Pi drains both message queues before emitting agent_end, so a follow-up queued from an agent_end handler makes _handlePostAgentRun() return true and the session calls agent.continue() - the run continues and agent_settled, the idle signal, never fires. The old guard hooked agent_settled, which fires only after that loop exits, so the idle signal escaped before the follow-up re-opened the turn. Measured with pi 0.81.1 against a local mock provider: the agent_end guard emits one idle signal, the agent_settled version emits two.

Deliberate decisions the captain scoped and approved, which may look surprising in the diff:

  • Scope is EXACTLY this event move plus documentation and a regression test. Other findings from the same investigation (a missing claude-opus-5 model catalog entry, adopting native Claude Code guards, packaging Firstmate as a plugin, retiring harness adapters) are explicitly deferred pending captain decisions and must NOT be touched here.
  • The single-follow-up re-entrancy latch is kept deliberately and unchanged. Pi can block repeatedly - a probe blocked 12 consecutive times with no ceiling - but the captain scoped this change to the event move only, so a block budget like Claude's was deliberately NOT introduced.
  • A known ordering nuance is documented rather than fixed: agent_end also fires at auto-retry and compaction boundaries, so a run needing one of those while supervision is already unhealthy spends its latched follow-up at that earlier boundary. The recovery instruction still lands in the same run, so this is an ordering nuance, not a blind turn end. Fixing it would require a block budget, which is out of scope.
  • Evidence went into the existing docs/verification/supervision.md Turn-end guard section rather than a new file, following the repo's one-owner rule.
  • docs/verification/supervision.md deliberately records that three Node-driven Pi tests in this suite, plus two other Pi test files, require a Node build with TypeScript type stripping and report not ok on a Node compiled without it. That is a pre-existing environmental limitation on origin/main, not introduced here, and is recorded honestly rather than hidden.

The new test test_pi_extension_blocks_before_the_idle_signal models Pi's real loop and was verified to fail against the agent_settled version.

What Changed

  • .pi/extensions/fm-primary-turnend-guard.ts now runs the shared bin/fm-turnend-guard.sh predicate from agent_end instead of agent_settled. Pi drains both message queues before emitting agent_end, so a follow-up queued there makes _handlePostAgentRun() return true and the session calls agent.continue() — the run keeps going and the idle signal never fires. The old agent_settled wiring ran after that loop had already exited, so the turn ended blind and the follow-up only re-opened it afterwards. Measured against pi 0.81.1 with a local mock provider: one idle signal with the agent_end guard, two with the agent_settled version.
  • A second agent_settled listener is registered that does nothing but clear the single-follow-up latch. agent_end fires again only if the run continues, so a run that dies after the guard queued its follow-up (ESC, fatal session error, discarded queue) would otherwise leave the latch set and let the next logical run end unguarded. The existing one-follow-up latch is otherwise kept unchanged — no block budget was added, since Pi can block repeatedly (a probe blocked 12 consecutive times) and that is out of scope here.
  • Docs and regression coverage follow the reclassification: docs/turnend-guard.md, docs/architecture.md, bin/fm-turnend-guard.sh's header, and the harness-adapters skill now describe Pi as a direct blocker alongside Claude and Codex, with the auto-retry/compaction ordering nuance recorded rather than fixed. docs/verification/supervision.md gains the pi 0.81.1 probe evidence in its existing Turn-end guard section, plus an honest note that the Node-driven Pi extension cases need a Node build with TypeScript type stripping (pre-existing on origin/main, and unverified in CI). tests/fm-turnend-guard.test.sh adds test_pi_extension_blocks_before_the_idle_signal, verified to fail against the agent_settled version, and test_pi_extension_releases_the_latch_when_a_run_dies_before_continuing.

Risk Assessment

✅ Low: The branch is now a single well-bounded event move backed by two behavioral regression tests, with the one real correctness gap fixed and every doc, skill, and code comment reconciled to the same mechanism; this round changed only prose.

Testing

Ran the targeted tests/fm-turnend-guard.test.sh suite (all 53 cases pass) after fixing the local environment blocker — the system Node is compiled without TypeScript type stripping, exactly the pre-existing limitation the change documents, so I ran the suite under an official Node 22.22.1 build. To prove the intent rather than just the assertions, I drove the real pi 0.81.1 CLI against a local mock provider with the actual tracked extension: with the shipped agent_end guard the predicate runs before the run can settle, Pi continues the same run, the FIRSTMATE_OP recovery instruction is delivered to the model, and the idle signal fires exactly once; with the base agent_settled version the run ends first, the idle signal fires twice, and the recovery instruction never reaches the model. I also confirmed the new regression test fails when pointed at the pre-change extension, and that the changed extension passes a strict typecheck against pi 0.81.1. The only failure observed anywhere was tests/fm-pi-primary-types.test.sh tripping on the untouched fm-calm.ts, which fails identically at the base commit.

Evidence: Real pi 0.81.1 before/after ordering transcript (agent_settled vs agent_end)

--- BEFORE: base a5fe1bc, guard hooked on agent_settled --- event order: LLM_REQUEST AGENT_END GUARD SETTLED SETTLED what the model was actually sent: --- model request 2 messages, last role=user --- [{"type":"text","text":"hello"}] -> The run ended, the guard only then ran, and the idle signal fired TWICE. No second LLM_REQUEST: the recovery instruction never reached the model. --- AFTER: 6d3f045, guard hooked on agent_end --- event order: LLM_REQUEST GUARD AGENT_END LLM_REQUEST AGENT_END SETTLED what the model was actually sent: --- model request 2 messages, last role=user --- [{"type":"text","text":"hello"}] --- model request 4 messages, last role=user --- [{"type":"text","text":"⁣FIRSTMATE_OP: v1 turn-end-guard: TURN WOULD END BLIND - supervision is off. The watcher cycle is missing, failed, or unhealthy. Follow the harness recovery instruction below before ending the turn.\n\nrun bin/fm-watch-arm.sh as its own background task to repair supervision\n"}] -> The guard ran BEFORE the run could settle, Pi continued the SAME run, the recovery instruction was delivered, and the idle signal fired exactly ONCE. One GUARD line: the second AGENT_END consumed the latch without re-running the predicate, so there is no recursion.

Pi 0.81.1 turn-end guard: does it BLOCK the turn end, or only react after it?
============================================================================

Real `pi` CLI (0.81.1), the real tracked extension file, and a local mock provider
on 127.0.0.1:8791, so no model request left the machine. Isolated HOME and FM_HOME.
A stub bin/fm-turnend-guard.sh always reports "supervision is off" (exit 2) and
appends GUARD. A probe extension appends AGENT_END / SETTLED. The mock server
appends LLM_REQUEST and records what the model actually saw.
SETTLED (agent_settled) is the idle signal a supervisor must never see while
supervision is unhealthy.

Command per side (evidence/pi-e2e/run.sh):
  pi -p "hello" --provider mockp --model mock-model -ne -na --no-session -nbt \
     -e mock-provider.ts -e <guard-variant>.ts -e probe.ts

--- BEFORE: base a5fe1bc, guard hooked on agent_settled ---------------------

event order:
  LLM_REQUEST
  AGENT_END
  GUARD
  SETTLED
  SETTLED

what the model was actually sent:
  --- model request 2 messages, last role=user ---
  [{"type":"text","text":"hello"}]
  
  -> The run ended (AGENT_END), the guard only then ran, and the idle signal fired
     TWICE. There is no second LLM_REQUEST: the recovery instruction never reached
     the model in this run. The turn ended blind first.

--- AFTER: 6d3f045, guard hooked on agent_end ------------------------------

event order:
  LLM_REQUEST
  GUARD
  AGENT_END
  LLM_REQUEST
  AGENT_END
  SETTLED

what the model was actually sent:
  --- model request 2 messages, last role=user ---
  [{"type":"text","text":"hello"}]
  
  --- model request 4 messages, last role=user ---
  [{"type":"text","text":"⁣FIRSTMATE_OP: v1 turn-end-guard: TURN WOULD END BLIND - supervision is off. The watcher cycle is missing, failed, or unhealthy. Follow the harness recovery instruction below before ending the turn.\n\nrun bin/fm-watch-arm.sh as its own background task to repair supervision\n"}]
  
  -> The guard ran BEFORE the run could settle and Pi continued the SAME run: the
     FIRSTMATE_OP turn-end-guard recovery instruction was delivered to the model,
     and only afterwards did the idle signal fire, exactly ONCE.
     One GUARD line: the second AGENT_END consumed the single-follow-up latch
     without re-running the predicate, so there is no recursion.
- Evidence: Reproducible pi e2e harness (mock provider, probe extension, both guard variants) (local file: /tmp/no-mistakes-evidence/01KYFKA5WDGW852V0H5MPMMRM6/pi-e2e)
Evidence: New regression test fails against the pre-change agent_settled extension
$ FM_OLD_GUARD_ROOT=<base a5fe1bc extension> bash <driver running only test_pi_extension_blocks_before_the_idle_signal>
not ok - Pi guard must block the turn end before the idle signal fires: expected exit 0, got 1
status=1
Evidence: tests/fm-turnend-guard.test.sh full output (Pi cases at lines 38-42)

ok - .pi primary extension: agent_end forces one follow-up through the shared guard ok - .pi primary extension: guard blocks the run before agent_settled emits the idle signal ok - .pi primary extension: an interrupted run releases the latch without re-opening the guard ok - .pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up ok - .pi primary extension: delivery failure resets the logical-run latch

ok - fm_supervision_unhealthy: false with no state/*.meta at all
ok - fm_supervision_unhealthy: true with in-flight task and no beacon ever
ok - fm_supervision_unhealthy: true with in-flight task and a beacon far outside the grace window
ok - fm_supervision_unhealthy: false with in-flight task and a fresh beacon
ok - fm_supervision_status: FM_SUP_QUEUE_PENDING tracks state/.wake-queue
ok - fm_supervision_needed: X-mode relay poll needs supervision without changing the task predicate
ok - fm-turnend-guard: silent no-op with nothing in flight
ok - fm-turnend-guard: blocks when a fresh beacon has no live watcher lock
ok - fm-turnend-guard: blocks on a dead watcher lock even when the beacon is fresh
ok - fm-turnend-guard: silent no-op with a live watcher lock and fresh beacon
ok - fm-turnend-guard: blocks on a live watcher lock with an ancient beacon
ok - fm-turnend-guard: blocks with the exact required reason in the primary when unhealthy
ok - fm-turnend-guard: blocks from active FM_HOME state, not only repo-root state
ok - fm-turnend-guard: X-mode repair reason sources the cadence config
ok - fm-turnend-guard: ignores stale repo-root state when FM_HOME is set
ok - fm-turnend-guard: uses FM_STATE_OVERRIDE ahead of FM_HOME/state
ok - fm-turnend-guard: stop_hook_active=true always allows the stop (never blocks twice in one turn)
ok - fm-turnend-guard: blocks a blind turn end in a secondmate's own home (.fm-secondmate-home no longer excludes it)
ok - fm-turnend-guard: idle-by-default - silent in a secondmate home with nothing in flight
ok - fm-turnend-guard: stop_hook_active=true allows the stop in a secondmate home (never blocks twice in one turn)
ok - fm-turnend-guard: secondmate deferred-death recovery - silent while watched, forces re-arm once the watcher exits
ok - fm-turnend-guard: inert in a secondmate's own child worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: blocks a blind turn end in a treehouse-leased LINKED secondmate home (marker force-include)
ok - fm-turnend-guard: an invalid (empty) marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: a non-ASCII marker cannot spoof inclusion; linked worktree stays exempt
ok - fm-turnend-guard: inert in a crewmate/scout task worktree (linked git worktree) even when unhealthy
ok - fm-turnend-guard: fails open (never blocks) when jq is missing
ok - fm-turnend-guard: silent no-op on empty stdin
ok - fm-turnend-guard: runs well under the generous timing margin (0s)
ok - fm-turnend-guard-grok: forces one explicitly marked same-session resume when the shared predicate blocks
ok - fm-turnend-guard-grok: loop guard prevents a nested resume loop
ok - .claude/settings.json: Stop hook uses CLAUDE_PROJECT_DIR-anchored --claude guard command
ok - .codex/hooks.json: Stop hook invokes the shared primary guard
ok - .codex/hooks.json: Stop hook uses hook process root when payload cwd is outside
ok - .codex/hooks.json: Stop hook ignores nested git root guard scripts
ok - .opencode primary plugin: session.idle forces one follow-up through the shared guard
ok - .opencode primary plugin: guard path is anchored to worktree, not directory
ok - .pi primary extension: agent_end forces one follow-up through the shared guard
ok - .pi primary extension: guard blocks the run before agent_settled emits the idle signal
ok - .pi primary extension: an interrupted run releases the latch without re-opening the guard
ok - .pi primary extension: no-tool and multi-tool runs each inject exactly one guard follow-up
ok - .pi primary extension: delivery failure resets the logical-run latch
ok - .grok primary hook: Stop hook invokes the grok adapter
ok - fm-turnend-guard --claude: re-blocks a loop-guarded stop while unhealthy and unclaimed (incident regression)
ok - fm-turnend-guard --claude: X-mode-only homes re-block when auto-arm recovery is absent
ok - fm-turnend-guard --claude: allows the stop when the Stop auto-arm owner holds this home
ok - fm-turnend-guard --claude: fresh rewake epoch prevents a duplicate continuation for the same event
ok - fm-turnend-guard --claude: stale rewake epoch does not allow a blind stop
ok - fm-turnend-guard --claude: re-block budget stays below the 8-block cap and resets after degraded allow
ok - fm-turnend-guard --claude: any allow resets the consecutive-block budget
ok - fm-turnend-guard --claude: bounded claim wait avoids a token-consuming forced continuation
ok - fm-turnend-guard --claude: secondmate home re-blocks unclaimed and allows auto-arm-claimed stops
- Outcome: ⚠️ 1 info across 1 run (10m11s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 5 issues found → auto-fixed (2) ✅
  • ⚠️ .pi/extensions/fm-primary-turnend-guard.ts:145 - The re-entrancy latch guardFollowupActive is now both set and cleared inside the agent_end handler. Unlike agent_settled — which pi emits from the agent loop's finally and is therefore guaranteed once per logical run — agent_end is emitted per agent.prompt()/agent.continue() call, so it only fires again if the run actually continues. If the run terminates after the guard queued its follow-up but before another agent_end (user interrupt/ESC while the follow-up is still queued, a fatal session error, or pi discarding the queue), the latch stays true for the lifetime of the process and the next logical run's turn end is silently skipped with no guard invocation at all — one unguarded blind turn end, exactly the failure class this guard exists to prevent. Under the previous agent_settled wiring the clearing event was guaranteed, so this robustness property regressed. The natural belt-and-braces fix (also clear the latch from an agent_settled listener, keeping agent_end as the blocking hook) is currently forbidden by the new assert_not_contains &#34;$content&#34; &#39;pi.on(&#34;agent_settled&#34;&#39; assertion at tests/fm-turnend-guard.test.sh:784, so this needs an author decision rather than a silent edit.
  • ⚠️ bin/fm-turnend-guard.sh:14 - The shared guard's header comment still reads "OpenCode, pi, and grok adapters use the same predicate and force one bounded follow-up because their turn-end events are passive." This change reclassified Pi as a direct blocker in docs/turnend-guard.md:81 ("Claude, Codex, and Pi block the turn end directly, while OpenCode and Grok use bounded passive follow-ups"), docs/turnend-guard.md:64, and .agents/skills/harness-adapters/SKILL.md:55-56, but the guard script itself — the file every adapter calls, and the one that points readers at docs/turnend-guard.md for per-harness mechanics — was left with the old claim. A reader landing in the guard first gets the exact wrong mental model of Pi's mechanism.
  • ℹ️ tests/fm-turnend-guard.test.sh:878 - idleEmittedBeforeFirstFollowUp can never become true in this harness: it is only set inside the sendUserMessage mock, sendUserMessage is only reachable from the awaited agentEnd handler, and every agentEnd call completes before idleEmitted = true on line 874. So the assertion "idle signal was emitted before the guard blocked" is dead code, and the property the test's name and comment advertise is not actually measured. What genuinely fails against the agent_settled version is the handler-registration throw on line 862 — which duplicates the string assertion already added at line 783. Either drop the dead flag or restructure the mock so the guard's follow-up delivery and the settle emission are independently ordered (e.g. deliver the follow-up asynchronously and emit settle only when the queue drains), so the ordering claim is really exercised.
  • ℹ️ docs/verification/supervision.md:64 - The section intro still reads "validated across all five harnesses on 2026-07-08 through 2026-07-12, with Claude's replacement Stop-owned path revalidated on 2026-07-24" — it was extended for Claude's revalidation but not for Pi's. The table row directly below now records a 2026-07-26 measurement on pi 0.81.1 with a different mechanism ("Blocking agent_end callback"), which falls outside the stated range. Add Pi's 2026-07-26 revalidation to the intro sentence so the date provenance stays self-consistent.
  • ℹ️ docs/turnend-guard.md:44 - "Pi listens for agent_end ..., runs once per logical agent run" is now imprecise: agent_end fires at every continuation, auto-retry, and compaction boundary, as the same document acknowledges at line 69. What is once-per-run is the follow-up (bounded by the latch); the bin/fm-turnend-guard.sh predicate subprocess itself is now spawned at each of those boundaries rather than once at settle. Rewording to "injects at most one follow-up per logical agent run" would keep the bullet accurate and avoid implying the predicate cost is unchanged.

🔧 Fix: release Pi guard latch on the guaranteed settle event
3 issues (1 warning, 2 infos) still open:

  • ⚠️ .agents/skills/harness-adapters/SKILL.md:285 - This line still reads "listens for logical-run agent_end, not per-tool-loop turn_end and not post-settle agent_settled", which the latch-release fix made false: .pi/extensions/fm-primary-turnend-guard.ts:179 now registers pi.on(&#34;agent_settled&#34;, ...). This is the agent-facing harness reference, so an agent reading it would conclude the settle listener is a bug and remove it, reintroducing the stuck-latch blind turn end the new test at tests/fm-turnend-guard.test.sh:894 exists to prevent. Reword to state that agent_end is the blocking hook and agent_settled is registered solely to release the latch, mirroring the wording already added at docs/turnend-guard.md:68-69.
  • ℹ️ docs/turnend-guard.md:95 - The regression-coverage sentence lists "Pi logical-run latching, Pi blocking before the idle signal" but was not extended for test_pi_extension_releases_the_latch_when_a_run_dies_before_continuing, even though the previous commit followed exactly this convention when it added test_pi_extension_blocks_before_the_idle_signal to the same list. Add the latch-release-after-an-interrupted-run case so the coverage inventory stays complete.
  • ℹ️ .pi/extensions/fm-primary-turnend-guard.ts:179 - The unconditional guardFollowupActive = false is safe only because pi emits agent_settled after the whole agent loop exits, which the inline comment and the 12-block probe in docs/verification/supervision.md:114 both establish for pi 0.81.1. If a future pi emitted settle between agent.continue() iterations, the latch would clear mid-loop and the guard would re-block at every agent_end with no block budget to stop it (the budget was deliberately deferred per the captain's scope). No action needed now; the assumption is documented in code and evidence, and this is worth rechecking on the next pi version bump rather than defending against speculatively.

🔧 Fix: document the Pi settle listener as latch release only
✅ Re-checked - no issues remain.

⚠️ **Test** - 1 info
  • ℹ️ tests/fm-pi-primary-types.test.sh:8 - tests/fm-pi-primary-types.test.sh fails in this environment, but only on the unchanged .pi/extensions/fm-calm.ts (TS2322 on renderResult, TS2345 on TerminalInputHandler) when typechecked against the installed pi 0.81.1. I confirmed this is pre-existing and unrelated to this change: the changed fm-primary-turnend-guard.ts typechecks clean both at 6d3f045 and at base a5fe1bc. The test also silently skips when tsc is absent, which is why it goes unnoticed locally. No action needed for this change.
  • bash tests/fm-turnend-guard.test.sh — all 53 cases ok under an official Node 22.22.1 build with TypeScript type stripping, including test_pi_extension_blocks_before_the_idle_signal, test_pi_extension_releases_the_latch_when_a_run_dies_before_continuing, and the reworked test_pi_extension_injects_once_per_logical_agent_run / test_pi_extension_retries_after_followup_delivery_failure
  • test_pi_extension_blocks_before_the_idle_signal run in isolation with ROOT pointed at the pre-change extension (git show a5fe1bc:.pi/extensions/fm-primary-turnend-guard.ts) → not ok - Pi guard must block the turn end before the idle signal fires, confirming the regression test genuinely fails on the agent_settled version (temporary driver deleted afterwards)
  • Real-harness ordering probe: pi -p &#34;hello&#34; --provider mockp --model mock-model -ne -na --no-session -nbt -e mock-provider.ts -e &lt;guard-variant&gt;.ts -e probe.ts against pi 0.81.1 with a local OpenAI-compatible mock provider, isolated HOME/FM_HOME, and a stub bin/fm-turnend-guard.sh returning exit 2 — run for both the shipped agent_end guard and the base a5fe1bc agent_settled guard
  • Captured the exact user message the mock provider received in each run, showing the FIRSTMATE_OP: v1 turn-end-guard recovery instruction reaching the model in the same run only with the agent_end guard
  • Strict no-emit typecheck of .pi/extensions/fm-primary-turnend-guard.ts + lib/fm-operational-input.ts against the installed @earendil-works/pi-coding-agent 0.81.1 (tsc 5.9.3, same tsconfig as tests/fm-pi-primary-types.test.sh) — clean for both the changed and base versions
  • Reproduced the documented environment limitation: system Node 22.22.1 (node_use_amaro: false) fails the .ts extension import with ERR_UNKNOWN_FILE_EXTENSION, matching the note added to docs/verification/supervision.md
🔧 **Document** - 1 issue found → auto-fixed ✅
  • ℹ️ docs/verification/supervision.md:126 - docs/verification/supervision.md states the four Node-driven Pi extension cases "pass on the CI runners". I could not verify that claim from this worktree: the local Node build has no TypeScript type stripping, so the cases abort locally, and I have no CI access. The surrounding paragraph is maintainer-verification material where every other claim carries a version, command, or observed output; this one carries none. Consider naming the CI Node version (or the workflow job) so the claim is checkable, or softening it to what was actually observed.

🔧 Fix: record Pi test Node requirement as observed, CI unverified
✅ Re-checked - no issues remain.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…settle

The Pi primary turn-end guard hooked agent_settled, which fires after the
agent loop has already exited. That emits the idle signal first, so anything
watching for idle sees a blind turn end, and the forced follow-up only
re-opens a turn that already ended.

Pi drains both message queues before emitting agent_end, so a follow-up
queued from an agent_end handler makes _handlePostAgentRun() return true and
the session calls agent.continue(). The same run keeps going and
agent_settled never fires until the guard stops queuing. Pi therefore blocks
genuinely, like the Claude and Codex Stop hooks, rather than passively.

Measured against pi 0.81.1 with a local mock provider: the shipped agent_end
guard emits one idle signal, the agent_settled version emits two. A probe
that re-queued from agent_end blocked 12 consecutive times with no ceiling,
so the single-follow-up limit is Firstmate's latch and not a Pi limit.

The re-entrancy latch is unchanged. Because agent_end also fires at
auto-retry and compaction boundaries, a run needing one of those while
supervision is already unhealthy spends its latched follow-up there instead
of at the final boundary; the recovery instruction still lands in the same
run, so this is an ordering nuance rather than a blind turn end.

Adds tests/fm-turnend-guard.test.sh:test_pi_extension_blocks_before_the_idle_signal,
which models Pi's loop and fails against the agent_settled version.
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