pi-plans 0.6.0 → 0.8.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.
Files changed (116) hide show
  1. package/AGENTS.md +58 -0
  2. package/CONTRIBUTING.md +8 -15
  3. package/README.md +39 -37
  4. package/agents/execution-reviewer.md +40 -0
  5. package/agents/reviewer.md +12 -3
  6. package/index.ts +55 -58
  7. package/package.json +2 -1
  8. package/references/pi-planning-workflow.md +50 -60
  9. package/references/plan-artifact-template.md +81 -60
  10. package/references/state-and-config.md +60 -44
  11. package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
  12. package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
  13. package/scripts/run-tests.ts +12 -1
  14. package/scripts/validate.ts +39 -11
  15. package/skills/debug-and-plan/SKILL.md +3 -3
  16. package/skills/plan-big/SKILL.md +3 -3
  17. package/skills/plan-normal/SKILL.md +3 -3
  18. package/skills/plan-small/SKILL.md +4 -4
  19. package/skills/plan-with-refs/SKILL.md +6 -6
  20. package/skills/planning/SKILL.md +1 -1
  21. package/src/ask-form.ts +4 -4
  22. package/src/auditor.ts +227 -0
  23. package/src/auto-approve.ts +1 -1
  24. package/src/autocomplete.ts +19 -17
  25. package/src/code-graph/commands.ts +8 -3
  26. package/src/code-graph/community.ts +1 -1
  27. package/src/code-graph/paths.ts +1 -1
  28. package/src/code-graph/watch.ts +2 -2
  29. package/src/compaction.ts +3 -3
  30. package/src/config-command.ts +146 -73
  31. package/src/dashboard.ts +303 -0
  32. package/src/exec.ts +1185 -924
  33. package/src/global-state.ts +304 -0
  34. package/src/guard.ts +18 -19
  35. package/src/messaging.ts +44 -0
  36. package/src/plan.ts +421 -112
  37. package/src/query-hook.ts +4 -4
  38. package/src/refine-prompts.ts +12 -70
  39. package/src/refine-ui-helpers.ts +24 -5
  40. package/src/refine-ui-state.ts +1 -1
  41. package/src/refine-ui.ts +19 -3
  42. package/src/resume-command.ts +45 -129
  43. package/src/resume.ts +5 -1
  44. package/src/role-panels.ts +542 -0
  45. package/src/run-context.ts +3 -10
  46. package/src/staleness.ts +53 -0
  47. package/src/state.ts +273 -72
  48. package/src/subagent.ts +19 -29
  49. package/src/task-tool.ts +100 -0
  50. package/src/tasks.ts +223 -0
  51. package/src/thinking-levels.ts +67 -0
  52. package/src/ui-language.ts +7 -54
  53. package/src/workflow-state.ts +76 -58
  54. package/tests/analyze-refs.test.ts +35 -18
  55. package/tests/ask-choice-schema.test.ts +0 -12
  56. package/tests/ask-choice.test.ts +2 -49
  57. package/tests/ask-form-tool.test.ts +4 -5
  58. package/tests/ask-form.test.ts +2 -2
  59. package/tests/auditor.test.ts +210 -0
  60. package/tests/auto-approve.test.ts +7 -10
  61. package/tests/autocomplete.test.ts +8 -11
  62. package/tests/code-graph-apply-action.test.ts +2 -2
  63. package/tests/code-graph-commands.test.ts +2 -2
  64. package/tests/code-graph-index.test.ts +2 -2
  65. package/tests/code-graph-loop.e2e.test.ts +1 -1
  66. package/tests/code-graph-mutations.test.ts +1 -1
  67. package/tests/code-graph-rollback.test.ts +1 -1
  68. package/tests/code-graph-v05.test.ts +2 -2
  69. package/tests/compaction.test.ts +1 -1
  70. package/tests/config-command.test.ts +103 -100
  71. package/tests/dashboard.test.ts +402 -0
  72. package/tests/exec-lifecycle.test.ts +181 -115
  73. package/tests/exec-panel-lifecycle.test.ts +106 -251
  74. package/tests/exec-review-loop.test.ts +331 -0
  75. package/tests/exec.test.ts +771 -1706
  76. package/tests/execute-plan.test.ts +44 -19
  77. package/tests/extension-load.test.ts +48 -0
  78. package/tests/global-state.test.ts +371 -0
  79. package/tests/graph-aware-file-tools.test.ts +5 -5
  80. package/tests/guard.test.ts +1 -1
  81. package/tests/multi-run.test.ts +3 -103
  82. package/tests/plan.test.ts +139 -62
  83. package/tests/plans.test.ts +7 -79
  84. package/tests/refine-prompts.test.ts +20 -71
  85. package/tests/refine-resume.test.ts +27 -22
  86. package/tests/refine-ui.test.ts +6 -15
  87. package/tests/resume-lifecycle.test.ts +41 -22
  88. package/tests/resume.test.ts +39 -81
  89. package/tests/role-panels.test.ts +391 -0
  90. package/tests/run-context.test.ts +1 -1
  91. package/tests/run-ownership.test.ts +1 -1
  92. package/tests/stale-ctx.test.ts +218 -0
  93. package/tests/staleness.test.ts +76 -0
  94. package/tests/state.test.ts +155 -32
  95. package/tests/subagent-thinking.test.ts +65 -0
  96. package/tests/subagent-usage.test.ts +1 -1
  97. package/tests/task-tool.test.ts +61 -0
  98. package/tests/tasks.test.ts +142 -0
  99. package/tests/thinking-levels.test.ts +77 -0
  100. package/tests/ui-language.test.ts +2 -17
  101. package/tests/workflow-state.test.ts +73 -90
  102. package/tools/analyze-refs.ts +67 -32
  103. package/tools/ask-choice.ts +7 -53
  104. package/tools/code-graph.ts +2 -2
  105. package/tools/execute-plan.ts +55 -99
  106. package/tools/graph-aware-file-tools.ts +4 -10
  107. package/tools/plans.ts +41 -67
  108. package/tools/refine.ts +101 -164
  109. package/agents/criticizer.md +0 -18
  110. package/agents/executor.md +0 -26
  111. package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
  112. package/src/panel.ts +0 -473
  113. package/src/termination-prompt.ts +0 -73
  114. package/tests/goal-wait.test.ts +0 -269
  115. package/tests/panel-i-zero.test.ts +0 -420
  116. package/tests/panel.test.ts +0 -355
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Task dashboard (v0.6.1): the execution-phase UI. A compact aboveEditor
3
+ * widget shows the current task, wave, progress bar, and verification-check
4
+ * state; Ctrl+Shift+T toggles an expanded tree view with per-task markers
5
+ * (✓ complete, ▸ current, ~ skipped, · pending). Pure model + render
6
+ * functions shared by the widget, the status line, and the expanded view so
7
+ * all three never disagree. Width-adaptive: narrow terminals get compact
8
+ * rows, wide terminals get full titles and file lists.
9
+ *
10
+ * Hard invariant: every returned line is at most `width` *display columns*.
11
+ * The widget host measures each line with its own `visibleWidth` and throws an
12
+ * uncaughtException (killing the whole TUI session) when a line is wider than
13
+ * the terminal, so all width math here is column-based via the shared helpers
14
+ * — never `.length`, `padEnd`, or `slice`, which count UTF-16 units and
15
+ * undercount CJK (one CJK char = one unit but two columns).
16
+ */
17
+
18
+ import type { CheckItem } from "./plan.ts";
19
+ import { REVIEW_MAX_ROUNDS } from "./auditor.ts";
20
+ import { truncateToWidth, visibleWidth } from "./refine-ui-helpers.ts";
21
+ import {
22
+ allTasksTerminal,
23
+ currentTask,
24
+ flattenTaskViews,
25
+ maxWave,
26
+ taskIsTerminal,
27
+ taskProgress,
28
+ type TaskView,
29
+ } from "./tasks.ts";
30
+
31
+ export const DASHBOARD_WIDGET_KEY = "pi-plans-dashboard";
32
+
33
+ export interface DashboardModel {
34
+ topic: string;
35
+ tasks: TaskView[];
36
+ checklist: CheckItem[];
37
+ paused: boolean;
38
+ pausedReason?: string;
39
+ /** Audit round counter; null = audit not yet started. */
40
+ auditRounds: number | null;
41
+ auditFailed: string[];
42
+ /** Checks whose verdict the auditor could not be read for. Neither passed
43
+ * nor failed: shown so the run does not read as finished. */
44
+ auditUndeterminable: string[];
45
+ /** v0.8: true while an execution-review round is in flight. The panel must
46
+ * never render `audit complete ✓` while this is set — the round-1 mis-cue
47
+ * showed the tick for the whole duration of a running audit. */
48
+ reviewRunning: boolean;
49
+ startedAt: string;
50
+ usage: { inToks: number; outToks: number };
51
+ }
52
+
53
+ /** Tree marker for one task row. A rolled-back task is `pending` but still
54
+ * carries the evidence of its previous attempt, so it gets its own marker
55
+ * rather than reading as untouched work. */
56
+ export function taskMarker(task: TaskView, currentId: string | null): string {
57
+ if (taskIsTerminal(task)) return task.status === "skipped" ? "~" : "✓";
58
+ if (task.id === currentId) return "▸";
59
+ return task.evidence === undefined ? "·" : "↺";
60
+ }
61
+
62
+ /** Truncate to at most `max` display columns, ellipsis when clipped. */
63
+ function clip(text: string, max: number): string {
64
+ return max <= 0 ? "" : truncateToWidth(text, max);
65
+ }
66
+
67
+ /** Pad with `fill` out to exactly `width` display columns; never widens. */
68
+ function padToWidth(text: string, width: number, fill = " "): string {
69
+ const gap = width - visibleWidth(text);
70
+ return gap > 0 ? text + fill.repeat(gap) : text;
71
+ }
72
+
73
+ /**
74
+ * One bordered row of exactly `width` display columns: left border, fitted
75
+ * body, filler, right border. Every compact row goes through here so the box
76
+ * can never be `width + 1` columns wide (the old `padEnd().slice(0, width) +
77
+ * border` shape was one column over on every row).
78
+ */
79
+ function boxRow(left: string, body: string, fill: string, width: number, right = left): string {
80
+ const inner = Math.max(0, width - 2);
81
+ return left + padToWidth(clip(body, inner), inner, fill) + right;
82
+ }
83
+
84
+ /** Last-resort clamp so no rendered line can ever exceed the terminal width. */
85
+ function clampLines(lines: string[], width: number): string[] {
86
+ if (width <= 0) return [];
87
+ return lines.map((line) => truncateToWidth(line, width));
88
+ }
89
+
90
+ /** Minimal structural subset of the pi Theme the widget needs. */
91
+ export interface DashboardTheme {
92
+ fg(color: string, text: string): string;
93
+ }
94
+
95
+ /** Brand label opening the title row. */
96
+ const BRAND = "π-plans";
97
+
98
+ /** `HH:MM:SS` since the run started. */
99
+ export function formatElapsed(startedAt: string): string {
100
+ const started = Date.parse(startedAt);
101
+ if (Number.isNaN(started)) return "00:00:00";
102
+ const total = Math.max(0, Math.floor((Date.now() - started) / 1000));
103
+ const h = String(Math.floor(total / 3600)).padStart(2, "0");
104
+ const m = String(Math.floor((total % 3600) / 60)).padStart(2, "0");
105
+ const sec = String(total % 60).padStart(2, "0");
106
+ return `${h}:${m}:${sec}`;
107
+ }
108
+
109
+ /**
110
+ * Title row: `┌─ π-plans: <plan> ───┐`, brand in the accent colour, the rest
111
+ * muted. Colours are emitted as *sequential* balanced spans rather than nested:
112
+ * a theme `fg()` closes with the default-foreground reset, so wrapping a muted
113
+ * span around an accent one would leave everything after it unstyled.
114
+ */
115
+ function titleRow(topic: string, width: number, theme?: DashboardTheme): string {
116
+ const inner = Math.max(0, width - 2);
117
+ const prefix = "─ ";
118
+ const sep = ": ";
119
+ const used = prefix.length + BRAND.length + sep.length;
120
+ const name = clip(topic, Math.max(0, inner - used));
121
+ const fill = "─".repeat(Math.max(0, inner - used - visibleWidth(name)));
122
+ const rest = `${sep}${name}${fill}┐`;
123
+ if (!theme) return `┌${prefix}${BRAND}${rest}`;
124
+ // The left corner rides in the first muted span so the whole border is one colour.
125
+ return theme.fg("muted", `┌${prefix}`) + theme.fg("accent", BRAND) + theme.fg("muted", rest);
126
+ }
127
+
128
+ /**
129
+ * Progress row: `Tasks 3/10 · Verification checks 1/6 · 00:04:12 ███░░░░░░░░`.
130
+ * The bar, then the elapsed time, then the counts are dropped in that order
131
+ * when the terminal is too narrow to hold them, so the counts always survive.
132
+ */
133
+ function progressRow(
134
+ model: DashboardModel,
135
+ done: number,
136
+ total: number,
137
+ vcDone: number,
138
+ width: number,
139
+ narrow: boolean,
140
+ ): string {
141
+ const inner = Math.max(0, width - 2);
142
+ const counts = `Tasks ${done}/${total} · Verification checks ${vcDone}/${model.checklist.length}`;
143
+ const elapsed = ` · ${formatElapsed(model.startedAt)}`;
144
+ const bar = ` ${progressBar(done, total, narrow ? 8 : 12)}`;
145
+ let body = ` ${counts}${elapsed}${bar}`;
146
+ if (visibleWidth(body) > inner) body = ` ${counts}${elapsed}`;
147
+ if (visibleWidth(body) > inner) body = ` ${counts}`;
148
+ return boxRow("│", body, " ", width);
149
+ }
150
+
151
+ /** Compact widget lines (aboveEditor). Adapts to width: <64 narrow. */
152
+ export function renderDashboardLines(model: DashboardModel, width: number, theme?: DashboardTheme): string[] {
153
+ const p = taskProgress(model.tasks);
154
+ const cur = currentTask(model.tasks);
155
+ const vcDone = model.checklist.filter((item) => item.done).length;
156
+ const narrow = width < 64;
157
+ const lines: string[] = [];
158
+ const inner = Math.max(0, width - 2);
159
+ lines.push(titleRow(model.topic, width, theme));
160
+ lines.push(progressRow(model, p.done, p.total, vcDone, width, narrow));
161
+ if (model.paused) {
162
+ lines.push(boxRow("│", ` ⏸ ${clip(model.pausedReason ?? "paused", inner - 3)}`, " ", width));
163
+ } else if (cur) {
164
+ const waveLine = narrow ? `▸ ${cur.id} (w${cur.wave})` : `▸ ${cur.id} (wave ${cur.wave}/${maxWave(model.tasks)}): ${cur.title}`;
165
+ lines.push(boxRow("│", ` ${clip(waveLine, inner - 2)}`, " ", width));
166
+ if (!narrow && cur.files.length > 0) {
167
+ lines.push(boxRow("│", ` ${clip(cur.files.join(", "), inner - 4)}`, " ", width));
168
+ }
169
+ } else if (allTasksTerminal(model.tasks)) {
170
+ // v0.8 mis-cue guard: `audit complete ✓` ONLY when nothing is running
171
+ // and nothing is owed. A running or owed review shows its round number
172
+ // instead — the panel must look alive, never finished-then-silent.
173
+ const owed = model.checklist.some((item) => !item.done);
174
+ const nextRound = (model.auditRounds ?? 0) + 1;
175
+ const auditLine = model.reviewRunning
176
+ ? `review: round ${nextRound}/${REVIEW_MAX_ROUNDS} running — read-only reviewer verifying`
177
+ : model.auditFailed.length > 0
178
+ ? `audit: ${model.auditFailed.length} check(s) failed — rollback pending`
179
+ : model.auditUndeterminable.length > 0
180
+ ? `audit: ${model.auditUndeterminable.length} check(s) undeterminable — verdict unreadable`
181
+ : owed
182
+ ? `review: round ${nextRound}/${REVIEW_MAX_ROUNDS} — verdict pending`
183
+ : model.auditRounds !== null
184
+ ? "audit complete ✓"
185
+ : "all tasks terminal — audit pending";
186
+ lines.push(boxRow("│", ` ${clip(auditLine, inner - 2)}`, " ", width));
187
+ }
188
+ if (!narrow && model.auditFailed.length > 0) {
189
+ lines.push(boxRow("│", ` ✗ ${clip(model.auditFailed.join(", "), inner - 3)}`, " ", width));
190
+ }
191
+ if (!narrow && model.auditUndeterminable.length > 0) {
192
+ lines.push(boxRow("│", ` ? ${clip(model.auditUndeterminable.join(", "), inner - 3)}`, " ", width));
193
+ }
194
+ // Rolled-back work keeps the evidence of the attempt that was rolled back.
195
+ // The tree view shows it per row; the compact panel has room for a summary
196
+ // and must show it too, otherwise the retention is invisible outside the
197
+ // expanded view. Capped at two rows to protect the fixed panel height.
198
+ for (const rolled of flattenTaskViews(model.tasks)
199
+ .filter((task) => task.status === "pending" && task.evidence !== undefined)
200
+ .slice(0, 2)) {
201
+ lines.push(boxRow("│", ` ↺ ${rolled.id} ${clip(rolled.evidence!, inner - 6)}`, " ", width));
202
+ }
203
+ lines.push(boxRow("└", "─ Ctrl+Shift+T tree", "─", width, "┘"));
204
+ // The title row carries its own colours; every other row is muted.
205
+ const painted = theme ? lines.map((line, i) => (i === 0 ? line : theme.fg("muted", line))) : lines;
206
+ return clampLines(painted, width);
207
+ }
208
+
209
+ function progressBar(done: number, total: number, width = 12): string {
210
+ if (total <= 0) return "";
211
+ const filled = Math.round((done / total) * width);
212
+ return `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
213
+ }
214
+
215
+ export function deriveDashboardModel(
216
+ topic: string,
217
+ tasks: TaskView[],
218
+ checklist: CheckItem[],
219
+ extra?: { paused?: boolean; pausedReason?: string; auditRounds?: number | null; auditFailed?: string[]; auditUndeterminable?: string[]; reviewRunning?: boolean; startedAt?: string; usage?: { inToks: number; outToks: number } },
220
+ ): DashboardModel {
221
+ return {
222
+ topic,
223
+ tasks,
224
+ checklist,
225
+ paused: extra?.paused ?? false,
226
+ pausedReason: extra?.pausedReason,
227
+ auditRounds: extra?.auditRounds ?? null,
228
+ auditFailed: extra?.auditFailed ?? [],
229
+ auditUndeterminable: extra?.auditUndeterminable ?? [],
230
+ reviewRunning: extra?.reviewRunning ?? false,
231
+ startedAt: extra?.startedAt ?? new Date().toISOString(),
232
+ usage: extra?.usage ?? { inToks: 0, outToks: 0 },
233
+ };
234
+ }
235
+
236
+ /** Status-bar summary line (single line, language-neutral symbols). */
237
+ export function formatDashboardSummaryLine(model: DashboardModel): string {
238
+ const p = taskProgress(model.tasks);
239
+ const vcDone = model.checklist.filter((item) => item.done).length;
240
+ const cur = currentTask(model.tasks);
241
+ const wave = cur ? ` · wave ${cur.wave}` : "";
242
+ const audit = model.auditRounds !== null ? ` · audit r${model.auditRounds}` : "";
243
+ const pause = model.paused ? " · ⏸ paused" : "";
244
+ return `plans: ${model.topic} ▸ tasks ${p.done}/${p.total} · VC ${vcDone}/${model.checklist.length}${wave}${audit}${pause}`;
245
+ }
246
+
247
+ /** Expanded tree view lines (Ctrl+Shift+T overlay). Wide layout from 96 cols. */
248
+ export function renderDashboardTreeLines(model: DashboardModel, width: number, theme?: DashboardTheme): string[] {
249
+ const p = taskProgress(model.tasks);
250
+ const vcDone = model.checklist.filter((item) => item.done).length;
251
+ const cur = currentTask(model.tasks);
252
+ const wide = width >= 96;
253
+ const lines: string[] = [];
254
+ lines.push(`${model.topic} — task tree (${p.done}/${p.total} done · VC ${vcDone}/${model.checklist.length})${model.paused ? " · ⏸ paused" : ""}`);
255
+ const row = (task: TaskView, depth: number): void => {
256
+ const indent = " ".repeat(depth);
257
+ const marker = taskMarker(task, cur?.id ?? null);
258
+ const title = wide ? task.title : clip(task.title, 48);
259
+ let line = `${indent}${marker} ${task.id}: ${title}`;
260
+ if (wide && task.files.length > 0) line += ` [${task.files.join(", ")}]`;
261
+ if (wide && task.deps.length > 0) line += ` (deps: ${task.deps.join(", ")})`;
262
+ if (task.status === "skipped" && task.skipReason) line += ` ~${task.skipReason}`;
263
+ if (task.status === "complete" && task.evidence) line += ` ✓${clip(task.evidence, 40)}`;
264
+ // Rolled-back work keeps its evidence, and that evidence is the only
265
+ // record of what the previous attempt did.
266
+ if (task.status === "pending" && task.evidence) line += ` ↺${clip(task.evidence, 40)}`;
267
+ lines.push(line);
268
+ for (const child of task.children) row(child, depth + 1);
269
+ }; for (const task of model.tasks) row(task, 0);
270
+ lines.push("");
271
+ lines.push("Verification checks:");
272
+ for (const item of model.checklist) {
273
+ const mark = model.auditFailed.includes(item.id) ? "✗" : item.done ? "☑" : "☐";
274
+ lines.push(` ${mark} ${item.id}${wide ? ` ${clip(item.text.split(";")[1] ?? item.text, Math.min(80, width))}` : ""}`);
275
+ }
276
+ if (model.reviewRunning) {
277
+ lines.push("");
278
+ lines.push(`Execution review: round ${(model.auditRounds ?? 0) + 1}/${REVIEW_MAX_ROUNDS} running`);
279
+ } else if (model.auditRounds !== null) {
280
+ lines.push("");
281
+ const verdict = model.auditFailed.length > 0
282
+ ? ` — failed: ${model.auditFailed.join(", ")}`
283
+ : model.auditUndeterminable.length > 0
284
+ ? ` — undeterminable: ${model.auditUndeterminable.join(", ")}`
285
+ : " — passed ✓";
286
+ lines.push(`Execution review: round ${model.auditRounds}${verdict}`);
287
+ }
288
+ const painted = theme ? lines.map((line) => theme.fg("muted", line)) : lines;
289
+ return clampLines(painted, width);
290
+ }
291
+
292
+ /** Flat open-task summary for injections: current wave first. */
293
+ export function openTaskSummary(tasks: TaskView[]): string {
294
+ const cur = currentTask(tasks);
295
+ if (!cur) return "(none — all tasks terminal)";
296
+ const open = flattenTaskViews(tasks).filter((task) => !taskIsTerminal(task));
297
+ const currentWave = cur.wave;
298
+ const inWave = open.filter((task) => task.wave === currentWave).map((task) => task.id);
299
+ const later = open.filter((task) => task.wave > currentWave).map((task) => task.id);
300
+ const parts = [`current wave ${currentWave}: ${inWave.join(", ") || "(none)"}`];
301
+ if (later.length > 0) parts.push(`later waves: ${later.join(", ")}`);
302
+ return parts.join(" · ");
303
+ }