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
package/src/panel.ts DELETED
@@ -1,473 +0,0 @@
1
- /**
2
- * Fixed tasks' status panel model — single source of truth for the execution
3
- * phase UI. Pure functions only (no TUI imports): the same derived model
4
- * feeds (1) the aboveEditor `╭─ pi-plans ─ <topic> ──╮` widget, (2) the
5
- * status-bar summary line, and (3) the execution injection text, so the panel
6
- * always shows exactly what the agent is told (D-014, R-011).
7
- *
8
- * Row-count discipline (D-004/D-023): the panel renders a FIXED number of
9
- * lines at every width — 7 rows at >= MIN_PANEL_WIDTH columns, a 3-line badge
10
- * below it — so the terminal buffer height never changes and pi-tui never
11
- * clears the scrollback (pi-goal-x scroll-repro lesson). Line content is
12
- * always truncated per the component's width.
13
- */
14
-
15
- import type { ImplDisplayState } from "./plan.ts";
16
- import { resolveImplStatuses, shortImplDescription, type CheckItem, type ImplItem } from "./plan.ts";
17
- import { truncateToWidth, visibleWidth } from "./refine-ui-helpers.ts";
18
-
19
- /** Widget key under which the panel is registered/cleared. */
20
- export const PANEL_WIDGET_KEY = "pi-plans";
21
-
22
- /** Full panel row count (box top+bottom + 5 content lines). */
23
- export const PANEL_ROW_COUNT = 7;
24
- /** Minimal terminal width for the full 7-row panel; below this a 3-line badge is rendered. */
25
- export const MIN_PANEL_WIDTH = 30;
26
- /** Badge row count below MIN_PANEL_WIDTH. */
27
- export const BADGE_ROW_COUNT = 3;
28
-
29
- export interface GoalWaitPanel {
30
- paused: boolean;
31
- pausedReason?: string;
32
- noProgressRounds: number;
33
- waitRounds: number;
34
- }
35
-
36
- import { panelChrome, type UiLanguage } from "./ui-language.ts";
37
-
38
- /** Chrome language for panel strings (issue #3); defaults to English. */
39
- export function panelLang(model: PanelModel): UiLanguage {
40
- return model.uiLanguage ?? "en";
41
- }
42
-
43
- export interface PanelModel {
44
- /** Run topic (stable run-level slug; D-018). */
45
- topic: string;
46
- /** Execution phase displayed in the header content line. */
47
- phase: string;
48
- /** Extra phase annotation, e.g. "⏸ paused". */
49
- phaseNote?: string;
50
- /** Activity line (footer content row): run status + since-time, or the
51
- * phase word when the run record is unavailable. Always a string (CQ4). */
52
- activity: string;
53
- vcDone: number;
54
- vcTotal: number;
55
- /** Implementation items not yet vc-passed. */
56
- remainingI: number;
57
- totalI: number;
58
- /** Plan-format warning replaces the I-count line (never a fake "I 0/0"). */
59
- implWarning: boolean;
60
- /** Chrome language for the warning strings (issue #3); undefined → "en". */
61
- uiLanguage?: UiLanguage;
62
- /** Resolved current I (marker-backed or inferred; inferred is display-only). */
63
- currentI?: string;
64
- /** Short one-line description of the current I (display-only). */
65
- currentIText?: string;
66
- currentState?: ImplDisplayState;
67
- currentInferred: boolean;
68
- /** Human action line shown to the user and injected to the agent. */
69
- nextAction: string;
70
- goalWait?: GoalWaitPanel;
71
- }
72
-
73
- /**
74
- * Derive the next human action from execution state. `waiting` is an explicit
75
- * approximation signal for "a subprocess is pending" (callers pass
76
- * `goalWait.waitRounds > 0 && !goalWait.paused`, matching the exec loop's
77
- * `/waiting for/` backoff heuristic; F-008).
78
- */
79
- export function deriveNextAction(
80
- execution: {
81
- items: CheckItem[];
82
- implItems?: ImplItem[];
83
- implStatus?: Record<string, ImplMarkerStateLike>;
84
- currentI?: string;
85
- goalWait?: { paused?: boolean; pausedReason?: string; noProgressRounds?: number; waitRounds?: number } | null;
86
- },
87
- waiting: boolean,
88
- statuses: Record<string, ImplDisplayState>,
89
- remainingItems: CheckItem[],
90
- currentId?: string,
91
- ): string {
92
- const goalWait = execution.goalWait;
93
- if (goalWait?.paused) {
94
- return `paused ${goalWait.pausedReason ? `(${goalWait.pausedReason})` : ""}— send a message or /plans-execute to resume`;
95
- }
96
- if (waiting && remainingItems.some((item) => !item.done)) {
97
- return "waiting for a subprocess result — poll with backoff 5s → 10s → 20s → 40s → 80s";
98
- }
99
- const implItems = execution.implItems;
100
- if (implItems && implItems.length > 0) {
101
- // Same current-I resolution as the panel model: marker-backed currentI
102
- // wins (execution focus), otherwise the first non-vc-passed item.
103
- // Inferred-only fallbacks are display guidance, never a state write.
104
- const current =
105
- implItems.find((item) => item.id === currentId && statuses[item.id] !== "vc-passed") ??
106
- implItems.find((item) => statuses[item.id] !== "vc-passed");
107
- if (current) {
108
- const state = statuses[current.id];
109
- const verb = state === "implemented" || state === "validating" ? "Verify" : "Implement";
110
- return `${verb} ${current.id} — ${shortImplDescription(current.text)}`;
111
- }
112
- // All impl markers passed even though VCs remain (marker drift): fall
113
- // back to the verifier list.
114
- }
115
- const next = remainingItems.find((item) => !item.done);
116
- if (next) return `Verify ${next.id} — ${shortImplDescription(next.text)}`;
117
- return "Report completion — no verifier checklist items remain";
118
- }
119
-
120
- type ImplMarkerStateLike = "implemented" | "validating";
121
-
122
- /**
123
- * Pure model derivation. `statuses` may be passed in to share one
124
- * resolveImplStatuses call across model + callers.
125
- */
126
- export function derivePanelModel(
127
- execution: {
128
- items: CheckItem[];
129
- implItems?: ImplItem[];
130
- implStatus?: Record<string, ImplMarkerStateLike>;
131
- currentI?: string;
132
- goalWait?: { paused?: boolean; pausedReason?: string; noProgressRounds?: number; waitRounds?: number } | null;
133
- /** Plan-lint warning (Implementation Items section parsed to zero
134
- * items). Non-null ⇒ implItems is empty — the panel must show the
135
- * warning instead of a fake "I 0/0" count (pi-goal-x semantics). */
136
- implWarning?: string | null;
137
- /** Chrome language for the warning strings (issue #3); undefined → "en". */
138
- uiLanguage?: UiLanguage;
139
- },
140
- topic: string,
141
- waiting: boolean,
142
- run?: { status: string; created_at: string; updated_at: string } | null,
143
- ): PanelModel {
144
- const items = execution.items;
145
- const vcDone = items.filter((item) => item.done).length;
146
- const vcTotal = items.length;
147
- const remainingItems = items.filter((item) => !item.done);
148
- const implItems = execution.implItems ?? [];
149
- const statuses = resolveImplStatuses(implItems.length ? implItems : [], items, execution.implStatus);
150
-
151
- let remainingI = 0;
152
- for (const id of Object.keys(statuses)) {
153
- if (statuses[id] !== "vc-passed") remainingI += 1;
154
- }
155
-
156
- // Current I: marker-backed currentI (only while still pending), else
157
- // display-only inference (first non-vc-passed); never persisted (D-014 /
158
- // pi-goal-x currentTaskId lesson).
159
- let currentI: string | undefined;
160
- let currentIText: string | undefined;
161
- let currentState: ImplDisplayState | undefined;
162
- let currentInferred = false;
163
- if (implItems.length > 0) {
164
- const markerCurrent = implItems.find((item) => item.id === execution.currentI && statuses[item.id] !== "vc-passed");
165
- const firstOpen = implItems.find((item) => statuses[item.id] !== "vc-passed");
166
- const currentImpl = markerCurrent ?? firstOpen;
167
- if (currentImpl) {
168
- currentI = currentImpl.id;
169
- currentIText = currentImpl.text;
170
- currentState = statuses[currentImpl.id];
171
- currentInferred = !markerCurrent;
172
- }
173
- }
174
-
175
- const nextAction = deriveNextAction(execution, waiting, statuses, remainingItems, currentI);
176
-
177
- const gw = execution.goalWait;
178
- const goalWait: GoalWaitPanel | undefined = gw
179
- ? {
180
- paused: gw.paused === true,
181
- pausedReason: gw.pausedReason,
182
- noProgressRounds: gw.noProgressRounds ?? 0,
183
- waitRounds: gw.waitRounds ?? 0,
184
- }
185
- : undefined;
186
-
187
- let phase = "executing";
188
- let phaseNote: string | undefined;
189
- if (goalWait?.paused) {
190
- phase = "paused";
191
- phaseNote = goalWait.pausedReason;
192
- } else if (goalWait && (goalWait.noProgressRounds > 0 || goalWait.waitRounds > 0)) {
193
- phase = "goal-wait";
194
- phaseNote = `no progress ${goalWait.noProgressRounds}/3 · waiting ${goalWait.waitRounds}/6`;
195
- } else if (waiting) {
196
- phase = "goal-wait";
197
- phaseNote = `waiting for subprocess (round ${goalWait?.waitRounds ?? 0}/6)`;
198
- }
199
-
200
- // Activity line: honest "since" semantics — RunInfo.updated_at is the
201
- // status-change time (execution turns only write the checkpoint, not
202
- // run.json), so we label it `since`, never "last activity" (CQ2/D-002).
203
- const activity = run
204
- ? `${run.status} · since ${formatActivityTime(run.updated_at || run.created_at)}`
205
- : phase;
206
-
207
- return {
208
- topic,
209
- phase,
210
- phaseNote,
211
- activity,
212
- vcDone,
213
- vcTotal,
214
- remainingI,
215
- totalI: Math.max(0, implItems.length),
216
- implWarning: (execution.implWarning ?? null) !== null,
217
- uiLanguage: execution.uiLanguage ?? "en",
218
- currentI,
219
- currentIText,
220
- currentState,
221
- currentInferred,
222
- nextAction,
223
- goalWait,
224
- };
225
- }
226
-
227
- // ---------------------------------------------------------------------------
228
- // Rendering helpers (width-safe, ANSI-aware via visibleWidth/truncateToWidth).
229
- // The panel is rendered by the widget factory's render(width) with the theme
230
- // applied per line by the caller (F-004: never prebake ANSI).
231
- // ---------------------------------------------------------------------------
232
-
233
- function fit(text: string, budget: number): string {
234
- return visibleWidth(text) <= budget ? text : truncateToWidth(text, budget);
235
- }
236
-
237
- function pad(text: string, width: number): string {
238
- const used = visibleWidth(text);
239
- return used < width ? text + " ".repeat(width - used) : text;
240
- }
241
-
242
- function boxLine(left: string, body: string, fill: string, width: number, right = left): string {
243
- const bodyFit = fit(body, Math.max(0, width - 2));
244
- return left + bodyFit + fill.repeat(Math.max(0, width - 2 - visibleWidth(bodyFit))) + right;
245
- }
246
-
247
- const BORDER_LEFT = "╭";
248
- const BORDER_RIGHT = "╮";
249
- const BORDER_BOTTOM_LEFT = "╰";
250
- const BORDER_BOTTOM_RIGHT = "╯";
251
- const BORDER_V = "│";
252
- const HORIZ = "─";
253
-
254
- /** `╭─ pi-plans ─ <topic> ────╮` header line (pi-goal-x boxHeader style). */
255
- export function renderPanelHeader(topic: string, width: number): string {
256
- const inner = `${HORIZ} pi-plans ${HORIZ} ${topic}`;
257
- return boxLine(BORDER_LEFT, inner, HORIZ, width, BORDER_RIGHT);
258
- }
259
-
260
- /** Content line with vertical borders; pads to the fixed width so the buffer
261
- * content never shrinks the row (row-count stability is width-independent). */
262
- export function renderPanelLine(content: string, width: number): string {
263
- return BORDER_V + pad(` ${fit(content, Math.max(0, width - 4))} `, width - 2) + BORDER_V;
264
- }
265
-
266
- export function renderPanelFooter(width: number): string {
267
- return boxLine(BORDER_BOTTOM_LEFT, "", HORIZ, width, BORDER_BOTTOM_RIGHT);
268
- }
269
-
270
- /** Format a run timestamp as local `MM-DD HH:mm` for the activity line.
271
- * Empty/invalid input degrades to `--` (never throws, never "undefined"). */
272
- export function formatActivityTime(iso: string | null | undefined): string {
273
- if (!iso) return "--";
274
- const d = new Date(iso);
275
- if (Number.isNaN(d.getTime())) return "--";
276
- const p2 = (n: number) => String(n).padStart(2, "0");
277
- return `${p2(d.getMonth() + 1)}-${p2(d.getDate())} ${p2(d.getHours())}:${p2(d.getMinutes())}`;
278
- }
279
-
280
- /** Progress bar (10 cells) for the I items: filled = vc-passed count. */
281
- export function renderProgressBar(done: number, total: number, width: number): string {
282
- const cells = Math.max(1, Math.min(10, Math.floor((width - 24) / 2)));
283
- const filled = total > 0 ? Math.round((done / total) * cells) : 0;
284
- return `${"█".repeat(Math.min(cells, filled))}${"░".repeat(Math.max(0, cells - filled))}`;
285
- }
286
-
287
- /**
288
- * Render the full panel as fixed rows. Mode selection:
289
- * - width >= MIN_PANEL_WIDTH → PANEL_ROW_COUNT rows with three-level narrow
290
- * degradation inside (D-023): @1 Next-action line keeps only verb + I-id,
291
- * @2 goal-wait counts are dropped, @3 the current-I line keeps only its id.
292
- * - width < MIN_PANEL_WIDTH → BADGE_ROW_COUNT rows (header + status + footer).
293
- * Returned array length is always PANEL_ROW_COUNT or BADGE_ROW_COUNT.
294
- */
295
- export function renderPanelLines(model: PanelModel, width: number): string[] {
296
- const w = Math.max(4, Math.floor(width));
297
- const chrome = panelChrome(panelLang(model));
298
- if (w < MIN_PANEL_WIDTH) {
299
- const status = model.implWarning
300
- ? chrome.badgeStatus(model.topic, model.vcDone, model.vcTotal)
301
- : `${model.topic} · I ${model.totalI - model.remainingI}/${model.totalI} · VC ${model.vcDone}/${model.vcTotal}`;
302
- return [
303
- boxLine(BORDER_LEFT, ` pi-plans ${HORIZ} ${fit(model.topic, Math.max(4, w - 14))}`, HORIZ, w, BORDER_RIGHT),
304
- renderPanelLine(status, w),
305
- boxLine(BORDER_BOTTOM_LEFT, "", HORIZ, w, BORDER_BOTTOM_RIGHT),
306
- ];
307
- }
308
-
309
- const statusLine =
310
- model.phase === "executing"
311
- ? `phase: executing`
312
- : `phase: ${model.phase}${model.phaseNote ? ` · ${model.phaseNote}` : ""}`;
313
-
314
- // Narrow degradation level 1: drop goal-wait counts from the status line.
315
- const narrow1 = w < 40;
316
- const statusContent = narrow1 && model.goalWait && !model.goalWait.paused && model.goalWait.waitRounds === 0
317
- ? `phase: ${model.phase}`
318
- : statusLine;
319
-
320
- const progressContent = model.implWarning
321
- ? chrome.progressWarning()
322
- : `I items ${model.totalI - model.remainingI}/${model.totalI} · ${renderProgressBar(model.totalI - model.remainingI, model.totalI, w)} · VC ${model.vcDone}/${model.vcTotal}`;
323
-
324
- // Narrow degradation level 3: current-I line keeps only the id.
325
- const currentLabel =
326
- model.currentI === undefined
327
- ? "(no implementation items)"
328
- : narrow1
329
- ? `${model.currentI} ${model.currentInferred ? "(inferred)" : ""}`
330
- : `${model.currentI} ${model.currentState ?? ""}${model.currentInferred ? " (inferred)" : ""} — ${fit(model.currentIText ?? "", Math.max(4, w - 30))}`;
331
-
332
- // Narrow degradation level 1: Next-action keeps verb + id only.
333
- const nextLabel = narrow1 ? model.nextAction.replace(/— .*$/, "") : model.nextAction;
334
-
335
- return [
336
- renderPanelHeader(model.topic, w),
337
- renderPanelLine(statusContent, w),
338
- renderPanelLine(progressContent, w),
339
- renderPanelLine(currentLabel, w),
340
- renderPanelLine(`Next: ${nextLabel}`, w),
341
- renderPanelLine(model.activity, w),
342
- renderPanelFooter(w),
343
- ];
344
- }
345
-
346
- /** Bottom status-bar summary line, derived from the same model (D-015). */
347
- export function formatPanelSummaryLine(model: PanelModel): string {
348
- if (model.implWarning) {
349
- return panelChrome(panelLang(model)).summaryWarning(model.topic, model.vcDone, model.vcTotal, model.nextAction);
350
- }
351
- const i = model.totalI > 0 ? ` · I ${model.totalI - model.remainingI}/${model.totalI}` : "";
352
- const phase =
353
- model.phase === "executing" ? "exec" : model.phase === "paused" ? "goal-wait paused" : model.phase;
354
- const paused = model.goalWait?.paused ? ` (${model.goalWait.pausedReason ?? "paused"})` : "";
355
- return `plans: ${model.topic} ▸ ${phase}${paused}${i} · VC ${model.vcDone}/${model.vcTotal} · next: ${model.nextAction}`;
356
- }
357
-
358
- /** Minimal theme surface the panel needs (pi's theme.fg). */
359
- export interface PanelTheme {
360
- fg(color: string, text: string): string;
361
- }
362
-
363
- /** Per-line theme mapping for the panel widget. Vertical borders (│) and box
364
- * chrome stay uniformly MUTED GRAY; only the content between the borders
365
- * takes the line's accent color — │ never inherits the line color
366
- * (user-requested uniform-gray frame). Pure: the widget just forwards. */
367
- export function themePanelLines(lines: string[], model: PanelModel, theme: PanelTheme): string[] {
368
- return lines.map((line, index) => {
369
- if (index === 0) {
370
- const brandIndex = line.indexOf("pi-plans");
371
- if (brandIndex >= 0) {
372
- return (
373
- theme.fg("muted", line.slice(0, brandIndex)) +
374
- theme.fg("accent", "pi-plans") +
375
- theme.fg("muted", line.slice(brandIndex + "pi-plans".length))
376
- );
377
- }
378
- return theme.fg("muted", line);
379
- }
380
- const color =
381
- index === 2
382
- ? (model.implWarning ? "warning" : "accent")
383
- : index === 4
384
- ? "success"
385
- : index === 1 && model.phase !== "executing"
386
- ? "warning"
387
- : "muted";
388
- // Border-aware coloring: content lines are │…│ — color only the inner
389
- // span so both │ glyphs keep the uniform muted gray.
390
- if (line.length >= 2 && line.startsWith(BORDER_V) && line.endsWith(BORDER_V)) {
391
- return (
392
- theme.fg("muted", BORDER_V) +
393
- theme.fg(color, line.slice(1, -1)) +
394
- theme.fg("muted", BORDER_V)
395
- );
396
- }
397
- return theme.fg("muted", line);
398
- });
399
- }
400
- // ---------------------------------------------------------------------------
401
- // Implementation-review loop widget (0.5.4, D-3): when execution is null but
402
- // the active run's checkpoint is still in the implementation-review phase, the
403
- // panel stays alive with a compact loop box derived from checkpoint state.
404
- // Pure model + renderer; exec.ts supplies live checkpoint reads at render
405
- // time (D-8 freshness) so post-write redraws never show stale rounds.
406
- // ---------------------------------------------------------------------------
407
-
408
- export interface ImplReviewLoopModel {
409
- /** Run topic for the box header. */
410
- topic: string;
411
- /** 1-based round label for the NEXT round (completedRounds + 1). */
412
- roundLabel: string;
413
- /** "<n> reviewer(s)" or "config pending" when reviewerCount is unset. */
414
- reviewerLabel: string;
415
- /** Serialized termination condition, or "config pending". */
416
- conditionLabel: string;
417
- }
418
-
419
- /** Derive the loop box model from the checkpoint's implementationReview state. */
420
- export function deriveImplReviewLoopModel(
421
- topic: string,
422
- review: { terminationCondition?: string; reviewerCount?: number; completedRounds: number } | undefined,
423
- ): ImplReviewLoopModel {
424
- const reviewers = review?.reviewerCount;
425
- return {
426
- topic,
427
- roundLabel: `round ${(review?.completedRounds ?? 0) + 1}`,
428
- reviewerLabel:
429
- reviewers === undefined ? "reviewers config pending" : `${reviewers} reviewer${reviewers === 1 ? "" : "s"}`,
430
- conditionLabel: review?.terminationCondition ?? "termination config pending",
431
- };
432
- }
433
-
434
- /** Render the compact loop box (4 lines: header, loop, condition, footer). */
435
- export function renderImplReviewLoopLines(model: ImplReviewLoopModel, width: number): string[] {
436
- const w = Math.max(4, Math.floor(width));
437
- const loopContent = `impl-review · ${model.roundLabel} · ${model.reviewerLabel}`;
438
- return [
439
- renderPanelHeader(model.topic, w),
440
- renderPanelLine(loopContent, w),
441
- renderPanelLine(`until: ${model.conditionLabel}`, w),
442
- renderPanelFooter(w),
443
- ];
444
- }
445
-
446
- /** Theme the loop box: header per themePanelLines conventions, accent loop
447
- * line, muted rest; │ borders never inherit content color. */
448
- export function themeImplReviewLoopLines(lines: string[], theme: PanelTheme): string[] {
449
- return lines.map((line, index) => {
450
- if (index === 0) {
451
- const brandIndex = line.indexOf("pi-plans");
452
- if (brandIndex >= 0) {
453
- return (
454
- theme.fg("muted", line.slice(0, brandIndex)) +
455
- theme.fg("accent", "pi-plans") +
456
- theme.fg("muted", line.slice(brandIndex + "pi-plans".length))
457
- );
458
- }
459
- return theme.fg("muted", line);
460
- }
461
- const color = index === 1 ? "accent" : "muted";
462
- if (line.length >= 2 && line.startsWith(BORDER_V) && line.endsWith(BORDER_V)) {
463
- return theme.fg("muted", BORDER_V) + theme.fg(color, line.slice(1, -1)) + theme.fg("muted", BORDER_V);
464
- }
465
- return theme.fg("muted", line);
466
- });
467
- }
468
-
469
- /** Bottom status-bar line for the live implementation-review loop (D-015:
470
- * same model as the box). */
471
- export function formatImplReviewLoopSummaryLine(model: ImplReviewLoopModel): string {
472
- return `🔁 plans: impl-review ${model.roundLabel} · ${model.reviewerLabel}`;
473
- }
@@ -1,73 +0,0 @@
1
- /**
2
- * Single source of truth for the post-execution implementation-review
3
- * termination question. Consumed by BOTH the goal-running continuation prompt
4
- * (src/exec.ts AMELIORATION_PROMPT_TEXT) and the ask_choice trailing branch
5
- * (tools/ask-choice.ts). Pure constants only — no runtime imports, no
6
- * execution-loop coupling.
7
- */
8
-
9
- export const TERMINATION_QUESTION = "How should the implementation-review loop terminate?";
10
-
11
- export const TERMINATION_OPTIONS = [
12
- "goal wait: continue until no unpassed VCs remain (auto-continue each round)",
13
- "until no high-severity finding (hard cap 5 rounds)",
14
- "1 round",
15
- "2 rounds",
16
- "3 rounds",
17
- ] as const;
18
-
19
- /** "1. <option> 2. <option> …" — recommended (goal wait) first. */
20
- export function renderTerminationOptions(): string {
21
- return TERMINATION_OPTIONS.map((option, index) => `${index + 1}. ${option}`).join(" ");
22
- }
23
-
24
- /** Stable question id for the termination question so cross-session resume
25
- * can deduplicate it and link the answer to the checkpoint (I-004). */
26
- export const TERMINATION_QUESTION_ID = "termination-condition";
27
-
28
- /** Instruction appended wherever the termination question is requested.
29
- * Persistence is deferred: BOTH config answers (termination + reviewer count)
30
- * land in ONE combined record-checkpoint (D-5 crash window stays a single
31
- * transaction boundary). */
32
- export const TERMINATION_RECORDING_INSTRUCTIONS =
33
- 'Ask it with ask_choice using questionId: "termination-condition" (autoComplete: false). Do not persist yet: after the reviewer-count question is also answered, persist BOTH together in one plans record-checkpoint (checkpoint: { transition: "implementation-review-configured", terminationCondition: "<the chosen option>", reviewerCount: <chosen integer> }).';
34
-
35
- /** Skill levels whose implementation-review rounds default to 3 concurrent
36
- * reviewers; every other skill (and unknown/absent skills) defaults to 1. */
37
- const THREE_REVIEWER_SKILLS = new Set(["plan-big", "plan-with-refs"]);
38
-
39
- /** Default concurrent reviewers for implementation-review rounds, derived
40
- * from the run's skill (D-1). */
41
- export function defaultImplReviewers(skill: string | undefined): 1 | 3 {
42
- return skill !== undefined && THREE_REVIEWER_SKILLS.has(skill) ? 3 : 1;
43
- }
44
-
45
- /** The per-run reviewer-count configuration question asked alongside the
46
- * termination condition (D-2). Stable question id so crash recovery can
47
- * rebuild answered config from the decisions ledger (D-5). */
48
- export const IMPL_REVIEWER_COUNT_QUESTION =
49
- "How many concurrent reviewers should each implementation-review round use?";
50
- export const IMPL_REVIEWER_COUNT_QUESTION_ID = "impl-review-reviewer-count";
51
-
52
- /** Pure-digit option labels with the recommended flag rendered as ★ by the
53
- * ask_choice UI — no "(recommended)" text inside labels, so the chosen label
54
- * IS the integer (D-6). Recommended first. */
55
- export function implReviewerCountOptions(
56
- defaultCount: 1 | 3,
57
- ): { label: string; recommended: boolean }[] {
58
- const recommended = String(defaultCount);
59
- const rest = ["1", "2", "3"].filter((label) => label !== recommended);
60
- return [{ label: recommended, recommended: true }, ...rest.map((label) => ({ label, recommended: false }))];
61
- }
62
-
63
- /** Skill-aware instruction line for the reviewer-count question, composed
64
- * into the goal-running continuation (src/exec.ts) and the ask_choice
65
- * trailing branch (tools/ask-choice.ts). Digit labels, recommended first
66
- * (D-6), combined persistence with the termination answer (D-5). */
67
- export function implReviewerCountPromptLine(skill: string | undefined): string {
68
- const countDefault = defaultImplReviewers(skill);
69
- const options = implReviewerCountOptions(countDefault)
70
- .map((option) => `${option.label}${option.recommended ? " (recommended)" : ""}`)
71
- .join(" ");
72
- return `Then ask the reviewer-count question as a second single-question ask_choice (autoComplete: false, allowOther: false, questionId: "${IMPL_REVIEWER_COUNT_QUESTION_ID}", options: ${options}): "${IMPL_REVIEWER_COUNT_QUESTION}" (default follows the run's skill: plan-big / plan-with-refs → 3, others → 1). After BOTH answers, persist them together in ONE call: plans record-checkpoint (checkpoint: { transition: "implementation-review-configured", terminationCondition: "<termination answer>", reviewerCount: <chosen integer> }). Afterwards run each implementation-review round with refine (role reviewer, target implementation, reviewers: <configured reviewerCount>; if omitted, the refine tool falls back to this configured value).`;
73
- }