pi-plans 0.1.1 → 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,15 +7,18 @@
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">
14
14
  <a href="https://github.com/MaxInGaussian/pi-plans/stargazers"><img alt="GitHub stars" src="https://img.shields.io/github/stars/MaxInGaussian/pi-plans?style=square" /></a>
15
15
  <a href="https://hits.sh/github.com/MaxInGaussian/pi-plans/"><img alt="Repo views" src="https://hits.sh/github.com/MaxInGaussian/pi-plans.svg?label=repo%20views" /></a>
16
- <a href="./LICENSE"><img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-blue.svg?style=square" /></a>
17
- <img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-TS-3178C6?style=square&logo=typescript&logoColor=white" />
18
- <img alt="pi-package" src="https://img.shields.io/badge/pi--package-ready-7c3aed?style=square" />
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
+ <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
+ <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>
20
+ <a href="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/pi-plans?color=22c55e" /></a>
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>
19
22
  </p>
20
23
 
21
24
  ---
@@ -60,7 +63,7 @@ A rough change request becomes a versioned Markdown plan instead of a surprise d
60
63
  tracked execution loop
61
64
  fused AGENTS.md × Ponytail executor rules
62
65
  checklist injected each turn, [DONE:VC-xxx]
63
- markers tracked via bottom status bar + detail panel
66
+ markers tracked via bottom status bar
64
67
  |
65
68
  v
66
69
  run status: done
@@ -110,10 +113,15 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
110
113
  | Capability | In short |
111
114
  |---|---|
112
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`) |
113
- | Choice prompts | `ask_choice`: recommended option first, answers auto-recorded per run |
114
- | 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 |
115
118
  | Workspace state | Config, runs, decisions, refs, and subagent ledgers in `.git/pi_plans/` (git common dir) |
116
- | Tracked execution | Checklist injected each turn; `[DONE:VC-xxx]` markers drive progress; live `x/y · spent · in/out-toks` in the bottom status bar |
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 |
117
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 |
118
126
  | Write guard | `edit`/`write` blocked outside planning artifacts while a run is active |
119
127
 
@@ -123,14 +131,35 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
123
131
  |---|---|
124
132
  | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
125
133
  | `ask_choice` | Numbered choice prompt; `autoComplete: false` for the merged accept/execute question and external-state questions |
126
- | `refine` | Reviewer/criticizer round via read-only subagents (`--tools read,grep,find,ls`); `reviewers: 3` for big plans; enforces role/model confirmation gates |
127
- | `execute_plan` | Execution handoff: re-confirms with the user, enters extension-managed execution mode |
128
- | `/plans` | Show config, active run, execution progress |
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 |
136
+ | `/plans` | Show config, active run, and execution progress |
129
137
  | `/plans-execute [plan.md]` | Manual execution handoff (defaults to highest `PLAN_vN.md`) |
130
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 |
131
140
  | `/plans-stop` | Stop execution mode |
132
141
  | `/plans-abandon` | Abandon the active run (lifts the write guard; artifacts stay) |
133
- | Status bar (lifecycle) | 💬 Q&A → 📝 draft written (planning sub-phases) → ⌛ executing `x/y · spent · in/out-toks` → ⛔ 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.
134
163
 
135
164
  ## The execution rules
136
165
 
@@ -148,6 +177,9 @@ Once you approve the handoff, every turn injects a compact rule set that fuses M
148
177
 
149
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.
150
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
+
151
183
  ## Skills
152
184
 
153
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.
@@ -188,12 +220,12 @@ or register the absolute path in `~/.pi/agent/settings.json`:
188
220
  pi-plans/
189
221
  ├── index.ts # Extension entry: tools, commands, guard, execution loop
190
222
  ├── tools/ # plans, ask-choice, refine, execute-plan
191
- ├── src/ # state, guard, plan parsing, subagent runner, exec loop
223
+ ├── src/ # state, guard, plan parsing, subagent runner, refine overlay, exec loop
192
224
  ├── skills/ # The planning router plus five specialist planning skills
193
225
  ├── references/ # Shared workflow, state/config, plan template (normative)
194
226
  ├── agents/ # reviewer.md / criticizer.md subagent prompts
195
227
  ├── scripts/validate.ts # Structure validator
196
- └── tests/ # node:test suite (state, guard, plan parsing, execution)
228
+ └── tests/ # node:test suite (state, guard, plan parsing, execution, refine progress)
197
229
  ```
198
230
 
199
231
  ## Safety model
@@ -217,7 +249,7 @@ The plan is the contract. Refinement converges on scope while nothing is writabl
217
249
 
218
250
  **What can Auto-complete decide on my behalf?**
219
251
 
220
- 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.
221
253
 
222
254
  **Where does all the state live?**
223
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,21 +19,37 @@ 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
- applyDoneMarkers,
23
- completeExecution,
24
- consumePendingPanelSync,
22
+ consumePlanningCompactionResumeGuard,
23
+ drainExecutionFlush,
25
24
  executionContextMessage,
25
+ filterExecutionResumeMessages,
26
+ filterPlanningResumeMessages,
26
27
  getExecution,
27
- isExecutionComplete,
28
- recordExecutionCompletion,
29
- recordTouchedPaths,
28
+ handleExecutionBeforeCompact,
29
+ handleExecutionCompact,
30
+ handleExecutionCompactFailed,
31
+ handleExecutionTurnCompaction,
32
+ handlePlanningBeforeCompact,
33
+ handlePlanningCompact,
34
+ handlePlanningCompactFailed,
35
+ PLANNING_PLAN_WRITTEN_CUSTOM_TYPE,
36
+ registerExecutionTurnHandlers,
37
+ refreshPlanningCompactionCooldown,
38
+ requestPlanningCompaction,
30
39
  restoreFromSession,
31
40
  stopExecution,
32
- syncExecutionPanel,
33
- toggleExecutionPanelView,
34
41
  updateStatusWidget,
42
+ shouldTriggerPlanningCompaction,
35
43
  } from "./src/exec.ts";
44
+ import {
45
+ autoCompleteStatus,
46
+ disableAutoComplete,
47
+ markPlanWritten,
48
+ registerAutoCompleteTurnHandlers,
49
+ restoreAutoCompleteFromSession,
50
+ } from "./src/autocomplete.ts";
36
51
  import { planningWriteBlockReason } from "./src/guard.ts";
52
+ import { registerQueryInterviewHooks } from "./src/query-hook.ts";
37
53
  import { latestPlanVersion, nextPlanVersionPath } from "./src/plan.ts";
38
54
  import { getRun, readActive, recordDecision, resolveStateRootOrNull, setRunStatus } from "./src/state.ts";
39
55
  import { registerAskChoiceTool } from "./tools/ask-choice.ts";
@@ -43,20 +59,37 @@ import { registerRefineTool } from "./tools/refine.ts";
43
59
 
44
60
  const baseDir = dirname(fileURLToPath(import.meta.url));
45
61
 
46
- function extractPathsFromBash(command: string): string[] {
47
- const values = new Set<string>();
48
- for (const token of command.split(/\s+/)) {
49
- const cleaned = token.replace(/^["'`(<[{]+|["'`)>}\],;]+$/g, "");
50
- if (!cleaned || cleaned === "." || cleaned === ".." || cleaned.startsWith("-") || cleaned.includes("=") ) continue;
51
- const looksLikePath =
52
- cleaned.includes("/") ||
53
- cleaned.startsWith(".") ||
54
- cleaned.startsWith("~") ||
55
- /^[A-Za-z0-9._-]+\.[A-Za-z0-9._-]+$/.test(cleaned);
56
- if (!looksLikePath) continue;
57
- values.add(cleaned);
62
+ // When THIS copy of the extension was imported into the running pi process.
63
+ // /plans compares it against the newest source-file mtime so a stale instance
64
+ // (code on disk newer than the loaded copy) is immediately visible.
65
+ const extensionLoadedAt = new Date();
66
+
67
+ function extensionStalenessLine(): string {
68
+ try {
69
+ const dirs = [baseDir, path.join(baseDir, "src"), path.join(baseDir, "tools")];
70
+ let newest = 0;
71
+ for (const dir of dirs) {
72
+ for (const name of fs.readdirSync(dir)) {
73
+ if (!name.endsWith(".ts")) continue;
74
+ const mtime = fs.statSync(path.join(dir, name)).mtimeMs;
75
+ if (mtime > newest) newest = mtime;
76
+ }
77
+ }
78
+ if (newest > extensionLoadedAt.getTime() + 2000) {
79
+ return `⚠ extension code on disk is newer than the loaded copy (loaded ${extensionLoadedAt.toISOString()}); run /reload to pick it up`;
80
+ }
81
+ return `Extension loaded: ${extensionLoadedAt.toISOString()} (up to date)`;
82
+ } catch {
83
+ return `Extension loaded: ${extensionLoadedAt.toISOString()}`;
58
84
  }
59
- return [...values];
85
+ }
86
+
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";
60
93
  }
61
94
 
62
95
  export default function piPlansExtension(pi: ExtensionAPI): void {
@@ -64,6 +97,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
64
97
  registerAskChoiceTool(pi);
65
98
  registerRefineTool(pi, baseDir);
66
99
  registerExecutePlanTool(pi);
100
+ registerQueryInterviewHooks(pi, hasActivePlanningWorkflow);
67
101
 
68
102
  // Contribute the router skill plus the five specialist planning skills.
69
103
  pi.on("resources_discover", () => ({
@@ -105,31 +139,71 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
105
139
  // not been approved), edit/write may only target planning artifacts.
106
140
  // -----------------------------------------------------------------------
107
141
  pi.on("tool_call", async (event, ctx) => {
108
- const execution = getExecution();
109
- if (execution) {
110
- if (event.toolName === "edit" || event.toolName === "write") {
111
- const rawPath = String((event.input as { path?: string }).path ?? "");
112
- if (rawPath) recordTouchedPaths(ctx.cwd, [rawPath]);
113
- }
114
- if (event.toolName === "bash") {
115
- const command = String((event.input as { command?: string }).command ?? "");
116
- if (command) recordTouchedPaths(ctx.cwd, extractPathsFromBash(command));
117
- }
118
- return;
119
- }
142
+ if (getExecution()) return;
120
143
  const rawPath = String((event.input as { path?: string }).path ?? "");
121
144
  if (!rawPath) return;
122
145
  const reason = planningWriteBlockReason({ workdir: ctx.cwd, toolName: event.toolName, rawPath });
123
- if (!reason) return;
124
- return { block: true, reason };
146
+ if (reason) return { block: true, reason };
147
+ // Allowed write: if it lands exactly on the run's latest plan file, drop a
148
+ // marker entry so planning-phase compaction can anchor its cut point there.
149
+ if (event.toolName === "write" || event.toolName === "edit") {
150
+ const active = readActive(ctx.cwd);
151
+ if (active) {
152
+ const latest = latestPlanVersion(active.artifact_dir);
153
+ if (latest && path.resolve(ctx.cwd, rawPath) === path.resolve(ctx.cwd, latest.path)) {
154
+ pi.appendEntry(PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, {
155
+ runId: active.run_id,
156
+ planPath: latest.path,
157
+ });
158
+ markPlanWritten(ctx);
159
+ }
160
+ }
161
+ }
162
+ return;
163
+ });
164
+
165
+ pi.on("context", (event) => {
166
+ const filteredExecution = filterExecutionResumeMessages(event.messages as Array<{ customType?: string }>);
167
+ const messages = filterPlanningResumeMessages(filteredExecution);
168
+ if (messages.length !== event.messages.length) {
169
+ return { messages };
170
+ }
171
+ });
172
+
173
+ pi.on("session_before_compact", async (event, ctx) => {
174
+ const executionResult = await handleExecutionBeforeCompact(pi, ctx, event);
175
+ if (executionResult) return executionResult;
176
+ return handlePlanningBeforeCompact(pi, ctx, event);
177
+ });
178
+ pi.on("session_compact", async (event, ctx) => {
179
+ handleExecutionCompact(pi, ctx, event);
180
+ handlePlanningCompact(pi, ctx, event);
181
+ });
182
+ pi.on("session_compact_failed", async (event, ctx) => {
183
+ handleExecutionCompactFailed(pi, ctx, event);
184
+ handlePlanningCompactFailed(pi, ctx, event);
185
+ });
186
+
187
+ // Flush points for deferred execution-loop writes: primary drain when the
188
+ // agent run fully settles, backstop drain at the next run's start (covers
189
+ // continuation paths that might not emit agent_settled), plus the forced
190
+ // synchronous flush inside stop/complete.
191
+ pi.on("agent_settled", async (_event, ctx) => {
192
+ drainExecutionFlush(pi, ctx);
125
193
  });
126
194
 
127
195
  // -----------------------------------------------------------------------
128
196
  // Execution loop: inject remaining checklist each turn, track markers.
129
197
  // -----------------------------------------------------------------------
130
- pi.on("before_agent_start", async () => {
198
+ pi.on("before_agent_start", async (_event, ctx) => {
199
+ drainExecutionFlush(pi, ctx);
131
200
  const content = executionContextMessage();
132
- if (!content) return;
201
+ if (!content) {
202
+ if (!getExecution() && shouldTriggerPlanningCompaction(ctx)) {
203
+ requestPlanningCompaction(ctx);
204
+ }
205
+ return;
206
+ }
133
207
  return {
134
208
  message: {
135
209
  customType: "pi-plans-exec-context",
@@ -139,58 +213,27 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
139
213
  };
140
214
  });
141
215
 
142
- // The turn_end projection does not carry usage; message_end delivers the
143
- // full assistant message, so we cache it there and consume it per turn.
144
- let lastAssistantUsage: { input: number; output: number } | null = null;
145
- pi.on("message_end", async (event) => {
146
- const message = event.message as { role?: string; usage?: { input?: number; output?: number } };
147
- if (message?.role === "assistant" && message.usage) {
148
- lastAssistantUsage = { input: message.usage.input ?? 0, output: message.usage.output ?? 0 };
149
- }
150
- });
151
-
152
- pi.on("turn_end", async (event, ctx) => {
153
- const message = event.message as { role?: string; content?: Array<{ type: string; text?: string }> };
154
- if (!message || message.role !== "assistant") {
155
- updateStatusWidget(ctx);
156
- return;
157
- }
158
- const text = (message.content ?? [])
159
- .filter((part) => part.type === "text")
160
- .map((part) => part.text ?? "")
161
- .join("\n");
162
- const changedIds = applyDoneMarkers(text);
163
- if (changedIds.length > 0) {
164
- // Attribute this turn's token usage to the finished items.
165
- const projection = (event.message as { usage?: { input?: number; output?: number } }).usage;
166
- const raw = projection ?? lastAssistantUsage;
167
- lastAssistantUsage = null; // consumed: never re-attribute a stale turn
168
- const usage = raw ? { input: raw.input ?? 0, output: raw.output ?? 0 } : undefined;
169
- recordExecutionCompletion(pi, ctx, changedIds, usage);
170
- }
171
- if (getExecution() && isExecutionComplete()) {
172
- completeExecution(pi, ctx);
216
+ registerExecutionTurnHandlers(pi, async (ctx) => {
217
+ if (getExecution()) {
218
+ handleExecutionTurnCompaction(ctx);
219
+ } else {
220
+ refreshPlanningCompactionCooldown(ctx);
221
+ if (consumePlanningCompactionResumeGuard(ctx)) {
222
+ updateStatusWidget(ctx);
223
+ return;
224
+ }
225
+ if (shouldTriggerPlanningCompaction(ctx)) {
226
+ requestPlanningCompaction(ctx);
227
+ }
173
228
  }
174
- // A busy-toggle during the previous turn deferred its re-render; the
175
- // turn just ended, so this is the safe point to apply it.
176
- if (consumePendingPanelSync()) syncExecutionPanel(ctx);
229
+ // A completed turn is the safe point for status updates.
177
230
  updateStatusWidget(ctx);
178
231
  });
232
+ registerAutoCompleteTurnHandlers(pi);
179
233
 
180
234
  // -----------------------------------------------------------------------
181
- // Commands and shortcuts
235
+ // Commands
182
236
  // -----------------------------------------------------------------------
183
- pi.registerShortcut("alt+o", {
184
- description: "Toggle pi-plans execution checklist widget",
185
- handler: async (ctx) => {
186
- const expanded = toggleExecutionPanelView(pi, ctx);
187
- if (expanded === null) {
188
- ctx.ui.notify("No execution in progress.", "info");
189
- return;
190
- }
191
- ctx.ui.notify(expanded ? "Execution checklist expanded." : "Execution checklist collapsed.", "info");
192
- },
193
- });
194
237
 
195
238
  pi.registerCommand("plans", {
196
239
  description: "Show pi-plans state: config, active run, and execution progress",
@@ -215,19 +258,17 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
215
258
  lines.push(` ${item.done ? "☑" : "☐"} ${item.id}`);
216
259
  }
217
260
  }
261
+ lines.push(`Auto-complete: ${autoCompleteStatus(ctx)}`);
262
+ lines.push(extensionStalenessLine());
218
263
  ctx.ui.notify(lines.join("\n"), "info");
219
264
  },
220
265
  });
221
266
 
222
- pi.registerCommand("plans-list", {
223
- description: "Toggle the pi-plans execution checklist widget",
267
+ pi.registerCommand("plans-autocomplete-stop", {
268
+ description: "Stop Auto-complete for the active planning run",
224
269
  handler: async (_args, ctx) => {
225
- const expanded = toggleExecutionPanelView(pi, ctx);
226
- if (expanded === null) {
227
- ctx.ui.notify("No execution in progress.", "info");
228
- return;
229
- }
230
- 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");
231
272
  },
232
273
  });
233
274
 
@@ -264,6 +305,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
264
305
 
265
306
  const active = readActive(ctx.cwd);
266
307
  const execution = getExecution();
308
+ disableAutoComplete(ctx, "plan update");
267
309
 
268
310
  // Resolve the plan to revise: explicit arg > running execution > latest in artifact dir.
269
311
  let sourcePlanPath: string | null = planArg
@@ -292,7 +334,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
292
334
  `${doneIds.length}/${execution.items.length} verifier item(s) already verified; their work stays. Remaining items return to planning.`,
293
335
  );
294
336
  if (!ok) return;
295
- stopExecution(pi, ctx, "interrupted by /update-plan");
337
+ await stopExecution(pi, ctx, "interrupted by /update-plan");
296
338
  }
297
339
 
298
340
  // Return the run to planning so refinement rules and guards apply again.
@@ -353,7 +395,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
353
395
  }
354
396
  const ok = await ctx.ui.confirm("Stop execution?", "Remaining verifier items will be left unfinished.");
355
397
  if (!ok) return;
356
- stopExecution(pi, ctx, "stopped by user via /plans-stop");
398
+ await stopExecution(pi, ctx, "stopped by user via /plans-stop");
357
399
  ctx.ui.notify("Execution stopped.", "info");
358
400
  },
359
401
  });
@@ -371,6 +413,11 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
371
413
  `${active.run_id}\nThe read-only guard lifts; committed artifacts stay in place.`,
372
414
  );
373
415
  if (!ok) return;
416
+ // Abandon must end execution first so the planning model is restored.
417
+ disableAutoComplete(ctx, "run abandoned");
418
+ if (getExecution()) {
419
+ await stopExecution(pi, ctx, "run abandoned via /plans-abandon");
420
+ }
374
421
  try {
375
422
  setRunStatus(ctx.cwd, active.run_id, "abandoned");
376
423
  ctx.ui.notify(`Run ${active.run_id} abandoned.`, "info");
@@ -385,6 +432,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
385
432
  // Session lifecycle
386
433
  // -----------------------------------------------------------------------
387
434
  pi.on("session_start", async (_event, ctx) => {
388
- restoreFromSession(pi, ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreFromSession>[2]);
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]);
389
437
  });
390
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plans",
3
- "version": "0.1.1",
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;