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/README.md +2 -2
- package/agents/execution-reviewer.md +62 -10
- package/package.json +2 -2
- package/references/pi-planning-workflow.md +16 -8
- package/references/state-and-config.md +2 -2
- package/src/auditor.ts +170 -21
- package/src/dashboard.ts +102 -19
- package/src/exec.ts +865 -107
- package/src/refine-ui.ts +21 -5
- package/src/resume-command.ts +26 -6
- package/src/review-budget.ts +290 -0
- package/src/tasks.ts +76 -0
- package/src/ui-language.ts +38 -0
- package/src/workflow-state.ts +217 -8
- package/tests/analyze-refs.test.ts +1 -1
- package/tests/auditor.test.ts +185 -1
- package/tests/dashboard.test.ts +165 -1
- package/tests/exec-review-loop.test.ts +1031 -10
- package/tests/exec.test.ts +32 -5
- package/tests/fixtures/lattice-code-blocked/plan-v2-trimmed.md +57 -0
- package/tests/fixtures/lattice-code-blocked/state.json +158 -0
- package/tests/refine-ui.test.ts +25 -2
- package/tests/resume.test.ts +45 -1
- package/tests/review-budget.test.ts +201 -0
- package/tests/tasks.test.ts +45 -0
- package/tests/workflow-state.test.ts +227 -0
- package/tools/analyze-refs.ts +17 -6
- package/tools/execute-plan.ts +12 -5
- package/tools/refine.ts +22 -3
package/tests/dashboard.test.ts
CHANGED
|
@@ -14,7 +14,7 @@ import * as assert from "node:assert/strict";
|
|
|
14
14
|
import { describe, it } from "node:test";
|
|
15
15
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
16
16
|
import { parsePlanTasks } from "../src/plan.ts";
|
|
17
|
-
import { buildTaskView } from "../src/tasks.ts";
|
|
17
|
+
import { buildTaskView, flattenTaskViews } from "../src/tasks.ts";
|
|
18
18
|
import { visibleWidth as localVisibleWidth } from "../src/refine-ui-helpers.ts";
|
|
19
19
|
import {
|
|
20
20
|
deriveDashboardModel,
|
|
@@ -186,6 +186,9 @@ describe("width invariant (TUI crash regression)", () => {
|
|
|
186
186
|
["paused", { ...base, paused: true, pausedReason: base.pausedReason as string }],
|
|
187
187
|
["all-terminal", { ...base, tasks: base.tasks.map((t) => ({ ...t, status: "complete" as const })) }],
|
|
188
188
|
["audit-failed", { ...base, auditRounds: 3, auditFailed: ["VC-1", "VC-2"] }],
|
|
189
|
+
// v0.9.2: the blocker row must keep the exact-width invariant too.
|
|
190
|
+
["blocked", { ...base, blockedTasks: ["Task-1", "Task-3", "Task-3.1"], blockedRound: 1 }],
|
|
191
|
+
["blocked-paused", { ...base, paused: true, pausedReason: "blocked: review round 2 cannot start — 1 task(s) reopened by round 1 still open (Task-3)", blockedTasks: ["Task-3"], blockedRound: 1 }],
|
|
189
192
|
];
|
|
190
193
|
};
|
|
191
194
|
|
|
@@ -400,3 +403,164 @@ describe("rolled-back task rendering", () => {
|
|
|
400
403
|
}
|
|
401
404
|
});
|
|
402
405
|
});
|
|
406
|
+
|
|
407
|
+
describe("findings visibility (v0.9)", () => {
|
|
408
|
+
const findings = [
|
|
409
|
+
{ id: "F-001", severity: "high", note: "union rollback missing", taskIds: ["Task-3"] },
|
|
410
|
+
{ id: "F-002", severity: "medium", note: "polish", taskIds: [] },
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
function withFindings(extra: { auditRounds?: number; reviewRunning?: boolean }) {
|
|
414
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {});
|
|
415
|
+
const checklist = [
|
|
416
|
+
{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: false },
|
|
417
|
+
{ id: "VC-002", text: "`VC-002` covers `Task-3`; pass condition: y", done: false },
|
|
418
|
+
];
|
|
419
|
+
return deriveDashboardModel("demo-run", tasks, checklist, {
|
|
420
|
+
startedAt: new Date().toISOString(),
|
|
421
|
+
findings,
|
|
422
|
+
auditRounds: extra.auditRounds ?? null,
|
|
423
|
+
reviewRunning: extra.reviewRunning ?? false,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
it("summary line shows review round/5 and the high count", () => {
|
|
428
|
+
const line = formatDashboardSummaryLine(withFindings({ auditRounds: 2 }));
|
|
429
|
+
assert.match(line, /review r2\/5/);
|
|
430
|
+
assert.match(line, /1 high/);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it("summary line omits the high token when only non-high findings remain", () => {
|
|
434
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {});
|
|
435
|
+
const m = deriveDashboardModel("demo-run", tasks, [], {
|
|
436
|
+
startedAt: new Date().toISOString(),
|
|
437
|
+
findings: [{ id: "F-002", severity: "medium", note: "polish", taskIds: [] }],
|
|
438
|
+
auditRounds: 3,
|
|
439
|
+
});
|
|
440
|
+
const line = formatDashboardSummaryLine(m);
|
|
441
|
+
assert.match(line, /review r3\/5/);
|
|
442
|
+
assert.doesNotMatch(line, /high/);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("compact panel renders the high-findings line in BOTH phases", () => {
|
|
446
|
+
// Non-terminal phase (the executor is repairing): the line must show.
|
|
447
|
+
const repairing = renderDashboardLines(withFindings({ auditRounds: 1 }), 80);
|
|
448
|
+
assert.ok(repairing.some((l) => /⚠.*high: F-001/.test(l)), "high findings visible while repairing");
|
|
449
|
+
|
|
450
|
+
// Terminal phase: still visible alongside the round counter.
|
|
451
|
+
const everyId = Object.fromEntries(
|
|
452
|
+
flattenTaskViews(buildTaskView(parsePlanTasks(PLAN), {})).map((t) => [t.id, { status: "complete" as const }]),
|
|
453
|
+
);
|
|
454
|
+
const allDone = buildTaskView(parsePlanTasks(PLAN), everyId);
|
|
455
|
+
const terminal = deriveDashboardModel("demo-run", allDone, [], {
|
|
456
|
+
startedAt: new Date().toISOString(),
|
|
457
|
+
findings,
|
|
458
|
+
auditRounds: 1,
|
|
459
|
+
});
|
|
460
|
+
const lines = renderDashboardLines(terminal, 80);
|
|
461
|
+
assert.ok(lines.some((l) => /⚠.*high: F-001/.test(l)));
|
|
462
|
+
assert.ok(lines.some((l) => /high finding\(s\) unresolved/.test(l)));
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("tree view lists findings with severity and mapping, and the verdict line names unresolved highs", () => {
|
|
466
|
+
const lines = renderDashboardTreeLines(withFindings({ auditRounds: 2 }), 100);
|
|
467
|
+
assert.ok(lines.some((l) => /⚠ F-001 \(high, Task-3\): union rollback missing/.test(l)));
|
|
468
|
+
assert.ok(lines.some((l) => /· F-002 \(medium\): polish/.test(l)));
|
|
469
|
+
assert.ok(lines.some((l) => /high findings unresolved: F-001/.test(l)));
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("blocked-review visibility (v0.9.2)", () => {
|
|
474
|
+
function withBlocker(extra: { paused?: boolean; pausedReason?: string } = {}) {
|
|
475
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {});
|
|
476
|
+
const checklist = [{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: false }];
|
|
477
|
+
return deriveDashboardModel("demo-run", tasks, checklist, {
|
|
478
|
+
startedAt: new Date().toISOString(),
|
|
479
|
+
auditRounds: 1,
|
|
480
|
+
auditFailed: ["VC-002"],
|
|
481
|
+
blockedTasks: ["Task-3", "Task-3.1"],
|
|
482
|
+
blockedRound: 1,
|
|
483
|
+
...extra,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
it("compact panel names the blocker and what to do about it", () => {
|
|
488
|
+
const lines = renderDashboardLines(withBlocker(), 100);
|
|
489
|
+
assert.ok(lines.some((l) => /⊘ blocked: Task-3, Task-3\.1 \(reopened by round 1\) — close with plans_update_task/.test(l)));
|
|
490
|
+
// Narrow widths keep the ids and drop the instruction.
|
|
491
|
+
const narrow = renderDashboardLines(withBlocker(), 40);
|
|
492
|
+
assert.ok(narrow.some((l) => /⊘ blocked: Task-3, Task-3\.1/.test(l)));
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it("the blocker row survives a paused run (the pause summary may be clipped)", () => {
|
|
496
|
+
const lines = renderDashboardLines(withBlocker({ paused: true, pausedReason: "blocked: review round 2 cannot start — 2 task(s) reopened by round 1 still open" }), 100);
|
|
497
|
+
assert.ok(lines.some((l) => /⏸ blocked: review round 2 cannot start/.test(l)));
|
|
498
|
+
assert.ok(lines.some((l) => /⊘ blocked: Task-3/.test(l)), "blocker row still lists the tasks");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("the summary line carries the blocked token only when blockers exist", () => {
|
|
502
|
+
assert.match(formatDashboardSummaryLine(withBlocker()), / · ⊘ blocked$/);
|
|
503
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {});
|
|
504
|
+
const clean = deriveDashboardModel("demo-run", tasks, [], { startedAt: new Date().toISOString(), auditRounds: 1 });
|
|
505
|
+
assert.doesNotMatch(formatDashboardSummaryLine(clean), /blocked/);
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe("review-budget visibility (v0.9.3)", () => {
|
|
510
|
+
function withBudget(extra: Record<string, unknown> = {}) {
|
|
511
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {});
|
|
512
|
+
const checklist = [{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: false }];
|
|
513
|
+
return deriveDashboardModel("demo-run", tasks, checklist, {
|
|
514
|
+
startedAt: new Date().toISOString(),
|
|
515
|
+
auditRounds: 2,
|
|
516
|
+
...extra,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
it("the summary line renders the chosen budget denominator", () => {
|
|
521
|
+
assert.match(formatDashboardSummaryLine(withBudget({ reviewBudget: 3 })), / · review r2\/3/);
|
|
522
|
+
assert.match(formatDashboardSummaryLine(withBudget({ reviewBudget: "unlimited" })), / · review r2\/∞/);
|
|
523
|
+
// A model without the field (legacy fixtures/checkpoints) reads as 5.
|
|
524
|
+
assert.match(formatDashboardSummaryLine(withBudget()), / · review r2\/5/);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it("the compact panel renders the budget denominator too", () => {
|
|
528
|
+
// The review row only renders once every task is terminal.
|
|
529
|
+
const terminal = (extra: Record<string, unknown>) => {
|
|
530
|
+
const tasks = buildTaskView(parsePlanTasks(PLAN), {
|
|
531
|
+
"Task-1": { status: "complete" },
|
|
532
|
+
"Task-2": { status: "complete" },
|
|
533
|
+
"Task-3": { status: "complete" },
|
|
534
|
+
"Task-3.1": { status: "complete" },
|
|
535
|
+
"Task-3.2": { status: "complete" },
|
|
536
|
+
});
|
|
537
|
+
const checklist = [{ id: "VC-001", text: "`VC-001` covers `Task-1`; pass condition: x", done: false }];
|
|
538
|
+
return deriveDashboardModel("demo-run", tasks, checklist, { startedAt: new Date().toISOString(), auditRounds: 2, ...extra });
|
|
539
|
+
};
|
|
540
|
+
const lines = renderDashboardLines(terminal({ reviewBudget: 3 }), 100);
|
|
541
|
+
assert.ok(lines.some((l) => /review: round 3\/3/.test(l)), "the numeric budget renders");
|
|
542
|
+
const unlimited = renderDashboardLines(terminal({ reviewBudget: "unlimited" }), 100);
|
|
543
|
+
assert.ok(unlimited.some((l) => /review: round 3\/∞/.test(l)), "the unlimited budget renders");
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
it("the expanded view carries the default annotation and the unlimited hard-cap progress", () => {
|
|
547
|
+
const defaulted = renderDashboardTreeLines(withBudget({ reviewBudget: 3, reviewBudgetDefaulted: true }), 120);
|
|
548
|
+
assert.ok(defaulted.some((l) => /Execution review: round 2\/3 \(default\)/.test(l)));
|
|
549
|
+
const userPicked = renderDashboardTreeLines(withBudget({ reviewBudget: 3 }), 120);
|
|
550
|
+
assert.ok(userPicked.some((l) => /Execution review: round 2\/3/.test(l)));
|
|
551
|
+
assert.equal(userPicked.some((l) => /\(default\)/.test(l)), false, "a user pick carries no annotation");
|
|
552
|
+
const unlimited = renderDashboardTreeLines(
|
|
553
|
+
withBudget({ reviewBudget: "unlimited", reviewRoundsTotal: 12, reviewCapExtension: 50 }),
|
|
554
|
+
120,
|
|
555
|
+
);
|
|
556
|
+
assert.ok(unlimited.some((l) => /Execution review: round 2\/∞ · cap 12\/100/.test(l)), "the run-cumulative cap progress renders");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("keeps the width invariant with the budget row present", () => {
|
|
560
|
+
const lines = renderDashboardLines(withBudget({ reviewBudget: "unlimited", reviewBudgetDefaulted: true }), 60);
|
|
561
|
+
for (const line of lines) {
|
|
562
|
+
assert.equal(localVisibleWidth(line), localVisibleWidth(line));
|
|
563
|
+
assert.ok(visibleWidth(line) <= 60, `line exceeds 60 columns: ${line}`);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
});
|