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.
- package/AGENTS.md +58 -0
- package/CONTRIBUTING.md +8 -15
- package/README.md +39 -37
- package/agents/execution-reviewer.md +40 -0
- package/agents/reviewer.md +12 -3
- package/index.ts +55 -58
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +50 -60
- package/references/plan-artifact-template.md +81 -60
- package/references/state-and-config.md +60 -44
- package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
- package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
- package/scripts/run-tests.ts +12 -1
- package/scripts/validate.ts +39 -11
- package/skills/debug-and-plan/SKILL.md +3 -3
- package/skills/plan-big/SKILL.md +3 -3
- package/skills/plan-normal/SKILL.md +3 -3
- package/skills/plan-small/SKILL.md +4 -4
- package/skills/plan-with-refs/SKILL.md +6 -6
- package/skills/planning/SKILL.md +1 -1
- package/src/ask-form.ts +4 -4
- package/src/auditor.ts +227 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +8 -3
- package/src/code-graph/community.ts +1 -1
- package/src/code-graph/paths.ts +1 -1
- package/src/code-graph/watch.ts +2 -2
- package/src/compaction.ts +3 -3
- package/src/config-command.ts +146 -73
- package/src/dashboard.ts +303 -0
- package/src/exec.ts +1185 -924
- package/src/global-state.ts +304 -0
- package/src/guard.ts +18 -19
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +12 -70
- package/src/refine-ui-helpers.ts +24 -5
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +19 -3
- package/src/resume-command.ts +45 -129
- package/src/resume.ts +5 -1
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/staleness.ts +53 -0
- package/src/state.ts +273 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +223 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +7 -54
- package/src/workflow-state.ts +76 -58
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-schema.test.ts +0 -12
- package/tests/ask-choice.test.ts +2 -49
- package/tests/ask-form-tool.test.ts +4 -5
- package/tests/ask-form.test.ts +2 -2
- package/tests/auditor.test.ts +210 -0
- package/tests/auto-approve.test.ts +7 -10
- package/tests/autocomplete.test.ts +8 -11
- package/tests/code-graph-apply-action.test.ts +2 -2
- package/tests/code-graph-commands.test.ts +2 -2
- package/tests/code-graph-index.test.ts +2 -2
- package/tests/code-graph-loop.e2e.test.ts +1 -1
- package/tests/code-graph-mutations.test.ts +1 -1
- package/tests/code-graph-rollback.test.ts +1 -1
- package/tests/code-graph-v05.test.ts +2 -2
- package/tests/compaction.test.ts +1 -1
- package/tests/config-command.test.ts +103 -100
- package/tests/dashboard.test.ts +402 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec-review-loop.test.ts +331 -0
- package/tests/exec.test.ts +771 -1706
- package/tests/execute-plan.test.ts +44 -19
- package/tests/extension-load.test.ts +48 -0
- package/tests/global-state.test.ts +371 -0
- package/tests/graph-aware-file-tools.test.ts +5 -5
- package/tests/guard.test.ts +1 -1
- package/tests/multi-run.test.ts +3 -103
- package/tests/plan.test.ts +139 -62
- package/tests/plans.test.ts +7 -79
- package/tests/refine-prompts.test.ts +20 -71
- package/tests/refine-resume.test.ts +27 -22
- package/tests/refine-ui.test.ts +6 -15
- package/tests/resume-lifecycle.test.ts +41 -22
- package/tests/resume.test.ts +39 -81
- package/tests/role-panels.test.ts +391 -0
- package/tests/run-context.test.ts +1 -1
- package/tests/run-ownership.test.ts +1 -1
- package/tests/stale-ctx.test.ts +218 -0
- package/tests/staleness.test.ts +76 -0
- package/tests/state.test.ts +155 -32
- package/tests/subagent-thinking.test.ts +65 -0
- package/tests/subagent-usage.test.ts +1 -1
- package/tests/task-tool.test.ts +61 -0
- package/tests/tasks.test.ts +142 -0
- package/tests/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +73 -90
- package/tools/analyze-refs.ts +67 -32
- package/tools/ask-choice.ts +7 -53
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +55 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +41 -67
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- package/agents/executor.md +0 -26
- package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
- package/src/panel.ts +0 -473
- package/src/termination-prompt.ts +0 -73
- package/tests/goal-wait.test.ts +0 -269
- package/tests/panel-i-zero.test.ts +0 -420
- 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
|
-
}
|