pi-plans 0.1.2 → 0.2.0

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
@@ -7,7 +7,7 @@
7
7
  <h2 align="center"><b>Plan. Review. Execute.</b></h2>
8
8
 
9
9
  <p align="center">
10
- <i>Versioned, reviewed Markdown plans land before any code changes.<br>Human-in-the-loop planning for the Pi coding agent.</i>
10
+ <i>Versioned, reviewed Markdown plans land before any code changes.<br>Human-in-the-loop planning for the <a href="https://github.com/earendil-works/pi">Pi coding agent</a>.</i>
11
11
  </p>
12
12
 
13
13
  <p align="center">
@@ -16,6 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/pi-plans"><img alt="npm downloads" src="https://img.shields.io/npm/dt/pi-plans?color=38bdf8" /></a>
17
17
  <a href="https://www.npmjs.com/package/pi-plans"><img alt="npm version" src="https://img.shields.io/npm/v/pi-plans?color=60a5fa" /></a>
18
18
  <a href="https://github.com/earendil-works/pi"><img alt="Pi package" src="https://img.shields.io/badge/Pi-package-fbbf24" /></a>
19
+ <a href="https://github.com/earendil-works/pi"><img alt="Pi coding agent" src="https://img.shields.io/badge/Pi%20coding%20agent-earendil--works-22d3ee" /></a>
19
20
  <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/pi-plans?color=22c55e" /></a>
20
21
  <a href="https://github.com/MaxInGaussian/pi-plans/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/MaxInGaussian/pi-plans/actions/workflows/ci.yml/badge.svg" /></a>
21
22
  </p>
@@ -62,7 +63,7 @@ A rough change request becomes a versioned Markdown plan instead of a surprise d
62
63
  tracked execution loop
63
64
  fused AGENTS.md × Ponytail executor rules
64
65
  checklist injected each turn, [DONE:VC-xxx]
65
- markers tracked via bottom status bar + detail panel
66
+ markers tracked via bottom status bar
66
67
  |
67
68
  v
68
69
  run status: done
@@ -112,13 +113,15 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
112
113
  | Capability | In short |
113
114
  |---|---|
114
115
  | Planning router + five specialist skills | Start with `/skill:planning` to route to the narrowest matching specialist (`plan-small` → `plan-big`, `debug-and-plan`, `plan-with-refs`) |
115
- | Choice prompts | `ask_choice`: recommended option first, answers auto-recorded per run |
116
- | Refinement rounds | Read-only reviewer/criticizer Pi subagents consolidate findings into the next plan version |
116
+ | Choice prompts | `ask_choice`: recommended option first, answers auto-recorded per run; choosing Auto-complete enables recommendation-only answers for later eligible questions in the current planning run, with `/plans-autocomplete-stop` available to take back control |
117
+ | Refinement rounds | Read-only reviewer/criticizer Pi subagents consolidate findings into the next plan version; delegated runs have standalone `Reviewer`/`Criticizer` progress overlays that close before the tool result returns |
117
118
  | Workspace state | Config, runs, decisions, refs, and subagent ledgers in `.git/pi_plans/` (git common dir) |
118
- | Tracked execution | Checklist injected each turn; `[DONE:VC-xxx]` markers drive completion; implementation items report progress with `[I-xxx:implemented]` / `[I-xxx:validating]` markers; `plans-list` groups work by I-item with `[Pending/Implementing/Implemented/Validating/VC passed]` states (passed items struck through); the collapsed footer and expanded panel share the same progress projection, so `x/y` updates in real time in both views |
119
- | Execution model | With `plans set-execution-model <provider/model[:thinking]>`, the main session switches to it at handoff and restores the planning model + thinking level on stop/complete/abandon; when unset, the first execution handoff prompts for one and recommends inheriting the current session model |
120
- | Execution-phase compaction | Pi core owns threshold, overflow, and manual compaction scheduling; pi-plans only supplies a plan-aware `session_before_compact` summary and queues one hidden continuation when Pi reports `willRetry: false`, while preserving the raw execution tail and `previousSummary` chaining |
121
- | Planning-phase auto compaction | In active planning runs (run.status=planning, no execution), context usage >=100% triggers a plan-aware compaction too: pre-plan history compresses, Q&A during planning stays in a dedicated section, the latest `PLAN_vN.md` and later conversation stay raw; cooldown + resume guard prevent ping-pong, and manual `/compact` follows the same rules |
119
+ | Smart compact (I-aware) | History is sliced by `I-###` instead of VC. The current-I slice above 20% of the model window triggers a bounded summary with paired `Read:` records, retains a legal recent suffix, targets <10% post-context, and records a hard-floor reason when unreachable. Planning phase falls back to the latest plan/Q&A focus when no current marker exists; cooldown + resume guard prevent ping-pong; one hidden continuation is queued when Pi reports `willRetry: false` |
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, bounded output preview, and clean cancelled/timed-out vs completed states. The overlay opens when the round starts and closes before the tool result returns to the main session |
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
+ | Execution handoff | The accepted plan resumes in the current session model; no separate model selection is performed. |
123
+ | Execution-phase compaction | Pi core owns threshold, overflow, and manual scheduling; pi-plans adds a current-I proactive check once the current-I slice exceeds 20% of the model window, summarizes I-level history and bounded `Read:` records, retains a legal recent suffix, targets under 10% when possible, and records hard-floor reasons when not; one hidden continuation is queued when Pi reports `willRetry: false` |
124
+ | Planning-phase auto compaction | In active planning runs (run.status=planning, no execution), the current-I check uses the same 20%/10% best-effort policy when a marker exists; without a marker it protects the latest plan/Q&A focus, while Pi threshold, overflow, and manual compaction remain supported; cooldown + resume guard prevent ping-pong and hidden resume messages stay out of model context |
122
125
  | 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 |
123
126
  | Write guard | `edit`/`write` blocked outside planning artifacts while a run is active |
124
127
 
@@ -126,17 +129,37 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
126
129
 
127
130
  | Tool / Command | Purpose |
128
131
  |---|---|
129
- | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-execution-model`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
132
+ | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
130
133
  | `ask_choice` | Numbered choice prompt; `autoComplete: false` for the merged accept/execute question and external-state questions |
131
- | `refine` | Reviewer/criticizer round via read-only subagents (`--tools read,grep,find,ls`); `reviewers: 3` for big plans; enforces role/model confirmation gates |
132
- | `execute_plan` | Execution handoff: re-confirms with the user, prompts for an execution model if needed, and enters extension-managed execution mode |
134
+ | `refine` | Reviewer/criticizer round via standalone read-only subagents (`--mode json -p --no-session --tools read,grep,find,ls`); delegated TUI runs show one `Reviewer`/`Criticizer` overlay and close it before returning; `reviewers: 3` for big plans; enforces role/model confirmation gates |
135
+ | `execute_plan` | Execution handoff: re-confirms with the user and enters extension-managed execution mode |
133
136
  | `/plans` | Show config, active run, and execution progress |
134
- | `/plans-list` | Toggle the execution checklist panel |
135
- | `/plans-execute [plan.md]` | Manual execution handoff (defaults to highest `PLAN_vN.md`; prompts for an execution model if unset) |
137
+ | `/plans-execute [plan.md]` | Manual execution handoff (defaults to highest `PLAN_vN.md`) |
136
138
  | `/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 |
139
+ | `/plans-autocomplete-stop` | Stop the current run's Auto-complete mode and return later planning questions to normal interaction |
137
140
  | `/plans-stop` | Stop execution mode |
138
141
  | `/plans-abandon` | Abandon the active run (lifts the write guard; artifacts stay) |
139
- | Status bar (lifecycle) | 💬 Q&A → 📝 draft written (planning sub-phases) → ⌛ executing `x/y · spent · in/out-toks` in the collapsed footer or expanded panel → ⛔ stopped / 🎯 done / 🚫 abandoned |
142
+ | Status bar (lifecycle) | 💬 Q&A → 📝 draft written (planning sub-phases) → ⌛ executing `x/y · spent · in/out-toks` in the bottom status bar → ⛔ stopped / 🎯 done / 🚫 abandoned |
143
+
144
+ ## Smart compact (I-aware)
145
+
146
+ Default `compaction` is Pi-core-owned: threshold, overflow, and manual `/compact` always run as designed. On top of that, pi-plans layers an **I-aware policy** so long, tool-heavy sessions survive the same run instead of running out of context:
147
+
148
+ - **Slice by implementation item.** History is grouped by `[I-###:current]` markers instead of `[DONE:VC-xxx]`. The current I's prefix can be summarized, the current I's recent suffix stays raw, and finished `I-###` items become independent sections.
149
+ - **Bounded read history.** Every paired `read` call/result is reduced to `Read: <path> line <X-Y> Extracted information summary: ...` (line range is `unknown` when no offset/limit is given). Records deduplicate by path/range across compactions and never embed full raw tool output.
150
+ - **20%/10% best-effort budget.** When the current-I slice exceeds 20% of `ctx.getContextUsage().contextWindow`, a compact is requested on the next settled turn. The summary's `details` record `contextWindow`, `tokensBefore`, `currentITokens`, `summaryTokens`, `keptSuffixTokens`, `estimatedAfterTokens`, `targetRatio`, `currentI`, `firstKeptEntryId`, `targetMet`, and a `hardFloorReason` when the 10% target cannot be reached (system prompt, tool definitions, single oversized tool result). Hard floors stop the loop; they do not silently fall through.
151
+ - **Pi-owned scheduling preserved.** Threshold, overflow, and manual triggers still come from Pi core. pi-plans only customizes the summary and re-arms once usage falls below the low watermark. The hidden `Continue execution.` resume message is queued on non-retry compactions and never enters model context.
152
+ - **Bounded model call.** Custom summaries reuse the current Pi model via `ctx.modelRegistry.complete(model, context, options)` with `event.signal`, `cacheRetention: "none"`, a fresh `sessionId`, and bounded `maxTokens`. Empty, length-stopped, error, or tool-call responses fall back to Pi's default compaction — no half-checkpoint is ever written.
153
+ - **Phase isolation.** Planning and execution keep independent compaction state (`pi-plans-plan-resume` vs `pi-plans-exec-resume`); planning without a current marker protects the latest plan/Q&A focus rather than leaking execution state.
154
+
155
+ ## Visible Refiner overlay
156
+
157
+ Delegated `refine` rounds (reviewer or criticizer) show their progress directly inside the Pi TUI instead of disappearing into the child process's terminal. The overlay is a public, named panel so users always know who is doing what:
158
+
159
+ - **Named public overlays.** Each round uses the literal overlay name `Reviewer` or `Criticizer` (no dependency on `pi-btw`; the renderer is built on Pi's public `pi-tui` primitives). The big-plan concurrent reviewer round renders one reviewer lane per subagent under the same `Reviewer` overlay.
160
+ - **Bounded live detail.** The overlay tracks lane state (`pending → running → completed | cancelled | timed-out`) and the most recent tool call plus a clipped argument preview. Raw tool output is never surfaced, so progress stays legible even when subagents read large files.
161
+ - **Clean lifecycle edges.** The overlay opens at round start, advances via the JSONL progress feed emitted by `pi --mode json`, and is `close()`d before the round's conclusion returns as a tool result to the main session. Cancelled and timed-out children render as terminal states with the original error message — never as silent drops.
162
+ - **Tool-only progress.** The overlay only consumes tool and message lifecycle events from the child; unrelated `pi` events are ignored, so a noisy upstream release does not desync the panel.
140
163
 
141
164
  ## The execution rules
142
165
 
@@ -154,6 +177,9 @@ Once you approve the handoff, every turn injects a compact rule set that fuses M
154
177
 
155
178
  The rules cost four lines per turn and buy back far more: fewer wrong turns, shorter implementation paths, plans that finish in fewer tokens.
156
179
 
180
+ Waiting for subprocess-backed verification:
181
+ For subprocess-backed verification, when a step starts a subprocess and needs its result before verifying, use literal `waiting for` with backoff `5s -> 10s -> 20s -> 40s -> 80s`, then keep polling at 80s; restart at 5s for each new subprocess.
182
+
157
183
  ## Skills
158
184
 
159
185
  Invoked via `resources_discover`, callable as `/skill:<name>`, directly as `/<name>` (e.g. `/planning`, `/plan-small` — extension aliases that forward to the skill), or picked automatically from the task description.
@@ -194,12 +220,12 @@ or register the absolute path in `~/.pi/agent/settings.json`:
194
220
  pi-plans/
195
221
  ├── index.ts # Extension entry: tools, commands, guard, execution loop
196
222
  ├── tools/ # plans, ask-choice, refine, execute-plan
197
- ├── src/ # state, guard, plan parsing, subagent runner, exec loop
223
+ ├── src/ # state, guard, plan parsing, subagent runner, refine overlay, exec loop
198
224
  ├── skills/ # The planning router plus five specialist planning skills
199
225
  ├── references/ # Shared workflow, state/config, plan template (normative)
200
226
  ├── agents/ # reviewer.md / criticizer.md subagent prompts
201
227
  ├── scripts/validate.ts # Structure validator
202
- └── tests/ # node:test suite (state, guard, plan parsing, execution)
228
+ └── tests/ # node:test suite (state, guard, plan parsing, execution, refine progress)
203
229
  ```
204
230
 
205
231
  ## Safety model
@@ -223,7 +249,7 @@ The plan is the contract. Refinement converges on scope while nothing is writabl
223
249
 
224
250
  **What can Auto-complete decide on my behalf?**
225
251
 
226
- Planning and refinement choices only (the recommended option). It is never offered for execution approval, installs, publishing, deployment, merge, push, or credentials — those questions stop and wait for you.
252
+ Planning and refinement choices only (the recommended option). Choosing Auto-complete enables the recommended answer for later eligible planning questions in the current run and the extension continues the planning turn when the model stops early. Use `/plans-autocomplete-stop` to take back control. It is never offered for execution approval, installs, publishing, deployment, merge, push, or credentials — those questions stop and wait for you.
227
253
 
228
254
  **Where does all the state live?**
229
255
 
package/index.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * - `refine` tool — reviewer/criticizer rounds via read-only pi subagents
11
11
  * - `execute_plan` — execution handoff into the tracked execution loop
12
12
  * - write guard — planning runs may only write planning artifacts
13
- * - execution loop — checklist injection, [DONE:VC-xxx] tracking, progress widget
13
+ * - execution loop — checklist injection, [DONE:VC-xxx] tracking, progress status
14
14
  */
15
15
 
16
16
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -19,7 +19,6 @@ import * as path from "node:path";
19
19
  import { dirname, join } from "node:path";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import {
22
- consumePendingPanelSync,
23
22
  consumePlanningCompactionResumeGuard,
24
23
  drainExecutionFlush,
25
24
  executionContextMessage,
@@ -29,23 +28,28 @@ import {
29
28
  handleExecutionBeforeCompact,
30
29
  handleExecutionCompact,
31
30
  handleExecutionCompactFailed,
31
+ handleExecutionTurnCompaction,
32
32
  handlePlanningBeforeCompact,
33
33
  handlePlanningCompact,
34
34
  handlePlanningCompactFailed,
35
35
  PLANNING_PLAN_WRITTEN_CUSTOM_TYPE,
36
36
  registerExecutionTurnHandlers,
37
- recordTouchedPaths,
38
37
  refreshPlanningCompactionCooldown,
39
38
  requestPlanningCompaction,
40
39
  restoreFromSession,
41
- ensureExecutionModelActive,
42
40
  stopExecution,
43
- syncExecutionPanel,
44
- toggleExecutionPanelView,
45
41
  updateStatusWidget,
46
42
  shouldTriggerPlanningCompaction,
47
43
  } from "./src/exec.ts";
44
+ import {
45
+ autoCompleteStatus,
46
+ disableAutoComplete,
47
+ markPlanWritten,
48
+ registerAutoCompleteTurnHandlers,
49
+ restoreAutoCompleteFromSession,
50
+ } from "./src/autocomplete.ts";
48
51
  import { planningWriteBlockReason } from "./src/guard.ts";
52
+ import { registerQueryInterviewHooks } from "./src/query-hook.ts";
49
53
  import { latestPlanVersion, nextPlanVersionPath } from "./src/plan.ts";
50
54
  import { getRun, readActive, recordDecision, resolveStateRootOrNull, setRunStatus } from "./src/state.ts";
51
55
  import { registerAskChoiceTool } from "./tools/ask-choice.ts";
@@ -80,20 +84,12 @@ function extensionStalenessLine(): string {
80
84
  }
81
85
  }
82
86
 
83
- function extractPathsFromBash(command: string): string[] {
84
- const values = new Set<string>();
85
- for (const token of command.split(/\s+/)) {
86
- const cleaned = token.replace(/^["'`(<[{]+|["'`)>}\],;]+$/g, "");
87
- if (!cleaned || cleaned === "." || cleaned === ".." || cleaned.startsWith("-") || cleaned.includes("=") ) continue;
88
- const looksLikePath =
89
- cleaned.includes("/") ||
90
- cleaned.startsWith(".") ||
91
- cleaned.startsWith("~") ||
92
- /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/.test(cleaned);
93
- if (!looksLikePath) continue;
94
- values.add(cleaned);
95
- }
96
- return [...values];
87
+ function hasActivePlanningWorkflow(ctx: Parameters<typeof updateStatusWidget>[0]): boolean {
88
+ if (getExecution()) return true;
89
+ const active = readActive(ctx.cwd);
90
+ if (!active) return false;
91
+ const status = getRun(ctx.cwd, active.run_id)?.status;
92
+ return status === "planning" || status === "accepted" || status === "executing";
97
93
  }
98
94
 
99
95
  export default function piPlansExtension(pi: ExtensionAPI): void {
@@ -101,6 +97,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
101
97
  registerAskChoiceTool(pi);
102
98
  registerRefineTool(pi, baseDir);
103
99
  registerExecutePlanTool(pi);
100
+ registerQueryInterviewHooks(pi, hasActivePlanningWorkflow);
104
101
 
105
102
  // Contribute the router skill plus the five specialist planning skills.
106
103
  pi.on("resources_discover", () => ({
@@ -142,25 +139,14 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
142
139
  // not been approved), edit/write may only target planning artifacts.
143
140
  // -----------------------------------------------------------------------
144
141
  pi.on("tool_call", async (event, ctx) => {
145
- const execution = getExecution();
146
- if (execution) {
147
- if (event.toolName === "edit" || event.toolName === "write") {
148
- const rawPath = String((event.input as { path?: string }).path ?? "");
149
- if (rawPath) recordTouchedPaths(ctx.cwd, [rawPath]);
150
- }
151
- if (event.toolName === "bash") {
152
- const command = String((event.input as { command?: string }).command ?? "");
153
- if (command) recordTouchedPaths(ctx.cwd, extractPathsFromBash(command));
154
- }
155
- return;
156
- }
142
+ if (getExecution()) return;
157
143
  const rawPath = String((event.input as { path?: string }).path ?? "");
158
144
  if (!rawPath) return;
159
145
  const reason = planningWriteBlockReason({ workdir: ctx.cwd, toolName: event.toolName, rawPath });
160
146
  if (reason) return { block: true, reason };
161
147
  // Allowed write: if it lands exactly on the run's latest plan file, drop a
162
148
  // marker entry so planning-phase compaction can anchor its cut point there.
163
- if (event.toolName === "write") {
149
+ if (event.toolName === "write" || event.toolName === "edit") {
164
150
  const active = readActive(ctx.cwd);
165
151
  if (active) {
166
152
  const latest = latestPlanVersion(active.artifact_dir);
@@ -169,6 +155,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
169
155
  runId: active.run_id,
170
156
  planPath: latest.path,
171
157
  });
158
+ markPlanWritten(ctx);
172
159
  }
173
160
  }
174
161
  }
@@ -210,7 +197,6 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
210
197
  // -----------------------------------------------------------------------
211
198
  pi.on("before_agent_start", async (_event, ctx) => {
212
199
  drainExecutionFlush(pi, ctx);
213
- await ensureExecutionModelActive(pi, ctx);
214
200
  const content = executionContextMessage();
215
201
  if (!content) {
216
202
  if (!getExecution() && shouldTriggerPlanningCompaction(ctx)) {
@@ -228,10 +214,11 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
228
214
  });
229
215
 
230
216
  registerExecutionTurnHandlers(pi, async (ctx) => {
231
- if (!getExecution()) {
217
+ if (getExecution()) {
218
+ handleExecutionTurnCompaction(ctx);
219
+ } else {
232
220
  refreshPlanningCompactionCooldown(ctx);
233
221
  if (consumePlanningCompactionResumeGuard(ctx)) {
234
- if (consumePendingPanelSync()) syncExecutionPanel(ctx);
235
222
  updateStatusWidget(ctx);
236
223
  return;
237
224
  }
@@ -239,26 +226,14 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
239
226
  requestPlanningCompaction(ctx);
240
227
  }
241
228
  }
242
- // A busy-toggle during the previous turn deferred its re-render; the
243
- // turn just ended, so this is the safe point to apply it.
244
- if (consumePendingPanelSync()) syncExecutionPanel(ctx);
229
+ // A completed turn is the safe point for status updates.
245
230
  updateStatusWidget(ctx);
246
231
  });
232
+ registerAutoCompleteTurnHandlers(pi);
247
233
 
248
234
  // -----------------------------------------------------------------------
249
- // Commands and shortcuts
235
+ // Commands
250
236
  // -----------------------------------------------------------------------
251
- pi.registerShortcut("alt+o", {
252
- description: "Toggle pi-plans execution checklist widget",
253
- handler: async (ctx) => {
254
- const expanded = toggleExecutionPanelView(pi, ctx);
255
- if (expanded === null) {
256
- ctx.ui.notify("No execution in progress.", "info");
257
- return;
258
- }
259
- ctx.ui.notify(expanded ? "Execution checklist expanded." : "Execution checklist collapsed.", "info");
260
- },
261
- });
262
237
 
263
238
  pi.registerCommand("plans", {
264
239
  description: "Show pi-plans state: config, active run, and execution progress",
@@ -283,20 +258,17 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
283
258
  lines.push(` ${item.done ? "☑" : "☐"} ${item.id}`);
284
259
  }
285
260
  }
261
+ lines.push(`Auto-complete: ${autoCompleteStatus(ctx)}`);
286
262
  lines.push(extensionStalenessLine());
287
263
  ctx.ui.notify(lines.join("\n"), "info");
288
264
  },
289
265
  });
290
266
 
291
- pi.registerCommand("plans-list", {
292
- description: "Toggle the pi-plans execution checklist widget",
267
+ pi.registerCommand("plans-autocomplete-stop", {
268
+ description: "Stop Auto-complete for the active planning run",
293
269
  handler: async (_args, ctx) => {
294
- const expanded = toggleExecutionPanelView(pi, ctx);
295
- if (expanded === null) {
296
- ctx.ui.notify("No execution in progress.", "info");
297
- return;
298
- }
299
- ctx.ui.notify(expanded ? "Execution checklist expanded." : "Execution checklist collapsed.", "info");
270
+ const stopped = disableAutoComplete(ctx, "stopped by user");
271
+ ctx.ui.notify(stopped ? "Auto-complete stopped." : "Auto-complete is not active.", stopped ? "info" : "warning");
300
272
  },
301
273
  });
302
274
 
@@ -333,6 +305,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
333
305
 
334
306
  const active = readActive(ctx.cwd);
335
307
  const execution = getExecution();
308
+ disableAutoComplete(ctx, "plan update");
336
309
 
337
310
  // Resolve the plan to revise: explicit arg > running execution > latest in artifact dir.
338
311
  let sourcePlanPath: string | null = planArg
@@ -441,6 +414,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
441
414
  );
442
415
  if (!ok) return;
443
416
  // Abandon must end execution first so the planning model is restored.
417
+ disableAutoComplete(ctx, "run abandoned");
444
418
  if (getExecution()) {
445
419
  await stopExecution(pi, ctx, "run abandoned via /plans-abandon");
446
420
  }
@@ -459,5 +433,6 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
459
433
  // -----------------------------------------------------------------------
460
434
  pi.on("session_start", async (_event, ctx) => {
461
435
  await restoreFromSession(pi, ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreFromSession>[2]);
436
+ restoreAutoCompleteFromSession(ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreAutoCompleteFromSession>[1]);
462
437
  });
463
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plans",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
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",
@@ -9,8 +9,9 @@ This skill set is written for the Pi coding agent's documented behavior:
9
9
  - the five skills are contributed by the pi-plans extension and loaded as Pi skills (also invokable as `/skill:<name>`);
10
10
  - skill references and helper sources are resolved relative to the directory containing `SKILL.md`;
11
11
  - the extension provides these tools: `plans` (workspace state), `ask_choice` (choice prompts), `refine` (reviewer/criticizer subagents), and `execute_plan` (execution handoff);
12
- - `refine` spawns read-only Pi subagents (`pi --mode json -p --no-session --tools read,grep,find,ls`) with isolated context; their results return to the main session as tool output;
13
- - the execution loop is extension-managed: remaining verifier items are injected each turn and `[DONE:VC-xxx]` markers are tracked with a footer progress widget.
12
+ - `refine` spawns read-only Pi subagents (`pi --mode json -p --no-session --tools read,grep,find,ls`) with isolated context; delegated Reviewer/Criticizer runs show a standalone aggregate overlay titled `Reviewer` or `Criticizer`, close the overlay before returning, and return conclusions to the main session as tool output;
13
+ - 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;
14
+ - execution and planning compaction keep Pi's SessionManager as the history owner, group compacted history by implementation item, retain a legal current-I suffix, summarize paired reads as bounded `Read: <path> line <X-Y> Extracted information summary: ...` records, and target `<10%` of the model window after a current-I slice exceeds `20%`; hard-floor cases are recorded instead of looping;
14
15
 
15
16
  ## Planning Boundary
16
17
 
@@ -60,6 +61,7 @@ Every user-facing planning or refinement question goes through the `ask_choice`
60
61
  - `options`: ordered options, recommended option first with `recommended: true` (exactly one), each with the tradeoff that matters in `description`;
61
62
  - do not add `Other` or `Auto-complete` yourself — the tool appends `Other…` second-last and `Auto-complete` last;
62
63
  - pass `autoComplete: false` for the merged accept/execute question — it contains the execution approval, so Auto-complete never appears there — and for any install waiver, publishing, deployment, merge, push, credential, or external-state question. Auto-complete may choose the recommended planning or refinement option only.
64
+ - When the user selects Auto-complete, it remains active for the current planning run: later eligible questions use their recommended options automatically, and the extension queues one deduplicated follow-up if the model stops after an auto-completed answer. `/plans-autocomplete-stop` disables it; session restore may reactivate it only for the same active run while its status is `planning`.
63
65
 
64
66
  Answers are recorded automatically in the active run's `decisions.jsonl`. You must still maintain `DECISIONS.md` in the artifact directory (summary table of questions, options, answers, answer sources, open assumptions).
65
67
 
@@ -126,7 +128,8 @@ A refinement round is complete when all reviewer outputs have returned or all cr
126
128
 
127
129
  When the user picks `✓ Accept PLAN_vN and execute it now` in the merged question, mark the plan accepted and call the `execute_plan` tool (or the user runs `/plans-execute`). It re-confirms with the user, then the extension enters execution mode:
128
130
 
129
- - every agent turn is injected with the remaining verifier checklist and execution rules (layered simplest implementation, no stopgaps, dependency and library discipline, minimum tests);
131
+ - every agent turn is injected with the remaining verifier checklist and execution rules (layered simplest implementation, waiting for subprocess-backed verification with backoff 5s -> 10s -> 20s -> 40s -> 80s, then keep polling at 80s and restart at 5s for each new subprocess, no stopgaps, dependency and library discipline, minimum tests);
132
+ - execution-phase compaction may proactively compact the current-I slice above 20% of the model window, summarizes the oldest eligible prefix and bounded Read records, preserves a legal recent suffix, and records whether the <10% target was met; Pi still owns threshold, overflow, and manual scheduling;
130
133
  - the read-only guard lifts: full write access returns;
131
134
  - the run status moves to `executing`, then `done` when the last `[DONE:VC-xxx]` marker lands;
132
135
  - `/plans-stop` stops execution; `/plans` shows progress.
@@ -120,7 +120,7 @@ If a spawn later fails because the stored selector is unavailable, reset the mar
120
120
 
121
121
  ## Subagent Spawning
122
122
 
123
- When `mode` is `delegated-subagent`, the `refine` tool spawns a read-only `pi` subprocess (`--mode json -p --no-session --tools read,grep,find,ls`) whose system prompt comes from `agents/reviewer.md` or `agents/criticizer.md`. The subagent:
123
+ When `mode` is `delegated-subagent`, the `refine` tool spawns a read-only `pi` subprocess (`--mode json -p --no-session --tools read,grep,find,ls`) whose system prompt comes from `agents/reviewer.md` or `agents/criticizer.md`. In TUI mode, delegated runs also show a standalone `Reviewer` or `Criticizer` overlay with live lane/tool status; the child is awaited and the overlay is closed before the tool result returns. The subagent:
124
124
 
125
125
  - performs read-only analysis and never edits files;
126
126
  - receives the full plan text and a review/criticism brief;
@@ -0,0 +1,163 @@
1
+ /** Run-scoped Auto-complete state and planning-turn continuation. */
2
+
3
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import { getRun, readActive } from "./state.ts";
5
+
6
+ export const AUTOCOMPLETE_ENTRY = "pi-plans-autocomplete";
7
+ const AUTOCOMPLETE_CONTINUE = "Continue the current planning workflow. Raise the next relevant question with ask_choice; do not stop after an auto-completed answer.";
8
+
9
+ type SessionEntry = { type: string; customType?: string; data?: unknown };
10
+
11
+ type AutoCompleteState = {
12
+ runId: string;
13
+ enabled: boolean;
14
+ pendingFollowUp: boolean;
15
+ askChoiceCount: number;
16
+ autoChoiceCount: number;
17
+ planWritten: boolean;
18
+ };
19
+
20
+ type SessionWithAutoComplete = ExtensionContext["sessionManager"] & {
21
+ __piPlansAutoComplete?: AutoCompleteState;
22
+ };
23
+
24
+ let api: ExtensionAPI | null = null;
25
+
26
+ export function setAutoCompleteApi(next: ExtensionAPI | null): void {
27
+ api = next;
28
+ }
29
+
30
+ function sessionState(ctx: ExtensionContext): AutoCompleteState | undefined {
31
+ return (ctx.sessionManager as SessionWithAutoComplete).__piPlansAutoComplete;
32
+ }
33
+
34
+ function activePlanningRun(ctx: ExtensionContext): { runId: string } | null {
35
+ const active = readActive(ctx.cwd);
36
+ if (!active) return null;
37
+ const run = getRun(ctx.cwd, active.run_id);
38
+ return run?.status === "planning" ? { runId: run.run_id } : null;
39
+ }
40
+
41
+ function appendState(runId: string, enabled: boolean, reason?: string): void {
42
+ if (!api) return;
43
+ api.appendEntry(AUTOCOMPLETE_ENTRY, { runId, enabled, ...(reason ? { reason } : {}) });
44
+ }
45
+
46
+ export function enableAutoComplete(ctx: ExtensionContext): boolean {
47
+ const run = activePlanningRun(ctx);
48
+ if (!run) return false;
49
+ const current = sessionState(ctx);
50
+ if (current?.enabled && current.runId === run.runId) return true;
51
+ (ctx.sessionManager as SessionWithAutoComplete).__piPlansAutoComplete = {
52
+ runId: run.runId,
53
+ enabled: true,
54
+ pendingFollowUp: false,
55
+ askChoiceCount: 0,
56
+ autoChoiceCount: 0,
57
+ planWritten: false,
58
+ };
59
+ appendState(run.runId, true);
60
+ return true;
61
+ }
62
+
63
+ export function disableAutoComplete(ctx: ExtensionContext, reason = "disabled"): boolean {
64
+ const state = sessionState(ctx);
65
+ const run = activePlanningRun(ctx);
66
+ const runId = state?.runId ?? run?.runId;
67
+ if (!runId && !state?.enabled) return false;
68
+ if (state) state.enabled = false;
69
+ if (runId) appendState(runId, false, reason);
70
+ return true;
71
+ }
72
+
73
+ export function isAutoCompleteEnabled(ctx: ExtensionContext): boolean {
74
+ const state = sessionState(ctx);
75
+ const run = activePlanningRun(ctx);
76
+ return !!state?.enabled && !!run && state.runId === run.runId;
77
+ }
78
+
79
+ /** Record every ask_choice call for this turn; autoChoice marks a recommendation selected by the mode. */
80
+ export function recordAskChoice(ctx: ExtensionContext, autoChoice: boolean): void {
81
+ const state = sessionState(ctx);
82
+ if (!state) return;
83
+ state.askChoiceCount += 1;
84
+ if (autoChoice) state.autoChoiceCount += 1;
85
+ }
86
+
87
+ export function markPlanWritten(ctx: ExtensionContext): void {
88
+ const state = sessionState(ctx);
89
+ if (state) state.planWritten = true;
90
+ }
91
+
92
+ export function resetAutoCompleteTurn(ctx: ExtensionContext): void {
93
+ const state = sessionState(ctx);
94
+ if (!state) return;
95
+ state.pendingFollowUp = false;
96
+ state.askChoiceCount = 0;
97
+ state.autoChoiceCount = 0;
98
+ state.planWritten = false;
99
+ }
100
+
101
+ export function shouldContinueAutoComplete(ctx: ExtensionContext): boolean {
102
+ const state = sessionState(ctx);
103
+ return isAutoCompleteEnabled(ctx)
104
+ && !!state
105
+ && state.autoChoiceCount > 0
106
+ && state.askChoiceCount === state.autoChoiceCount
107
+ && !state.planWritten
108
+ && !state.pendingFollowUp;
109
+ }
110
+
111
+ export async function continueAutoComplete(ctx: ExtensionContext): Promise<boolean> {
112
+ if (!api || !shouldContinueAutoComplete(ctx)) return false;
113
+ const state = sessionState(ctx);
114
+ if (!state) return false;
115
+ state.pendingFollowUp = true;
116
+ try {
117
+ await api.sendUserMessage(AUTOCOMPLETE_CONTINUE, { deliverAs: "followUp" });
118
+ return true;
119
+ } catch {
120
+ state.pendingFollowUp = false;
121
+ return false;
122
+ }
123
+ }
124
+
125
+ export function registerAutoCompleteTurnHandlers(pi: ExtensionAPI): void {
126
+ setAutoCompleteApi(pi);
127
+ pi.on("turn_start", async (_event, ctx) => {
128
+ resetAutoCompleteTurn(ctx);
129
+ });
130
+ pi.on("turn_end", async (event, ctx) => {
131
+ const message = event.message as { role?: string } | undefined;
132
+ if (message?.role === "assistant") await continueAutoComplete(ctx);
133
+ });
134
+ }
135
+
136
+ export function restoreAutoCompleteFromSession(ctx: ExtensionContext, entries: SessionEntry[]): void {
137
+ const session = ctx.sessionManager as SessionWithAutoComplete;
138
+ delete session.__piPlansAutoComplete;
139
+ let restored: { runId: string; enabled: boolean } | null = null;
140
+ for (let i = entries.length - 1; i >= 0; i--) {
141
+ const entry = entries[i];
142
+ if (entry.type !== "custom" || entry.customType !== AUTOCOMPLETE_ENTRY || !entry.data) continue;
143
+ const data = entry.data as { runId?: unknown; enabled?: unknown };
144
+ if (typeof data.runId === "string" && typeof data.enabled === "boolean") {
145
+ restored = { runId: data.runId, enabled: data.enabled };
146
+ break;
147
+ }
148
+ }
149
+ const active = activePlanningRun(ctx);
150
+ if (!restored || !restored.enabled || !active || restored.runId !== active.runId) return;
151
+ session.__piPlansAutoComplete = {
152
+ runId: active.runId,
153
+ enabled: true,
154
+ pendingFollowUp: false,
155
+ askChoiceCount: 0,
156
+ autoChoiceCount: 0,
157
+ planWritten: false,
158
+ };
159
+ }
160
+
161
+ export function autoCompleteStatus(ctx: ExtensionContext): "enabled" | "disabled" {
162
+ return isAutoCompleteEnabled(ctx) ? "enabled" : "disabled";
163
+ }