pi-plans 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dashboard.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import type { CheckItem } from "./plan.ts";
19
- import { REVIEW_MAX_ROUNDS } from "./auditor.ts";
19
+ import { LEGACY_REVIEW_MAX_ROUNDS, formatReviewBudget, unlimitedHardCapCeiling, type ReviewBudget } from "./review-budget.ts";
20
20
  import { truncateToWidth, visibleWidth } from "./refine-ui-helpers.ts";
21
21
  import {
22
22
  allTasksTerminal,
@@ -46,6 +46,24 @@ export interface DashboardModel {
46
46
  * never render `audit complete ✓` while this is set — the round-1 mis-cue
47
47
  * showed the tick for the whole duration of a running audit. */
48
48
  reviewRunning: boolean;
49
+ /** v0.9: unresolved findings from the newest committed review round
50
+ * (stable ids). High entries block completion; the rest are recorded. */
51
+ findings: Array<{ id: string; severity: string; note: string; taskIds: string[] }>;
52
+ /** v0.9.2: tasks that keep the review from starting — the newest failed
53
+ * round's rollback set intersected with the still-open tasks. Empty = the
54
+ * review is not blocked by open work. */
55
+ blockedTasks: string[];
56
+ /** Round that reopened those tasks (shown as `reopened by round N`). */
57
+ blockedRound: number | null;
58
+ /** v0.9.3: the per-run execution-review budget (rounds or `unlimited`).
59
+ * Null/absent = not chosen yet; `reviewBudgetDefaulted` marks the no-UI
60
+ * fallback so the expanded view can annotate it `(default)`. */
61
+ reviewBudget?: ReviewBudget | null;
62
+ /** v0.9.3: committed rounds across the whole run + granted extension —
63
+ * the unlimited budget's `n/50` progress. */
64
+ reviewRoundsTotal?: number;
65
+ reviewCapExtension?: number;
66
+ reviewBudgetDefaulted?: boolean;
49
67
  startedAt: string;
50
68
  usage: { inToks: number; outToks: number };
51
69
  }
@@ -172,22 +190,45 @@ export function renderDashboardLines(model: DashboardModel, width: number, theme
172
190
  // instead — the panel must look alive, never finished-then-silent.
173
191
  const owed = model.checklist.some((item) => !item.done);
174
192
  const nextRound = (model.auditRounds ?? 0) + 1;
193
+ const highCount = model.findings.filter((f) => f.severity === "high").length;
175
194
  const auditLine = model.reviewRunning
176
- ? `review: round ${nextRound}/${REVIEW_MAX_ROUNDS} running — read-only reviewer verifying`
195
+ ? `review: round ${nextRound}/${budgetLabelOf(model)} running — read-only reviewer verifying`
177
196
  : model.auditFailed.length > 0
178
197
  ? `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";
198
+ : highCount > 0
199
+ ? `review: round ${nextRound}/${budgetLabelOf(model)} — ${highCount} high finding(s) unresolved`
200
+ : model.auditUndeterminable.length > 0
201
+ ? `audit: ${model.auditUndeterminable.length} check(s) undeterminable — verdict unreadable`
202
+ : owed
203
+ ? `review: round ${nextRound}/${budgetLabelOf(model)} — verdict pending`
204
+ : model.auditRounds !== null
205
+ ? "audit complete ✓"
206
+ : "all tasks terminal — audit pending";
186
207
  lines.push(boxRow("│", ` ${clip(auditLine, inner - 2)}`, " ", width));
187
208
  }
209
+ // v0.9.2: the blocker row renders in BOTH phases — a paused run sees it
210
+ // below the pause summary (which may be clipped at narrow widths), and a
211
+ // live run sees exactly what keeps the review from starting.
212
+ if (model.blockedTasks.length > 0) {
213
+ const provenance = model.blockedRound !== null ? ` (reopened by round ${model.blockedRound})` : "";
214
+ // Provenance once per row keeps the id list readable and short enough to
215
+ // survive a 100-column panel without dropping the instruction.
216
+ const rowText = narrow
217
+ ? `⊘ blocked: ${model.blockedTasks.join(", ")}${provenance}`
218
+ : `⊘ blocked: ${model.blockedTasks.join(", ")}${provenance} — close with plans_update_task`;
219
+ lines.push(boxRow("│", ` ${clip(rowText, inner - 2)}`, " ", width));
220
+ }
188
221
  if (!narrow && model.auditFailed.length > 0) {
189
222
  lines.push(boxRow("│", ` ✗ ${clip(model.auditFailed.join(", "), inner - 3)}`, " ", width));
190
223
  }
224
+ // v0.9: unresolved findings render in both phases — the fix loop's whole
225
+ // point is that the executor sees what it owes while repairing.
226
+ const highFindings = model.findings.filter((f) => f.severity === "high");
227
+ if (!narrow && highFindings.length > 0) {
228
+ lines.push(boxRow("│", ` ⚠ ${clip(`high: ${highFindings.map((f) => f.id).join(", ")}`, inner - 3)}`, " ", width));
229
+ } else if (!narrow && model.findings.length > 0) {
230
+ lines.push(boxRow("│", ` · ${clip(`findings: ${model.findings.map((f) => f.id).join(", ")}`, inner - 3)}`, " ", width));
231
+ }
191
232
  if (!narrow && model.auditUndeterminable.length > 0) {
192
233
  lines.push(boxRow("│", ` ? ${clip(model.auditUndeterminable.join(", "), inner - 3)}`, " ", width));
193
234
  }
@@ -206,6 +247,22 @@ export function renderDashboardLines(model: DashboardModel, width: number, theme
206
247
  return clampLines(painted, width);
207
248
  }
208
249
 
250
+ /** v0.9.3: the budget denominator as shown in every review line (`3`, `∞`).
251
+ * A model without the field (legacy fixtures) reads as the legacy 5. */
252
+ function budgetLabelOf(model: DashboardModel): string {
253
+ return formatReviewBudget(model.reviewBudget ?? LEGACY_REVIEW_MAX_ROUNDS);
254
+ }
255
+
256
+ /** v0.9.3: unlimited budgets render their run-cumulative hard-cap progress. */
257
+ function hardCapOf(model: DashboardModel): string | null {
258
+ if ((model.reviewBudget ?? null) !== "unlimited") return null;
259
+ const ceiling = unlimitedHardCapCeiling({
260
+ reviewRoundsTotal: model.reviewRoundsTotal ?? 0,
261
+ reviewCapExtension: model.reviewCapExtension ?? 0,
262
+ });
263
+ return `${model.reviewRoundsTotal ?? 0}/${ceiling}`;
264
+ }
265
+
209
266
  function progressBar(done: number, total: number, width = 12): string {
210
267
  if (total <= 0) return "";
211
268
  const filled = Math.round((done / total) * width);
@@ -216,7 +273,7 @@ export function deriveDashboardModel(
216
273
  topic: string,
217
274
  tasks: TaskView[],
218
275
  checklist: CheckItem[],
219
- extra?: { paused?: boolean; pausedReason?: string; auditRounds?: number | null; auditFailed?: string[]; auditUndeterminable?: string[]; reviewRunning?: boolean; startedAt?: string; usage?: { inToks: number; outToks: number } },
276
+ extra?: { paused?: boolean; pausedReason?: string; auditRounds?: number | null; auditFailed?: string[]; auditUndeterminable?: string[]; reviewRunning?: boolean; findings?: Array<{ id: string; severity: string; note: string; taskIds: string[] }>; blockedTasks?: string[]; blockedRound?: number | null; reviewBudget?: ReviewBudget | null; reviewRoundsTotal?: number; reviewCapExtension?: number; reviewBudgetDefaulted?: boolean; startedAt?: string; usage?: { inToks: number; outToks: number } },
220
277
  ): DashboardModel {
221
278
  return {
222
279
  topic,
@@ -228,6 +285,13 @@ export function deriveDashboardModel(
228
285
  auditFailed: extra?.auditFailed ?? [],
229
286
  auditUndeterminable: extra?.auditUndeterminable ?? [],
230
287
  reviewRunning: extra?.reviewRunning ?? false,
288
+ findings: extra?.findings ?? [],
289
+ blockedTasks: extra?.blockedTasks ?? [],
290
+ blockedRound: extra?.blockedRound ?? null,
291
+ reviewBudget: extra?.reviewBudget ?? null,
292
+ reviewRoundsTotal: extra?.reviewRoundsTotal ?? 0,
293
+ reviewCapExtension: extra?.reviewCapExtension ?? 0,
294
+ reviewBudgetDefaulted: extra?.reviewBudgetDefaulted ?? false,
231
295
  startedAt: extra?.startedAt ?? new Date().toISOString(),
232
296
  usage: extra?.usage ?? { inToks: 0, outToks: 0 },
233
297
  };
@@ -239,9 +303,15 @@ export function formatDashboardSummaryLine(model: DashboardModel): string {
239
303
  const vcDone = model.checklist.filter((item) => item.done).length;
240
304
  const cur = currentTask(model.tasks);
241
305
  const wave = cur ? ` · wave ${cur.wave}` : "";
242
- const audit = model.auditRounds !== null ? ` · audit r${model.auditRounds}` : "";
306
+ // v0.9: the review token carries the budget denominator and the unresolved
307
+ // high count — visible in BOTH phases (executing repair and verifying), so
308
+ // convergence is legible exactly while the executor is fixing.
309
+ const highs = model.findings.filter((f) => f.severity === "high").length;
310
+ const audit = model.auditRounds !== null ? ` · review r${model.auditRounds}/${budgetLabelOf(model)}` : "";
311
+ const highToken = highs > 0 ? ` · ${highs} high` : "";
243
312
  const pause = model.paused ? " · ⏸ paused" : "";
244
- return `plans: ${model.topic} ▸ tasks ${p.done}/${p.total} · VC ${vcDone}/${model.checklist.length}${wave}${audit}${pause}`;
313
+ const blocked = model.blockedTasks.length > 0 ? " · ⊘ blocked" : "";
314
+ return `plans: ${model.topic} ▸ tasks ${p.done}/${p.total} · VC ${vcDone}/${model.checklist.length}${wave}${audit}${highToken}${pause}${blocked}`;
245
315
  }
246
316
 
247
317
  /** Expanded tree view lines (Ctrl+Shift+T overlay). Wide layout from 96 cols. */
@@ -266,24 +336,37 @@ export function renderDashboardTreeLines(model: DashboardModel, width: number, t
266
336
  if (task.status === "pending" && task.evidence) line += ` ↺${clip(task.evidence, 40)}`;
267
337
  lines.push(line);
268
338
  for (const child of task.children) row(child, depth + 1);
269
- }; for (const task of model.tasks) row(task, 0);
339
+ };
340
+ for (const task of model.tasks) row(task, 0);
270
341
  lines.push("");
271
342
  lines.push("Verification checks:");
272
343
  for (const item of model.checklist) {
273
344
  const mark = model.auditFailed.includes(item.id) ? "✗" : item.done ? "☑" : "☐";
274
345
  lines.push(` ${mark} ${item.id}${wide ? ` ${clip(item.text.split(";")[1] ?? item.text, Math.min(80, width))}` : ""}`);
275
346
  }
347
+ if (model.findings.length > 0) {
348
+ lines.push("");
349
+ lines.push("Findings (stable ids; absence from the newest round = resolved):");
350
+ for (const f of model.findings) {
351
+ const mark = f.severity === "high" ? "⚠" : f.severity === "malformed" ? "?" : "·";
352
+ lines.push(` ${mark} ${f.id} (${f.severity}${f.taskIds.length ? `, ${f.taskIds.join(", ")}` : ""}): ${wide ? clip(f.note, 72) : clip(f.note, 40)}`);
353
+ }
354
+ }
355
+ const budgetNote = `${budgetLabelOf(model)}${model.reviewBudgetDefaulted ? " (default)" : ""}${hardCapOf(model) ? ` · cap ${hardCapOf(model)}` : ""}`;
276
356
  if (model.reviewRunning) {
277
357
  lines.push("");
278
- lines.push(`Execution review: round ${(model.auditRounds ?? 0) + 1}/${REVIEW_MAX_ROUNDS} running`);
358
+ lines.push(`Execution review: round ${(model.auditRounds ?? 0) + 1}/${budgetNote} running`);
279
359
  } else if (model.auditRounds !== null) {
280
360
  lines.push("");
361
+ const highIds = model.findings.filter((f) => f.severity === "high").map((f) => f.id);
281
362
  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}`);
363
+ ? ` — failed: ${model.auditFailed.join(", ")}${highIds.length > 0 ? `; high: ${highIds.join(", ")}` : ""}`
364
+ : highIds.length > 0
365
+ ? ` — high findings unresolved: ${highIds.join(", ")}`
366
+ : model.auditUndeterminable.length > 0
367
+ ? ` — undeterminable: ${model.auditUndeterminable.join(", ")}`
368
+ : " — passed ✓";
369
+ lines.push(`Execution review: round ${model.auditRounds}/${budgetNote}${verdict}`);
287
370
  }
288
371
  const painted = theme ? lines.map((line) => theme.fg("muted", line)) : lines;
289
372
  return clampLines(painted, width);