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/dashboard.ts
ADDED
|
@@ -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
|
+
}
|