pi-plans 0.1.1 → 0.1.2

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
@@ -13,9 +13,11 @@
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="./LICENSE"><img alt="License" src="https://img.shields.io/npm/l/pi-plans?color=22c55e" /></a>
20
+ <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
21
  </p>
20
22
 
21
23
  ---
@@ -113,7 +115,10 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
113
115
  | Choice prompts | `ask_choice`: recommended option first, answers auto-recorded per run |
114
116
  | Refinement rounds | Read-only reviewer/criticizer Pi subagents consolidate findings into the next plan version |
115
117
  | 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 |
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 |
117
122
  | 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
123
  | Write guard | `edit`/`write` blocked outside planning artifacts while a run is active |
119
124
 
@@ -121,16 +126,17 @@ Planning artifacts live under `./docs/pi-plans/YYYY-MM-DD-<topic>/` by default (
121
126
 
122
127
  | Tool / Command | Purpose |
123
128
  |---|---|
124
- | `plans` | State CLI: `init`, `show`, `set-language`, `set-artifact-root`, `set-role`, `start-run`, `set-status`, `record-decision`, `record-ref`, `record-subagent` |
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` |
125
130
  | `ask_choice` | Numbered choice prompt; `autoComplete: false` for the merged accept/execute question and external-state questions |
126
131
  | `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 |
129
- | `/plans-execute [plan.md]` | Manual execution handoff (defaults to highest `PLAN_vN.md`) |
132
+ | `execute_plan` | Execution handoff: re-confirms with the user, prompts for an execution model if needed, and enters extension-managed execution mode |
133
+ | `/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) |
130
136
  | `/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 |
131
137
  | `/plans-stop` | Stop execution mode |
132
138
  | `/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 |
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 |
134
140
 
135
141
  ## The execution rules
136
142
 
package/index.ts CHANGED
@@ -19,19 +19,31 @@ 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
22
  consumePendingPanelSync,
23
+ consumePlanningCompactionResumeGuard,
24
+ drainExecutionFlush,
25
25
  executionContextMessage,
26
+ filterExecutionResumeMessages,
27
+ filterPlanningResumeMessages,
26
28
  getExecution,
27
- isExecutionComplete,
28
- recordExecutionCompletion,
29
+ handleExecutionBeforeCompact,
30
+ handleExecutionCompact,
31
+ handleExecutionCompactFailed,
32
+ handlePlanningBeforeCompact,
33
+ handlePlanningCompact,
34
+ handlePlanningCompactFailed,
35
+ PLANNING_PLAN_WRITTEN_CUSTOM_TYPE,
36
+ registerExecutionTurnHandlers,
29
37
  recordTouchedPaths,
38
+ refreshPlanningCompactionCooldown,
39
+ requestPlanningCompaction,
30
40
  restoreFromSession,
41
+ ensureExecutionModelActive,
31
42
  stopExecution,
32
43
  syncExecutionPanel,
33
44
  toggleExecutionPanelView,
34
45
  updateStatusWidget,
46
+ shouldTriggerPlanningCompaction,
35
47
  } from "./src/exec.ts";
36
48
  import { planningWriteBlockReason } from "./src/guard.ts";
37
49
  import { latestPlanVersion, nextPlanVersionPath } from "./src/plan.ts";
@@ -43,6 +55,31 @@ import { registerRefineTool } from "./tools/refine.ts";
43
55
 
44
56
  const baseDir = dirname(fileURLToPath(import.meta.url));
45
57
 
58
+ // When THIS copy of the extension was imported into the running pi process.
59
+ // /plans compares it against the newest source-file mtime so a stale instance
60
+ // (code on disk newer than the loaded copy) is immediately visible.
61
+ const extensionLoadedAt = new Date();
62
+
63
+ function extensionStalenessLine(): string {
64
+ try {
65
+ const dirs = [baseDir, path.join(baseDir, "src"), path.join(baseDir, "tools")];
66
+ let newest = 0;
67
+ for (const dir of dirs) {
68
+ for (const name of fs.readdirSync(dir)) {
69
+ if (!name.endsWith(".ts")) continue;
70
+ const mtime = fs.statSync(path.join(dir, name)).mtimeMs;
71
+ if (mtime > newest) newest = mtime;
72
+ }
73
+ }
74
+ if (newest > extensionLoadedAt.getTime() + 2000) {
75
+ return `⚠ extension code on disk is newer than the loaded copy (loaded ${extensionLoadedAt.toISOString()}); run /reload to pick it up`;
76
+ }
77
+ return `Extension loaded: ${extensionLoadedAt.toISOString()} (up to date)`;
78
+ } catch {
79
+ return `Extension loaded: ${extensionLoadedAt.toISOString()}`;
80
+ }
81
+ }
82
+
46
83
  function extractPathsFromBash(command: string): string[] {
47
84
  const values = new Set<string>();
48
85
  for (const token of command.split(/\s+/)) {
@@ -120,16 +157,67 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
120
157
  const rawPath = String((event.input as { path?: string }).path ?? "");
121
158
  if (!rawPath) return;
122
159
  const reason = planningWriteBlockReason({ workdir: ctx.cwd, toolName: event.toolName, rawPath });
123
- if (!reason) return;
124
- return { block: true, reason };
160
+ if (reason) return { block: true, reason };
161
+ // Allowed write: if it lands exactly on the run's latest plan file, drop a
162
+ // marker entry so planning-phase compaction can anchor its cut point there.
163
+ if (event.toolName === "write") {
164
+ const active = readActive(ctx.cwd);
165
+ if (active) {
166
+ const latest = latestPlanVersion(active.artifact_dir);
167
+ if (latest && path.resolve(ctx.cwd, rawPath) === path.resolve(ctx.cwd, latest.path)) {
168
+ pi.appendEntry(PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, {
169
+ runId: active.run_id,
170
+ planPath: latest.path,
171
+ });
172
+ }
173
+ }
174
+ }
175
+ return;
176
+ });
177
+
178
+ pi.on("context", (event) => {
179
+ const filteredExecution = filterExecutionResumeMessages(event.messages as Array<{ customType?: string }>);
180
+ const messages = filterPlanningResumeMessages(filteredExecution);
181
+ if (messages.length !== event.messages.length) {
182
+ return { messages };
183
+ }
184
+ });
185
+
186
+ pi.on("session_before_compact", async (event, ctx) => {
187
+ const executionResult = await handleExecutionBeforeCompact(pi, ctx, event);
188
+ if (executionResult) return executionResult;
189
+ return handlePlanningBeforeCompact(pi, ctx, event);
190
+ });
191
+ pi.on("session_compact", async (event, ctx) => {
192
+ handleExecutionCompact(pi, ctx, event);
193
+ handlePlanningCompact(pi, ctx, event);
194
+ });
195
+ pi.on("session_compact_failed", async (event, ctx) => {
196
+ handleExecutionCompactFailed(pi, ctx, event);
197
+ handlePlanningCompactFailed(pi, ctx, event);
198
+ });
199
+
200
+ // Flush points for deferred execution-loop writes: primary drain when the
201
+ // agent run fully settles, backstop drain at the next run's start (covers
202
+ // continuation paths that might not emit agent_settled), plus the forced
203
+ // synchronous flush inside stop/complete.
204
+ pi.on("agent_settled", async (_event, ctx) => {
205
+ drainExecutionFlush(pi, ctx);
125
206
  });
126
207
 
127
208
  // -----------------------------------------------------------------------
128
209
  // Execution loop: inject remaining checklist each turn, track markers.
129
210
  // -----------------------------------------------------------------------
130
- pi.on("before_agent_start", async () => {
211
+ pi.on("before_agent_start", async (_event, ctx) => {
212
+ drainExecutionFlush(pi, ctx);
213
+ await ensureExecutionModelActive(pi, ctx);
131
214
  const content = executionContextMessage();
132
- if (!content) return;
215
+ if (!content) {
216
+ if (!getExecution() && shouldTriggerPlanningCompaction(ctx)) {
217
+ requestPlanningCompaction(ctx);
218
+ }
219
+ return;
220
+ }
133
221
  return {
134
222
  message: {
135
223
  customType: "pi-plans-exec-context",
@@ -139,37 +227,17 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
139
227
  };
140
228
  });
141
229
 
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);
230
+ registerExecutionTurnHandlers(pi, async (ctx) => {
231
+ if (!getExecution()) {
232
+ refreshPlanningCompactionCooldown(ctx);
233
+ if (consumePlanningCompactionResumeGuard(ctx)) {
234
+ if (consumePendingPanelSync()) syncExecutionPanel(ctx);
235
+ updateStatusWidget(ctx);
236
+ return;
237
+ }
238
+ if (shouldTriggerPlanningCompaction(ctx)) {
239
+ requestPlanningCompaction(ctx);
240
+ }
173
241
  }
174
242
  // A busy-toggle during the previous turn deferred its re-render; the
175
243
  // turn just ended, so this is the safe point to apply it.
@@ -215,6 +283,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
215
283
  lines.push(` ${item.done ? "☑" : "☐"} ${item.id}`);
216
284
  }
217
285
  }
286
+ lines.push(extensionStalenessLine());
218
287
  ctx.ui.notify(lines.join("\n"), "info");
219
288
  },
220
289
  });
@@ -292,7 +361,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
292
361
  `${doneIds.length}/${execution.items.length} verifier item(s) already verified; their work stays. Remaining items return to planning.`,
293
362
  );
294
363
  if (!ok) return;
295
- stopExecution(pi, ctx, "interrupted by /update-plan");
364
+ await stopExecution(pi, ctx, "interrupted by /update-plan");
296
365
  }
297
366
 
298
367
  // Return the run to planning so refinement rules and guards apply again.
@@ -353,7 +422,7 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
353
422
  }
354
423
  const ok = await ctx.ui.confirm("Stop execution?", "Remaining verifier items will be left unfinished.");
355
424
  if (!ok) return;
356
- stopExecution(pi, ctx, "stopped by user via /plans-stop");
425
+ await stopExecution(pi, ctx, "stopped by user via /plans-stop");
357
426
  ctx.ui.notify("Execution stopped.", "info");
358
427
  },
359
428
  });
@@ -371,6 +440,10 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
371
440
  `${active.run_id}\nThe read-only guard lifts; committed artifacts stay in place.`,
372
441
  );
373
442
  if (!ok) return;
443
+ // Abandon must end execution first so the planning model is restored.
444
+ if (getExecution()) {
445
+ await stopExecution(pi, ctx, "run abandoned via /plans-abandon");
446
+ }
374
447
  try {
375
448
  setRunStatus(ctx.cwd, active.run_id, "abandoned");
376
449
  ctx.ui.notify(`Run ${active.run_id} abandoned.`, "info");
@@ -385,6 +458,6 @@ export default function piPlansExtension(pi: ExtensionAPI): void {
385
458
  // Session lifecycle
386
459
  // -----------------------------------------------------------------------
387
460
  pi.on("session_start", async (_event, ctx) => {
388
- restoreFromSession(pi, ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreFromSession>[2]);
461
+ await restoreFromSession(pi, ctx, ctx.sessionManager.getEntries() as unknown as Parameters<typeof restoreFromSession>[2]);
389
462
  });
390
463
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-plans",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",