ef32a32464
Plan resolution was cwd relative with no notion of a thread: PLAN_ID, then .planning/.active_plan, then the newest plan directory by mtime, every read taken relative to the process cwd. A Codex thread sitting at a shared parent such as /workspace therefore saw only the parent's pointer, and the plan of an unrelated project was injected on every prompt and every matched tool call while the nested project's own .active_plan was never consulted (#212). PWF_PLAN_ROOT binds resolution to a project root by absolute path, which a cwd relative PLAN_ID slug structurally cannot express. A pin that does not resolve fails closed with a notice rather than falling back to the ambiguous plan the caller was escaping. Containment is checked against the pin. When the plan was chosen by a cwd guess (the shared pointer, newest by mtime, or a legacy root task_plan.md) and a project one directory below carries its own live plan, nothing is injected and the notice names both escape hatches. An explicit PLAN_ID, a pin, or an attached session stays authoritative and skips the check. Detection is one glob at depth one, which is the whole per fire budget. A nested pointer that is empty or names a deleted directory does not compete, so an abandoned subproject cannot kill injection at the root. The session attachment guard from #146 now runs on this route too. It is not silent: on hosts that never set PWF_SESSION_ID every session is unattached by construction, so a stale .planning/sessions/ directory would otherwise kill injection permanently with no symptom, and .planning/ is gitignored so the state is invisible to review. The notice is turn scoped so it cannot become per tool call spam. Also fixed here, both found by the new tests: - ledger-summary.sh now takes the resolved plan directory as an argument. Under a pin it previously re-resolved from the process cwd, so an autonomous loop could read the parent's "1/1 complete" while its own pinned plan had an open phase. With no plan directory determinable it says so instead of reporting a confident 0/0. - GNU sha256sum prefixes its output line with a backslash when the filename needs escaping, which any Windows style pin triggers, so every attested pinned plan reported PLAN TAMPERED on every fire. plan-doctor reports a refusal as its own state with the remedy. It previously counted the refusal notice as bytes of plan context and printed PASS, so the one tool a dark user is pointed at gave a green light. The legacy invariant holds: with no pin, no sessions directory and no nested competing plan, output is byte identical to the previous release across every context and mode, verified by diffing captured stdout against HEAD.