Skip to content

v3.9.0: plan resolution binds to a root, refusals are never silent

Choose a tag to compare

@OthmanAdi OthmanAdi released this 01 Aug 20:16
· 13 commits to master since this release

Closes #212, #211, #210.

A thread's plan is now bound to a root, not guessed from the cwd

A Codex thread whose cwd was a shared parent injected an unrelated project's plan on every hook fire (#212, reported by @webwww123). Resolution was cwd relative with no notion of a thread, so with /workspace holding one plan and /workspace/project holding the real one, the parent's pointer was the only pointer the hook could see. The wrong plan then arrived as high priority context on every prompt and every matched tool call, competing with the user's own corrections.

  • PWF_PLAN_ROOT binds resolution to a project root by absolute path. PLAN_ID is a cwd relative slug and structurally could not name a nested project's plan at all. A pin that does not resolve fails closed with a notice.
  • An ambiguous cwd refuses to inject. When the plan came from the shared pointer or the newest by mtime fallback and a project one directory below has its own live plan, nothing is injected and the notice names both escape hatches. An explicit pin, PLAN_ID, or an attached session stays authoritative.
  • Applied to the canonical dispatcher, the .codex/hooks.json route, the Cursor hooks, and the shared resolver in both shell and PowerShell.

Three bugs found while verifying that report

  • PLANNING_DISABLED=1 did nothing on eleven of thirteen install routes. Those SKILL.md variants still carried the v2.43 hook body inlined in their YAML scalars, so they never received the #195 opt-out, the symlink containment guard, nonce delimiters, the v3 attestation refusal, the ledger summary, or PWF_INJECT=smart, and they still wrote the SHA cache to the world writable /tmp path that moved to $XDG_CACHE_HOME in v3.0.0. All eleven now dispatch to the versioned script, and a test fails the build if one drifts back.
  • The Stop hook could never find its script on six hosts. Codex, Cursor, Factory, CodeBuddy, Mastra and OpenCode inherited a discovery list naming only Claude Code paths, so completion checking silently did nothing. Discovery is also a first match wins loop now: ls a b c | head -1 returns the alphabetically first hit, so a stale marketplace copy outranked the host native one.
  • Eight shipped PowerShell scripts could not be parsed by Windows PowerShell 5.1. A BOM-less .ps1 is read as ANSI, and the UTF-8 bytes for an em dash end in 0x94, which CP1252 maps to a closing curly quote, opening a string literal that never closes. Every dispatcher swallows the parse error. Dead on Windows: the Cursor injection hook, both .kiro asset scripts, and init-session for both Chinese variants, so those users could not create a plan at all.

Pi extension, provider errors and the status bar

agent_end never read its event (#211, reported by @killianMei), so a turn that ended with a provider error was treated as complete and got the auto continue follow up, which started another request into the same failing provider up to the limit, burying the original error. It now returns on a trailing assistant stopReason of error or aborted before the counter is touched, so an outage costs no retry budget.

The phase count is published from all four active handlers in every mode, including the all-phases-complete branch where the N/M to M/M transition reached the notification but never the bar. Bundled extension 1.2.3.

Prompt caching, answered with measurements

Injection is byte identical across fires in every context and mode, now asserted rather than assumed (#210, raised by @GlitterKill). Five routes were emitting unnormalized wall clock times because each builds its injection independently and none had received the v2.40 pass. The skill text also no longer attributes a whole-workflow token measurement to per tool call recitation.

Refusals are never silent

The session guard, the ambiguity refusal, a broken pin, and a missing script each say so once per turn and name the way out. plan-doctor reports a refusal as its own state instead of counting the notice as plan bytes and printing PASS.

Verification

Suite 311 to 411 passing, 282 to 453 subtests, 48 Pi extension tests, IDE folder sync clean. Built by Fable agents, reviewed by Opus, with a Sonnet recon fleet and a four agent adversarial pass that returned a do-not-ship verdict on the first build; four blockers, two introduced by the fix itself, were closed before release.

Full detail in CHANGELOG.md.