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 +15 -9
- package/index.ts +115 -42
- package/package.json +1 -1
- package/src/exec.ts +812 -33
- package/src/execution-panel.ts +155 -21
- package/src/plan.ts +106 -0
- package/src/state.ts +36 -0
- package/tests/exec.test.ts +837 -40
- package/tests/execution-panel.test.ts +44 -15
- package/tests/plan.test.ts +84 -1
- package/tests/plans.test.ts +36 -0
- package/tests/state.test.ts +60 -0
- package/tools/execute-plan.ts +46 -6
- package/tools/plans.ts +34 -1
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="
|
|
17
|
-
<img alt="
|
|
18
|
-
<img alt="
|
|
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;
|
|
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-
|
|
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
|
-
|
|
28
|
-
|
|
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 (
|
|
124
|
-
|
|
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)
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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