pi-plans 0.3.1 β†’ 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -119,10 +119,10 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
119
119
  | VCC compact | Active planning/execution compaction uses deterministic, no-LLM VCC-style summaries when Pi core emits manual `/compact`, threshold, or overflow events. Summaries use five bracket sections plus a brief transcript, keep a smart recent tail, support `keep:N`, and write VCC details/stats without adding `/pi-vcc` commands. |
120
120
  | Visible Refiner overlay | Delegated reviewer/criticizer subagents surface as a named public overlay in the TUI β€” one `Reviewer`/`Criticizer` panel with per-lane tool progress, full streaming transcript with follow-bottom scroll, Tab-pane focus, retention until the user presses `Esc` after completion, and clean cancelled/timed-out vs completed states. `reviewers: 3` renders three equal-height panes inside the same overlay |
121
121
  | Tracked execution | Checklist injected each turn; `[DONE:VC-xxx]` markers drive completion; implementation items report progress with `[I-xxx:implemented]` / `[I-xxx:validating]` markers; the bottom status bar shows lifecycle, `x/y` progress, elapsed time, and input/output token usage in real time |
122
- | Goal-wait continuation | While execution is active, a turn that ends with unpassed VCs gets an automatic light follow-up (`πŸ” goal-wait` in the status bar) β€” the worker keeps going, and rounds blocked on an external event poll via the taught `waiting for` backoff instead of stopping. Guard rails: 3 consecutive no-progress rounds, or 6 waiting rounds, pause the continuation (`⏸ goal-wait paused (reason)` in the status bar); any new input or `/plans-execute` re-kicks it, and `/plans-stop` always terminates |
122
+ | Goal-wait continuation | In TUI/RPC, only a fully settled agent with unpassed VCs and no pending input or compaction gets one hidden wake carrying the latest checklist. Tool turns never queue reminders or consume guard rounds. The status bar shows progress; 3 no-progress cycles or 6 waiting cycles pause continuation. User interruption and final model errors also pause it. Only genuine user input or `/plans-execute` resumes; extension messages cannot. Print/JSON single-shot sessions track progress without automatic wakes. `/plans-stop` terminates execution |
123
123
  | Execution handoff | The accepted plan resumes in the current session model; no separate model selection is performed. |
124
124
  | Execution-phase compaction | Pi core owns scheduling; pi-plans maps the active plan path, current `I-###`, implementation IDs, and remaining `VC-###` checklist into the VCC sections. The old current-I proactive trigger and model-generated summary path are removed. |
125
- | Planning-phase compaction | During `run.status=planning` with no active execution, pi-plans maps active run, artifact directory, latest plan path from session entries, and observed current-I markers into the VCC sections. Without an active planning run, compaction returns to Pi core. |
125
+ | Planning-phase compaction | During `run.status=planning` with no active execution, pi-plans maps active run, artifact directory, latest plan path from session entries, and observed current-I markers into the VCC sections. Without an active planning run, compaction returns to Pi core. Additionally, creating a new run (`plans start-run`) proactively requests one pre-plan VCC compaction and resumes planning with a hidden message (default on; `prePlanCompact:false` disables). |
126
126
  | Efficient executor prompt | Each turn, the executor is steered by a fused rule set β€” Marcos Hernanz's AGENTS.md principles Γ— Ponytail minimalism: layered growth, simplest implementation, long-term architecture (no stopgaps), library discipline β€” so plans finish in fewer tokens and fewer detours |
127
127
  | Write guard | `edit`/`write` blocked outside planning artifacts while a run is active |
128
128
 
@@ -130,14 +130,15 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
130
130
 
131
131
  | Tool / Command | Purpose |
132
132
  |---|---|
133
- | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-refs-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
133
+ | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-refs-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent`, `record-checkpoint` (state-machine-validated workflow transitions) |
134
134
  | `ask_choice` | Numbered choice prompt; `autoComplete: false` for the merged accept/execute question and external-state questions |
135
135
  | `refine` | Reviewer/criticizer round via standalone read-only subagents (`--mode json -p --no-session --tools read,grep,find,ls`, plus `code_graph` for both roles when the workspace has the code graph enabled); `target: "plan"` (default) reviews the plan, `target: "implementation"` reviews the implemented worktree against the plan; delegated TUI runs show one `Reviewer`/`Criticizer` overlay (78% width Γ— 78% height, top-center, β‰₯72 cols) with per-lane transcript, follow-bottom scroll, Tab focus, and retention until `Esc`; `reviewers: 3` renders three equal-height panes; enforces role/model confirmation gates |
136
136
  | `analyze_refs` | plan-with-refs reference analysis: one independent read-only subagent per downloaded reference (cwd = the ref directory), reusing the reviewer role gates and the concurrent overlay (titled `Refs`); batches of at most 3 lanes run sequentially; returns structured per-reference sections for `REF_ANALYSIS.md` |
137
137
  | `execute_plan` | Execution handoff: re-confirms with the user and enters extension-managed execution mode |
138
138
  | `/plans` | Show config, active run, and execution progress |
139
139
  | `/config-pi-plans` | Re-ask workspace defaults for language, artifact root, refs root, code graph, reviewer mode/model, and criticizer mode/model |
140
- | `/plans-execute [plan.md]` | Manual execution handoff (defaults to highest `PLAN_vN.md`) |
140
+ | `/resume-plans` | Resume the repository's working plan in the CURRENT session across restarts: unfinished planning (pending question + answered decisions), reviewing (round/lane state, successful outputs reused), execution (approval digest + HEAD + verified VCs), and implementation review (termination condition + round count). The unfinished active run wins; otherwise a unique candidate resumes directly and multiple candidates get a chooser. Linked worktrees share candidates; a cross-worktree resume confirms, copies artifacts without overwriting, and resets approval + VC validity (the termination condition survives, rounds restart at 0). An unchanged plan digest with a changed HEAD keeps the authorization but re-verifies old VCs first. Busy sessions and runs actively owned by a live process only notify β€” no queueing, no takeover. Interactive (TUI/RPC) only |
141
+ | `/plans-execute [plan.md]` | Resume a paused active execution without losing verified progress; otherwise enter the explicit execution handoff (defaults to highest `PLAN_vN.md`) |
141
142
  | `/update-plan [plan.md] [reason…]` | Interrupt-and-refine: stops execution (if any), returns the run to planning, and directs the agent to revise the plan into `PLAN_vN+1.md` while preserving verified work |
142
143
  | `/plans-autocomplete-stop` | Stop the current run's Auto-complete mode and return later planning questions to normal interaction |
143
144
  | `/plans-stop` | Stop execution mode |
@@ -153,7 +154,8 @@ Pi core remains the owner of compaction scheduling: manual `/compact`, threshold
153
154
  - **Tail policy.** The default keep is one recent user turn; smart keep may retain more turns when the tail is still small. Explicit `keep:N` is honored, while no-anchor and oversized-tail cases use a deterministic token-budget cut that avoids starting retained context with an orphan tool result.
154
155
  - **Manual matrix.** Plain `/compact` and `/compact keep:N` compact and show stats without continuing. `/compact <text>` and `/compact keep:N <text>` compact, then send the text once as the follow-up prompt. Internal pi-plans compaction markers are never reused as user follow-up prompts.
155
156
  - **Fallbacks and stats.** Unsafe manual/threshold cuts cancel with a warning; overflow or retrying unsafe cuts return control to Pi core. Successful VCC compactions notify with kept-tail and summarized-message stats. Threshold/overflow compactions may queue one hidden continuation only when the running Pi version still needs it and `continueAfterThresholdCompact` is enabled.
156
- - **Repo-private config.** Defaults are scaffolded in `.git/pi_plans/pi-vcc-config.json` under the resolved git common dir: `overrideDefaultCompaction:true`, `smartKeepTail:true`, `continueAfterThresholdCompact:true`, `debug:false`. Global pi-vcc config and `PI_VCC_CONFIG_PATH` are intentionally ignored.
157
+ - **Pre-plan compaction.** When `plans start-run` creates a new planning run, pi-plans proactively requests one VCC compaction (internal hint `pi-plans planning pre-plan compact`) right after the run is created and before the first planning question, then resumes the planning turn with a hidden message β€” so each new plan starts on a lean context (LLM reasoning degrades with longer input). Small sessions, already-compacted sessions, and failures skip silently and still resume. `prePlanCompact:false` in the repo-private config restores the old behavior.
158
+ - **Repo-private config.** Defaults are scaffolded in `.git/pi_plans/pi-vcc-config.json` under the resolved git common dir: `overrideDefaultCompaction:true`, `smartKeepTail:true`, `continueAfterThresholdCompact:true`, `prePlanCompact:true`, `debug:false`. Global pi-vcc config and `PI_VCC_CONFIG_PATH` are intentionally ignored.
157
159
 
158
160
  ## Visible Refiner overlay
159
161
 
package/index.ts CHANGED
@@ -20,9 +20,11 @@ import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import {
22
22
  consumePlanningCompactionResumeGuard,
23
+ consumePrePlanCompactPending,
23
24
  drainExecutionFlush,
24
25
  executionContextMessage,
25
26
  filterExecutionResumeMessages,
27
+ filterGoalWaitMessages,
26
28
  filterPlanningResumeMessages,
27
29
  getExecution,
28
30
  handleExecutionBeforeCompact,
@@ -35,12 +37,12 @@ import {
35
37
  noteCompactionEnded,
36
38
  noteCompactionStarted,
37
39
  PLANNING_PLAN_WRITTEN_CUSTOM_TYPE,
40
+ PLANNING_PREPLAN_COMPACT_HINT,
41
+ sendPrePlanCompactResume,
38
42
  registerExecutionTurnHandlers,
39
43
  refreshPlanningCompactionCooldown,
40
44
  requestPlanningCompaction,
41
45
  restoreFromSession,
42
- resetGoalWaitTurnFlags,
43
- resumeGoalWaitIfPaused,
44
46
  stopExecution,
45
47
  updateStatusWidget,
46
48
  shouldTriggerPlanningCompaction,
@@ -67,9 +69,12 @@ import {
67
69
  } from "./src/code-graph/commands.ts";
68
70
  import { latestPlanVersion, nextPlanVersionPath } from "./src/plan.ts";
69
71
  import { configPiPlansCommand } from "./src/config-command.ts";
72
+ import { resumePlansCommand } from "./src/resume-command.ts";
70
73
  import { getRun, loadConfig, readActive, recordDecision, resolveStateRootOrNull, setRunStatus } from "./src/state.ts";
74
+ import { boundRunId, resolveActiveRun, restoreRunBindingFromSession } from "./src/run-context.ts";
75
+ import { applyPlanWritten, mutateCheckpoint, planIdentityOf } from "./src/workflow-state.ts";
71
76
  import { registerAskChoiceTool } from "./tools/ask-choice.ts";
72
- import { executeHandoff, registerExecutePlanTool } from "./tools/execute-plan.ts";
77
+ import { executeCommand, registerExecutePlanTool } from "./tools/execute-plan.ts";
73
78
  import { registerPlansTool } from "./tools/plans.ts";
74
79
  import { registerRefineTool } from "./tools/refine.ts";
75
80
  import { registerAnalyzeRefsTool } from "./tools/analyze-refs.ts";
@@ -108,7 +113,7 @@ function extensionStalenessLine(): string {
108
113
 
109
114
  function hasActivePlanningWorkflow(ctx: Parameters<typeof updateStatusWidget>[0]): boolean {
110
115
  if (getExecution()) return true;
111
- const active = readActive(ctx.cwd);
116
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
112
117
  if (!active) return false;
113
118
  const status = getRun(ctx.cwd, active.run_id)?.status;
114
119
  return status === "planning" || status === "accepted" || status === "executing";
@@ -167,12 +172,17 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
167
172
  if (getExecution()) return;
168
173
  const rawPath = String((event.input as { path?: string }).path ?? "");
169
174
  if (!rawPath) return;
170
- const reason = planningWriteBlockReason({ workdir: ctx.cwd, toolName: event.toolName, rawPath });
175
+ const reason = planningWriteBlockReason({
176
+ workdir: ctx.cwd,
177
+ toolName: event.toolName,
178
+ rawPath,
179
+ activeRunId: boundRunId(ctx.sessionManager, ctx.cwd),
180
+ });
171
181
  if (reason) return { block: true, reason };
172
182
  // Allowed write: if it lands exactly on the run's latest plan file, drop a
173
183
  // marker entry so planning-phase compaction can anchor its cut point there.
174
184
  if (event.toolName === "write" || event.toolName === "edit") {
175
- const active = readActive(ctx.cwd);
185
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
176
186
  if (active) {
177
187
  const latest = latestPlanVersion(active.artifact_dir);
178
188
  if (latest && path.resolve(ctx.cwd, rawPath) === path.resolve(ctx.cwd, latest.path)) {
@@ -187,6 +197,63 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
187
197
  return;
188
198
  });
189
199
 
200
+ // I-003: a successful write/edit that lands on the run's latest plan
201
+ // records the plan identity in the run checkpoint (post-execution, so the
202
+ // digest covers the NEW file bytes β€” unlike the pre-execution marker above).
203
+ pi.on("tool_result", async (event, ctx) => {
204
+ if (event.isError) return;
205
+ if (getExecution()) return;
206
+ if (event.toolName !== "write" && event.toolName !== "edit") return;
207
+ const rawPath = String((event.input as { path?: string }).path ?? "");
208
+ if (!rawPath) return;
209
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
210
+ if (!active) return;
211
+ const latest = latestPlanVersion(active.artifact_dir);
212
+ if (!latest) return;
213
+ if (path.resolve(ctx.cwd, rawPath.replace(/^@/, "")) !== path.resolve(ctx.cwd, latest.path)) return;
214
+ try {
215
+ const identity = planIdentityOf(path.resolve(ctx.cwd, latest.path), latest.version);
216
+ mutateCheckpoint(ctx.cwd, active.run_id, (cp) => applyPlanWritten(cp, identity));
217
+ } catch {
218
+ /* best-effort; the model can also call plans record-checkpoint explicitly */
219
+ }
220
+ });
221
+
222
+ // Pre-plan compaction: right after `plans start-run` creates a new planning
223
+ // run, trigger one VCC compaction (PLANNING_PREPLAN_COMPACT_HINT routes it
224
+ // through the planning session_before_compact path) so the new plan starts
225
+ // on a lean context. ctx.compact() aborts the current agent operation
226
+ // first, so it must fire here β€” after the tool result is appended, never
227
+ // inside the tool execute stack. Pi's manual compaction never continues the
228
+ // aborted turn, so resume planning exactly once on success AND failure.
229
+ pi.on("tool_result", async (event, ctx) => {
230
+ if (event.isError) return;
231
+ if (event.toolName !== "plans") return;
232
+ if (!consumePrePlanCompactPending(ctx)) return;
233
+ if (typeof ctx.compact !== "function") {
234
+ sendPrePlanCompactResume(pi);
235
+ return;
236
+ }
237
+ let resumed = false;
238
+ const resumeOnce = () => {
239
+ if (resumed) return;
240
+ resumed = true;
241
+ sendPrePlanCompactResume(pi);
242
+ };
243
+ ctx.compact({
244
+ customInstructions: PLANNING_PREPLAN_COMPACT_HINT,
245
+ onComplete: () => resumeOnce(),
246
+ onError: () => {
247
+ try {
248
+ ctx.ui?.notify?.("pi-plans: pre-plan compaction skipped; continuing planning.", "info");
249
+ } catch {
250
+ /* notify is best-effort */
251
+ }
252
+ resumeOnce();
253
+ },
254
+ });
255
+ });
256
+
190
257
  // Bidirectional code-graph reminder hook: separate from the planning guard
191
258
  // above (which early-returns during execution). Fires only when the graph
192
259
  // is enabled and an execution is active. Reminders are best-effort notifies.
@@ -215,7 +282,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
215
282
 
216
283
  pi.on("context", (event) => {
217
284
  const filteredExecution = filterExecutionResumeMessages(event.messages as Array<{ customType?: string }>);
218
- const messages = filterPlanningResumeMessages(filteredExecution);
285
+ const messages = filterGoalWaitMessages(filterPlanningResumeMessages(filteredExecution));
219
286
  if (messages.length !== event.messages.length) {
220
287
  return { messages };
221
288
  }
@@ -238,21 +305,11 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
238
305
  noteCompactionEnded(ctx, event.customInstructions);
239
306
  });
240
307
 
241
- // Flush points for deferred execution-loop writes: primary drain when the
242
- // agent run fully settles, backstop drain at the next run's start (covers
243
- // continuation paths that might not emit agent_settled), plus the forced
244
- // synchronous flush inside stop/complete.
245
- pi.on("agent_settled", async (_event, ctx) => {
246
- drainExecutionFlush(pi, ctx);
247
- });
248
-
249
308
  // -----------------------------------------------------------------------
250
309
  // Execution loop: inject remaining checklist each turn, track markers.
251
310
  // -----------------------------------------------------------------------
252
311
  pi.on("before_agent_start", async (_event, ctx) => {
253
312
  drainExecutionFlush(pi, ctx);
254
- resetGoalWaitTurnFlags();
255
- resumeGoalWaitIfPaused(pi, ctx);
256
313
  const content = executionContextMessage(ctx);
257
314
  if (!content) {
258
315
  if (!getExecution() && shouldTriggerPlanningCompaction(ctx)) {
@@ -344,7 +401,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
344
401
  description: "Show pi-plans state: config, active run, and execution progress",
345
402
  handler: async (_args, ctx) => {
346
403
  const lines: string[] = [];
347
- const active = readActive(ctx.cwd);
404
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
348
405
  const run = active ? getRun(ctx.cwd, active.run_id) : null;
349
406
  if (!run) {
350
407
  lines.push("No active planning run.");
@@ -388,7 +445,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
388
445
  description: "Execute handoff: enter tracked execution mode for an accepted plan",
389
446
  handler: async (args, ctx) => {
390
447
  const planPath = args.trim() || undefined;
391
- const outcome = await executeHandoff(ctx, planPath);
448
+ const outcome = await executeCommand(ctx, planPath);
392
449
  ctx.ui.notify(outcome.message, outcome.status === "error" ? "error" : "info");
393
450
  },
394
451
  });
@@ -415,7 +472,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
415
472
  }
416
473
  }
417
474
 
418
- const active = readActive(ctx.cwd);
475
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
419
476
  const execution = getExecution();
420
477
  disableAutoComplete(ctx, "plan update");
421
478
 
@@ -512,10 +569,18 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
512
569
  },
513
570
  });
514
571
 
572
+ pi.registerCommand("resume-plans", {
573
+ description:
574
+ "Resume the working plan in this repository: unfinished planning / reviewing / execution / implementation review, across sessions and linked worktrees, in the current session.",
575
+ handler: async (_args, ctx) => {
576
+ await resumePlansCommand(pi, ctx, baseDir);
577
+ },
578
+ });
579
+
515
580
  pi.registerCommand("plans-abandon", {
516
581
  description: "Abandon the active planning run (lifts the read-only guard; artifacts are kept)",
517
582
  handler: async (_args, ctx) => {
518
- const active = readActive(ctx.cwd);
583
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
519
584
  if (!active) {
520
585
  ctx.ui.notify("No active planning run.", "info");
521
586
  return;
@@ -543,8 +608,21 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
543
608
  // -----------------------------------------------------------------------
544
609
  // Session lifecycle
545
610
  // -----------------------------------------------------------------------
611
+ pi.on("session_tree", async (_event, ctx) => {
612
+ await restoreFromSession(pi, ctx, ctx.sessionManager.getBranch() as unknown as Parameters<typeof restoreFromSession>[2]);
613
+ restoreRunBindingFromSession(
614
+ ctx.sessionManager,
615
+ ctx.cwd,
616
+ ctx.sessionManager.getBranch() as unknown as Parameters<typeof restoreRunBindingFromSession>[2],
617
+ );
618
+ });
546
619
  pi.on("session_start", async (_event, ctx) => {
547
- await restoreFromSession(pi, ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreFromSession>[2]);
620
+ await restoreFromSession(pi, ctx, ctx.sessionManager.getBranch() as unknown as Parameters<typeof restoreFromSession>[2]);
548
621
  restoreAutoCompleteFromSession(ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreAutoCompleteFromSession>[1]);
622
+ restoreRunBindingFromSession(
623
+ ctx.sessionManager,
624
+ ctx.cwd,
625
+ ctx.sessionManager.getBranch() as unknown as Parameters<typeof restoreRunBindingFromSession>[2],
626
+ );
549
627
  });
550
628
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plans",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Human-in-the-loop planning extension for the Pi coding agent: researched, refined Markdown plans before any code changes.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,7 +12,7 @@ This skill set is written for the Pi coding agent's documented behavior:
12
12
  - `refine` spawns read-only Pi subagents (`pi --mode json -p --no-session --tools read,grep,find,ls`, plus `code_graph` for both roles when workspace `graph_enabled` is true) with isolated context; delegated Reviewer/Criticizer runs show a standalone aggregate overlay titled `Reviewer` or `Criticizer` (78% Γ— 78% top-center, β‰₯72 cols, no input row), stream assistant/thinking/tool events into per-lane transcripts with follow-bottom scroll, dismiss on `Esc` (close-only β€” the refiner child keeps running and its result still flows back as tool output), replace any retained finished overlay when a new round begins, and return conclusions to the main session as tool output; `analyze_refs` spawns one read-only subagent per downloaded reference (cwd = that ref's directory) reusing the reviewer role gates, shows the same overlay titled `Refs` in batches of at most 3 lanes, and returns structured per-reference sections for `REF_ANALYSIS.md`;
13
13
  - when graph mode is enabled, graph-aware `read`/`edit` overrides are active for indexed source files: `read` returns a capped function digest (≀50 lines, synthetic anonymous entries folded) by default β€” drill in via `offset/limit` or `code_graph get-function`, and `full: true` is the only whole-file exit (small/zero-function files return full text; safety truncation matches native read); `write`/`edit` stage DB-first mutations until materialized via the `code_graph` tool's `apply` action (same planning/accepted gate as /apply-graph; refused for read-only refiner subagents via the PI_PLANS_REFINER marker; returns a per-file report with counts and a post-apply drift summary, and never changes run status); unexpected fallbacks (`not indexed` / `runtime unavailable` / `config read failed`) are marked at the top of the result while flag-off fallbacks stay unmarked;
14
14
  - the execution loop is extension-managed: remaining verifier items are injected each turn, implementation items emit `[I-###:current]`/`[I-###:implemented|validating]`, and `[DONE:VC-xxx]` markers are tracked with a bottom status bar;
15
- - execution and planning compaction keep Pi's SessionManager as the history owner; during active pi-plans runs, `session_before_compact` uses a deterministic no-LLM VCC-style summary with `[Session Goal]`, `[Files And Changes]`, `[Commits]`, `[Outstanding Context]`, `[User Preferences]`, and a ranked brief transcript; Pi core owns manual `/compact`, threshold, and overflow scheduling, while pi-plans handles smart tail keep, `keep:N`, stats, and phase-specific run/plan/current-I/checklist context;
15
+ - execution and planning compaction keep Pi's SessionManager as the history owner; during active pi-plans runs, `session_before_compact` uses a deterministic no-LLM VCC-style summary with `[Session Goal]`, `[Files And Changes]`, `[Commits]`, `[Outstanding Context]`, `[User Preferences]`, and a ranked brief transcript; Pi core owns manual `/compact`, threshold, and overflow scheduling, while pi-plans handles smart tail keep, `keep:N`, stats, and phase-specific run/plan/current-I/checklist context; in addition, creating a new planning run (`plans start-run`) proactively requests one pre-plan VCC compaction before the first planning question and resumes the planning turn with a hidden message (default on, `prePlanCompact` in `pi-vcc-config.json`);
16
16
 
17
17
  ## Planning Boundary
18
18
 
@@ -137,6 +137,27 @@ When the user picks `βœ“ Accept PLAN_vN and execute it now` in the merged questi
137
137
 
138
138
  If the user declines, stay in planning (or stop, per their choice). Never start implementation without the approved handoff.
139
139
 
140
+ ### Execution Goal-Wait
141
+
142
+ In TUI/RPC, automatic goal-wait is evaluated only at `agent_settled`, after
143
+ Pi has finished natural tool continuation, retries, and compaction. The
144
+ extension rechecks that the same execution is active and incomplete, the
145
+ session is idle, and neither pending input nor compaction owns continuation.
146
+ Each eligible settled cycle can send at most one hidden custom message with
147
+ the current execution rules and remaining VC checklist. Tool `turn_end`
148
+ events only update progress; they never prequeue goal-wait reminders.
149
+
150
+ No-progress and literal `waiting for` counters advance only on eligible
151
+ settled cycles. Real marker progress resets both; thresholds remain 3 and 6.
152
+ User interruption and final model errors also pause continuation. Genuine
153
+ interactive/RPC user input or `/plans-execute` can resume a paused active
154
+ execution without losing verified VCs; extension input cannot unpause it.
155
+ New-plan handoffs and the `execute_plan` tool still require explicit approval.
156
+ Completion, stop, and session replacement invalidate the extension's wake
157
+ identity without clearing user or other-extension queues. Print/JSON
158
+ single-shot sessions keep VC tracking and completion but never auto-wake;
159
+ use RPC for persistent headless execution.
160
+
140
161
  ### Post-Execution Continuation
141
162
 
142
163
  When execution completes in an interactive session, the completion message attaches a goal-running continuation block and triggers a new agent turn so the model can enter the implementation-review loop immediately. The interactive-only trigger keeps headless sessions silent (no unconsented subagent cost). The same behavior applies on both completion call sites (the normal `turn_end` completion and the `restoreFromSession` recovery path).
@@ -149,6 +170,12 @@ The agent then asks one `ask_choice` question for the termination condition of t
149
170
  - Round audit trail: `decisions.jsonl`, `subagents.jsonl`, and `pi-plans-ameliorate` entries (one at goal start, then one per round) carry `currentRound` for post-hoc verification.
150
171
  - Headless sessions skip the prompt entirely; no `pi-plans-ameliorate` entry is appended.
151
172
 
173
+ `refine` records each round and lane outcome durably (successful outputs are persisted to run-state files before the tool result returns) and accepts `resumeRoundId` to resume an interrupted round lane-by-lane: completed lanes are reused from their persisted outputs and never re-run; a round id is never reused across plan versions.
174
+
175
+ ## Resuming (`/resume-plans`)
176
+
177
+ After a restart or in a fresh session, `/resume-plans` (interactive only) restores the repository's working plan in the current session: the unfinished active run wins; otherwise a unique candidate resumes directly and multiple candidates get a chooser. It resumes unfinished planning (re-asks the pending question with the same `questionId`, never re-asks answered decisions), reviewing (resumes interrupted rounds via `refine resumeRoundId`, consolidates completed ones), execution (durable approval: unchanged plan digest keeps the authorization β€” a changed HEAD re-verifies old VCs first; legacy runs without checkpoints must re-approve), and the implementation-review loop (asks the termination condition only when it was never chosen). Linked worktrees share candidates; cross-worktree resumes confirm, copy artifacts without overwriting, reset approval and VC validity, and restart round counts. Record semantic boundaries with `plans record-checkpoint` (`plan-written`, `review-consolidated`, `implementation-review-configured`, `implementation-round-finished`, `completed` with evidence).
178
+
152
179
  `refine` accepts a `target` parameter (`"plan"` default, `"implementation"` for the post-execution loop). The implementation brief anchors findings to the plan's goals and acceptance criteria, explicitly assesses delivery maturity (MVP-only vs. long-term refinement: stopgaps, missing tests, technical debt, production readiness), and tags out-of-scope improvements as low severity.
153
180
 
154
181
  ## Red Flags
@@ -72,6 +72,10 @@ Summarize the user's request in one paragraph.
72
72
 
73
73
  State anything the executor should know, including order of work, files to avoid, and verification commands. The merged accept/execute question still requires explicit user approval (ask_choice with `autoComplete: false`, then the `execute_plan` tool) and must never be auto-completed. Once approved, the extension-managed execution loop injects the remaining checklist every turn and completes when every `[DONE:VC-xxx]` marker has landed β€” keep this section concise enough to serve as the executor's brief.
74
74
 
75
+ ## Termination Recording (implementation review)
76
+
77
+ When the post-execution implementation-review loop starts, the termination question is asked with `ask_choice` using `questionId: "termination-condition"` and persisted via `plans record-checkpoint` (`transition: "implementation-review-configured"`). Each disposed round records `implementation-round-finished`; the loop closes with `completed` plus evidence. These records make `/resume-plans` continue the loop with its original condition and round count.
78
+
75
79
  ## Revision Ledger
76
80
 
77
81
  - `PLAN_v1`: Initial plan from resolved questions and evidence.
@@ -92,6 +92,7 @@ Default values:
92
92
  "overrideDefaultCompaction": true,
93
93
  "smartKeepTail": true,
94
94
  "continueAfterThresholdCompact": true,
95
+ "prePlanCompact": true,
95
96
  "debug": false
96
97
  }
97
98
  ```
@@ -103,6 +104,7 @@ Rules:
103
104
  - `overrideDefaultCompaction:false` returns ordinary Pi manual/threshold/overflow compactions to Pi core. Explicit pi-plans internal compact hints can still use the VCC path.
104
105
  - `smartKeepTail:true` starts from the requested/default keep count and may retain more recent user turns when the retained tail remains within the safe token budget. Explicit `keep:N` is honored.
105
106
  - `continueAfterThresholdCompact:true` permits one hidden continuation after successful threshold/overflow compaction only on Pi versions that still need extension-driven resume behavior. Plain manual `/compact` never auto-continues, and `/compact <text>` sends the text once as the follow-up prompt.
107
+ - `prePlanCompact:true` requests one VCC planning compaction (internal hint `pi-plans planning pre-plan compact`) from the `plans` `tool_result` hook right after `plans start-run` creates a new run, before the first planning question, and resumes the planning turn with one hidden message on success and failure alike. Small sessions, already-compacted sessions, and aborts skip silently with an info notice. The trigger is disabled while an execution is active. `prePlanCompact:false` restores the old behavior.
106
108
  - `debug:false` writes no diagnostics; `debug:true` writes a best-effort `/tmp/pi-vcc-debug.json` snapshot for local troubleshooting.
107
109
 
108
110
  ## Language Setting
@@ -182,3 +184,20 @@ One run directory per planning request: `<git-common-dir>/pi_plans/runs/<YYYYMMD
182
184
  `run.json` includes: run ID; skill name; original request; target workspace; artifact directory; language tag; status (`planning` β†’ `accepted` β†’ `executing` β†’ `done`, with `stopped`/`abandoned` as exits); timestamps.
183
185
 
184
186
  `decisions.jsonl` is appended automatically by `ask_choice` (question, options, answer, answer source). `subagents.jsonl` records reviewer/criticizer/ref-analyst spawns. `refs.jsonl` records reference metadata via `plans` (`record-ref`).
187
+
188
+ ## Workflow Checkpoints (`/resume-plans`)
189
+
190
+ Each run may carry a `checkpoint.json` β€” the durable, cross-session workflow state that `/resume-plans` restores in the current session. It records: logical `phase` (`planning | reviewing | executing | implementation-review | completed`), `nextAction`, the exact plan identity (path + version + SHA-256), pending/answered questions (stable `questionId`), review rounds with per-lane status and result-file references, execution approval evidence (plan digest, worktree, `git rev-parse HEAD` at approval, verified VC/I set, usage), the implementation-review termination condition and completed-round count, and ownership metadata. Full review outputs live in separate `reviews/` files; the checkpoint keeps only validated references.
191
+
192
+ Rules:
193
+
194
+ - Validation is explicit: unknown schema versions, malformed shapes, and unexpected keys are rejected; missing and corrupt checkpoints are distinct, and corrupt files are never silently overwritten.
195
+ - Writes are atomic with monotonic revisions; writers may require ownership (token + generation) or an expected revision.
196
+ - Model-driven boundaries (plan written, review consolidated, termination condition recorded, implementation round finished, completed) go through the whitelisted `plans record-checkpoint` action, which enforces state-machine preconditions β€” it cannot set execution approval, mark VCs passed, or forge terminal states.
197
+ - `ask_choice` accepts `questionId`/`purpose`; a pending question is durable before the panel opens and the answer before it returns. When a crash leaves a question both answered (ledger) and pending (checkpoint), the answered entry wins.
198
+ - On execution resume, an unchanged plan digest with a changed HEAD keeps the authorization but re-verifies previously verified VCs first; loading execution from a checkpoint writes an immediate session snapshot so session restore cannot clear it.
199
+ - Cross-worktree resumes copy artifacts without overwriting, reset approval and VC validity, keep the termination condition, and restart completed-round counts at 0 for the target worktree.
200
+
201
+ ## Run Ownership
202
+
203
+ A run may be held by at most one live owner (`owner.json`: host, pid, process start time via `ps -o lstart=`, session id, random process token, generation). Acquisition is an atomic exclusive create; takeovers require proof the previous owner is dead (process gone, or pid alive with a different start time β€” PID reuse). Foreign hosts, corrupt records, and unverifiable liveness are conservatively refused; `/resume-plans` never queues or interrupts. Sessions bind to the run they start/execute/resume (restored from `pi-plans-run-start` entries on the current branch), and attribution (tools, write guard, autocomplete, execution bookkeeping, code-graph apply gate) prefers the binding over the shared `active.json` pointer.
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
4
  import { getRun, readActive } from "./state.ts";
5
+ import { resolveActiveRun } from "./run-context.ts";
5
6
 
6
7
  export const AUTOCOMPLETE_ENTRY = "pi-plans-autocomplete";
7
8
  const AUTOCOMPLETE_CONTINUE = "Continue the current planning workflow. Raise the next relevant question with ask_choice; do not stop after an auto-completed answer.";
@@ -32,7 +33,7 @@ function sessionState(ctx: ExtensionContext): AutoCompleteState | undefined {
32
33
  }
33
34
 
34
35
  function activePlanningRun(ctx: ExtensionContext): { runId: string } | null {
35
- const active = readActive(ctx.cwd);
36
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
36
37
  if (!active) return null;
37
38
  const run = getRun(ctx.cwd, active.run_id);
38
39
  return run?.status === "planning" ? { runId: run.run_id } : null;
@@ -19,7 +19,8 @@ import type { ParserBackend } from "./parser.ts";
19
19
  import type { Language } from "./types.ts";
20
20
  import { materialize, type MaterializeReport } from "./materialize.ts";
21
21
  import { generateSummaries, type CompletionHandle, type SummaryReport } from "./summary.ts";
22
- import { readActive, getRun, setRunStatus } from "../state.ts";
22
+ import { readActive, getRun, setRunStatus, type ActiveInfo } from "../state.ts";
23
+ import { boundRunId, resolveActiveRun } from "../run-context.ts";
23
24
 
24
25
  interface CommandContext {
25
26
  cwd: string;
@@ -84,7 +85,15 @@ async function bootstrap(ctx: CommandContext, opts: { reindex?: boolean }): Prom
84
85
  return { store, paths, parsers, runtimeStatus: status };
85
86
  }
86
87
 
87
- function activePlanningRun(workdir: string): ActiveInfo | null {
88
+ function activePlanningRun(workdir: string, sessionRunId?: string | null): ActiveInfo | null {
89
+ // The session-bound run gates first: a planning run in THIS session must
90
+ // refuse apply even when the shared pointer moved to another run (I-002).
91
+ if (sessionRunId) {
92
+ const boundRun = getRun(workdir, sessionRunId);
93
+ if (boundRun && (boundRun.status === "planning" || boundRun.status === "accepted")) {
94
+ return { run_id: sessionRunId, run_dir: "", artifact_dir: boundRun.artifact_dir };
95
+ }
96
+ }
88
97
  const active = readActive(workdir);
89
98
  if (!active) return null;
90
99
  const run = getRun(workdir, active.run_id);
@@ -189,8 +198,11 @@ export interface ApplyGraphCoreResult {
189
198
  * No notifications and no run-status side effects; callers own presentation
190
199
  * and lifecycle transitions.
191
200
  */
192
- export async function applyGraphCore(workdir: string, opts: { force?: boolean } = {}): Promise<ApplyGraphCoreResult> {
193
- const planned = activePlanningRun(workdir);
201
+ export async function applyGraphCore(
202
+ workdir: string,
203
+ opts: { force?: boolean; sessionRunId?: string | null } = {},
204
+ ): Promise<ApplyGraphCoreResult> {
205
+ const planned = activePlanningRun(workdir, opts.sessionRunId);
194
206
  if (planned) {
195
207
  return { refused: `code-graph apply refused: a planning run is currently planning or accepted (run ${planned.run_id}).` };
196
208
  }
@@ -224,7 +236,10 @@ export async function applyGraphCommand(args: string, ctx: CommandContext): Prom
224
236
  return;
225
237
  }
226
238
  const flags = parseCommandArgs(args).flags;
227
- const core = await applyGraphCore(ctx.cwd, { force: flags.has("force") });
239
+ const core = await applyGraphCore(ctx.cwd, {
240
+ force: flags.has("force"),
241
+ sessionRunId: boundRunId(ctx.sessionManager, ctx.cwd),
242
+ });
228
243
  if (core.refused) {
229
244
  ctx.ui.notify(core.refused, "error");
230
245
  return;
@@ -243,7 +258,7 @@ export async function applyGraphCommand(args: string, ctx: CommandContext): Prom
243
258
  `code-graph apply: ${ok} ok, ${deleted} deleted, ${stale} stale, ${skipped} skipped-missing, ${errors} error`,
244
259
  errors > 0 ? "error" : "info",
245
260
  );
246
- const active = readActive(ctx.cwd);
261
+ const active = resolveActiveRun(ctx.sessionManager, ctx.cwd);
247
262
  if (active) setRunStatus(ctx.cwd, active.run_id, "executing");
248
263
  }
249
264
 
package/src/compaction.ts CHANGED
@@ -14,6 +14,8 @@ export interface PiPlansVccSettings {
14
14
  overrideDefaultCompaction: boolean;
15
15
  smartKeepTail: boolean;
16
16
  continueAfterThresholdCompact: boolean;
17
+ /** Pre-plan compaction: trigger a VCC compaction right after `plans start-run`. */
18
+ prePlanCompact: boolean;
17
19
  debug: boolean;
18
20
  }
19
21
 
@@ -21,6 +23,7 @@ export const DEFAULT_VCC_SETTINGS: PiPlansVccSettings = {
21
23
  overrideDefaultCompaction: true,
22
24
  smartKeepTail: true,
23
25
  continueAfterThresholdCompact: true,
26
+ prePlanCompact: true,
24
27
  debug: false,
25
28
  };
26
29
 
@@ -34,9 +37,15 @@ export const MAX_CHARS_PER_TOKEN = 6;
34
37
  export const PI_SELF_RESUME_VERSION: readonly [number, number, number] = [0, 84, 4];
35
38
  export const PI_VCC_COMPACT_INSTRUCTION = "__pi_vcc__";
36
39
 
40
+ /** Internal hint for the pre-plan compaction requested right after start-run
41
+ * (tools/plans.ts marks pending; index.ts requests the compact action).
42
+ * Single source of truth: also referenced by exec.ts and the tests. */
43
+ export const PLANNING_PREPLAN_COMPACT_HINT = "pi-plans planning pre-plan compact";
44
+
37
45
  const INTERNAL_COMPACT_INSTRUCTIONS = new Set([
38
46
  "pi-plans execution auto compact",
39
47
  "pi-plans planning auto compact",
48
+ PLANNING_PREPLAN_COMPACT_HINT,
40
49
  ]);
41
50
 
42
51
  export function vccSettingsPath(stateRoot: string): string {
@@ -95,6 +104,7 @@ export function loadVccSettings(stateRoot: string): PiPlansVccSettings {
95
104
  overrideDefaultCompaction: typeof parsed.overrideDefaultCompaction === "boolean" ? parsed.overrideDefaultCompaction : DEFAULT_VCC_SETTINGS.overrideDefaultCompaction,
96
105
  smartKeepTail: typeof parsed.smartKeepTail === "boolean" ? parsed.smartKeepTail : DEFAULT_VCC_SETTINGS.smartKeepTail,
97
106
  continueAfterThresholdCompact: typeof parsed.continueAfterThresholdCompact === "boolean" ? parsed.continueAfterThresholdCompact : DEFAULT_VCC_SETTINGS.continueAfterThresholdCompact,
107
+ prePlanCompact: typeof parsed.prePlanCompact === "boolean" ? parsed.prePlanCompact : DEFAULT_VCC_SETTINGS.prePlanCompact,
98
108
  debug: typeof parsed.debug === "boolean" ? parsed.debug : DEFAULT_VCC_SETTINGS.debug,
99
109
  };
100
110
  }
@@ -349,6 +349,8 @@ export async function configPiPlansCommand(_args: string, ctx: ConfigCommandCont
349
349
  return config;
350
350
  });
351
351
 
352
+ // Display-only consumer (F-006): deliberately keeps the shared active
353
+ // pointer β€” the wizard summarizes repo state, not session attribution.
352
354
  const active = readActive(workdir);
353
355
  const lines = summarizeConfig(updated.config);
354
356
  if (active) {