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/tasks.test.ts
CHANGED
|
@@ -7,7 +7,9 @@ import { describe, it } from "node:test";
|
|
|
7
7
|
import type { CheckItem } from "../src/plan.ts";
|
|
8
8
|
import {
|
|
9
9
|
auditRollbackSet,
|
|
10
|
+
blockedReviewTasks,
|
|
10
11
|
buildTaskView,
|
|
12
|
+
rollbackCoverageIds,
|
|
11
13
|
invalidateChecksForRolledBackTasks,
|
|
12
14
|
taskProgressMap,
|
|
13
15
|
type TaskProgressMap,
|
|
@@ -113,6 +115,49 @@ describe("progress persistence", () => {
|
|
|
113
115
|
});
|
|
114
116
|
});
|
|
115
117
|
|
|
118
|
+
describe("blocked review tasks", () => {
|
|
119
|
+
it("reports every non-terminal member of the authoritative rollback set in tree order", () => {
|
|
120
|
+
// Task-3 is complete but its child is open, so the parent is not
|
|
121
|
+
// terminal either; both must surface as blockers.
|
|
122
|
+
const tasks = view({ "Task-1": { status: "complete" }, "Task-3": { status: "complete" } });
|
|
123
|
+
assert.deepEqual(
|
|
124
|
+
blockedReviewTasks(tasks, ["Task-1", "Task-2", "Task-3", "Task-3.1"]),
|
|
125
|
+
["Task-2", "Task-3", "Task-3.1"],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("reports a task that was already pending, unlike the flip-reporting rollback helper", () => {
|
|
130
|
+
// The rollback helpers report only nodes they flipped; a task that was
|
|
131
|
+
// pending before the round must still count as blocking work.
|
|
132
|
+
const tasks = view({ "Task-1": { status: "pending" } });
|
|
133
|
+
assert.deepEqual(auditRollbackSet(tasks, checks(), "VC-001"), []);
|
|
134
|
+
assert.deepEqual(blockedReviewTasks(tasks, ["Task-1"]), ["Task-1"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("ignores terminal tasks and ids outside the rollback set", () => {
|
|
138
|
+
const tasks = view(ALL_DONE);
|
|
139
|
+
assert.deepEqual(blockedReviewTasks(tasks, ["Task-1", "Task-3"]), []);
|
|
140
|
+
assert.deepEqual(blockedReviewTasks(tasks, []), []);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("computes a failed round's coverage (pre-feature backfill) without mutating the tree", () => {
|
|
144
|
+
// VC-002 covers Task-2 and Task-3; the cascade pulls Task-3.1 in.
|
|
145
|
+
const tasks = view(ALL_DONE);
|
|
146
|
+
const before = JSON.stringify(tasks);
|
|
147
|
+
assert.deepEqual(rollbackCoverageIds(tasks, checks(), ["VC-002"]), ["Task-2", "Task-3", "Task-3.1"]);
|
|
148
|
+
assert.deepEqual(rollbackCoverageIds(tasks, checks(), ["VC-003"]), ["Task-3.1"]);
|
|
149
|
+
assert.deepEqual(rollbackCoverageIds(tasks, checks(), ["VC-999"]), []);
|
|
150
|
+
assert.equal(JSON.stringify(tasks), before, "coverage computation never reopens anything");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("never mutates status, evidence, or skipReason", () => {
|
|
154
|
+
const tasks = view({ "Task-2": { status: "pending", evidence: "previous attempt" }, "Task-3": { status: "skipped", skipReason: "covered by Task-2" } });
|
|
155
|
+
const before = JSON.stringify(tasks);
|
|
156
|
+
blockedReviewTasks(tasks, ["Task-1", "Task-2", "Task-3", "Task-3.1"]);
|
|
157
|
+
assert.equal(JSON.stringify(tasks), before);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
116
161
|
describe("done-flag invalidation", () => {
|
|
117
162
|
it("clears done on checks whose covered tasks were reopened", () => {
|
|
118
163
|
// VC-002 covers Task-2 and Task-3; its rollback reopens those, so a
|
|
@@ -482,4 +482,231 @@ describe("blocking budget persistence", () => {
|
|
|
482
482
|
const loaded = loadCheckpoint(workdir, runId);
|
|
483
483
|
assert.equal(loaded.status, "corrupt", "the closed schema is still enforced");
|
|
484
484
|
});
|
|
485
|
+
// v0.9: findings ride the same audit slot — same persistence rules.
|
|
486
|
+
|
|
487
|
+
it("round-trips findings through the checkpoint validator", () => {
|
|
488
|
+
const { workdir, runId } = setupRun("findings-roundtrip");
|
|
489
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), {
|
|
490
|
+
audit: {
|
|
491
|
+
rounds: 2,
|
|
492
|
+
lastResult: "highs: F-001",
|
|
493
|
+
findings: [
|
|
494
|
+
{ id: "F-001", severity: "high", taskIds: ["Task-2"], note: "broken", evidence: "src/b.ts", raw: "raw line" },
|
|
495
|
+
{ id: "F-002", severity: "medium", taskIds: [], proposedTask: "tidy", note: "polish", evidence: "e", raw: "r" },
|
|
496
|
+
],
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
500
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
501
|
+
assert.ok(loaded.status === "ok");
|
|
502
|
+
assert.deepEqual(loaded.checkpoint.execution?.audit?.findings, [
|
|
503
|
+
{ id: "F-001", severity: "high", taskIds: ["Task-2"], note: "broken", evidence: "src/b.ts", raw: "raw line" },
|
|
504
|
+
{ id: "F-002", severity: "medium", taskIds: [], proposedTask: "tidy", note: "polish", evidence: "e", raw: "r" },
|
|
505
|
+
]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("loads a checkpoint written before findings existed (absent, not corrupt)", () => {
|
|
509
|
+
const { workdir, runId } = setupRun("findings-legacy");
|
|
510
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { audit: { rounds: 1, lastResult: "VC-001" } });
|
|
511
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
512
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
513
|
+
assert.ok(loaded.status === "ok", "legacy checkpoint still loads");
|
|
514
|
+
assert.equal(loaded.checkpoint.execution?.audit?.findings, undefined);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("rejects unknown keys inside a findings record", () => {
|
|
518
|
+
const { workdir, runId } = setupRun("findings-bad-record");
|
|
519
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), {
|
|
520
|
+
audit: { rounds: 1, findings: [{ id: "F-001", severity: "high", taskIds: [], note: "n", evidence: "e", raw: "r", bogus: true } as never] },
|
|
521
|
+
});
|
|
522
|
+
// Validation runs at write: a malformed findings record never lands.
|
|
523
|
+
assert.throws(() => mutateCheckpoint(workdir, runId, () => cp), /findings\.0: unexpected key/);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe("blocked-point persistence (v0.9.2)", () => {
|
|
528
|
+
function executingCheckpoint(workdir: string, runId: string) {
|
|
529
|
+
createCheckpoint(workdir, { runId, originWorkdir: workdir, workdir });
|
|
530
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
531
|
+
fs.writeFileSync(planPath, "# PLAN_v1 - blocked demo\n", "utf8");
|
|
532
|
+
const plan = planIdentityOf(planPath, 1);
|
|
533
|
+
let cp = applyPlanWritten(baseCheckpoint(workdir, runId), plan);
|
|
534
|
+
cp = { ...cp, nextAction: "accept-execute" };
|
|
535
|
+
cp = applyExecutionApproved(cp, { plan, worktree: cp.worktreeRoot, headAtApproval: null, approvedAt: cp.updatedAt });
|
|
536
|
+
return cp;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const BLOCKED = {
|
|
540
|
+
rolledBack: ["Task-2", "Task-2.1", "Task-7"],
|
|
541
|
+
tasks: ["Task-2"],
|
|
542
|
+
round: 1,
|
|
543
|
+
escalatedRounds: 0,
|
|
544
|
+
since: "2026-10-02T10:32:00Z",
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
it("round-trips the blocker and its escalation counter", () => {
|
|
548
|
+
const { workdir, runId } = setupRun("blocked-roundtrip");
|
|
549
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { blocked: BLOCKED });
|
|
550
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
551
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
552
|
+
assert.ok(loaded.status === "ok");
|
|
553
|
+
assert.deepEqual(loaded.checkpoint.execution?.blocked, BLOCKED);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("null clears the blocker (delete-on-null, like pausedReason)", () => {
|
|
557
|
+
const { workdir, runId } = setupRun("blocked-null");
|
|
558
|
+
let cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { blocked: BLOCKED });
|
|
559
|
+
cp = applyExecutionProgress(cp, { blocked: null });
|
|
560
|
+
assert.equal(cp.execution?.blocked, undefined);
|
|
561
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
562
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
563
|
+
assert.ok(loaded.status === "ok");
|
|
564
|
+
assert.equal(loaded.checkpoint.execution?.blocked, undefined);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
it("loads a checkpoint written before the field existed", () => {
|
|
568
|
+
const { workdir, runId } = setupRun("blocked-legacy");
|
|
569
|
+
const cp = executingCheckpoint(workdir, runId);
|
|
570
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
571
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
572
|
+
assert.ok(loaded.status === "ok", "legacy checkpoint still loads");
|
|
573
|
+
assert.equal(loaded.checkpoint.execution?.blocked, undefined);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("rejects unknown keys inside the blocker record", () => {
|
|
577
|
+
const { workdir, runId } = setupRun("blocked-bad-record");
|
|
578
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), {
|
|
579
|
+
blocked: { ...BLOCKED, bogus: true } as never,
|
|
580
|
+
});
|
|
581
|
+
assert.throws(() => mutateCheckpoint(workdir, runId, () => cp), /blocked: unexpected key/);
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("a stop revokes the blocker (stale rollback authorization)", () => {
|
|
585
|
+
const { workdir, runId } = setupRun("blocked-stopped");
|
|
586
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { blocked: BLOCKED });
|
|
587
|
+
const stopped = applyExecutionStopped(cp, "user stop");
|
|
588
|
+
assert.equal(stopped.execution?.blocked, undefined);
|
|
589
|
+
assert.equal(stopped.execution?.pausedReason, "user stop");
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it("completion clears the blocker", () => {
|
|
593
|
+
const { workdir, runId } = setupRun("blocked-completed");
|
|
594
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { blocked: BLOCKED });
|
|
595
|
+
assert.equal(applyExecutionCompleted(cp).execution?.blocked, undefined);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
describe("review-budget persistence (v0.9.3)", () => {
|
|
600
|
+
function executingCheckpoint(workdir: string, runId: string) {
|
|
601
|
+
createCheckpoint(workdir, { runId, originWorkdir: workdir, workdir });
|
|
602
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
603
|
+
fs.writeFileSync(planPath, "# PLAN_v1 - budget demo\n", "utf8");
|
|
604
|
+
const plan = planIdentityOf(planPath, 1);
|
|
605
|
+
let cp = applyPlanWritten(baseCheckpoint(workdir, runId), plan);
|
|
606
|
+
cp = { ...cp, nextAction: "accept-execute" };
|
|
607
|
+
cp = applyExecutionApproved(cp, { plan, worktree: cp.worktreeRoot, headAtApproval: null, approvedAt: cp.updatedAt });
|
|
608
|
+
return cp;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const BUDGET = {
|
|
612
|
+
reviewBudget: "unlimited" as const,
|
|
613
|
+
reviewBudgetDefaulted: true,
|
|
614
|
+
reviewRoundsTotal: 7,
|
|
615
|
+
reviewCapExtension: 50,
|
|
616
|
+
reviewNoProgress: { key: "VC-002|VC-001|F-001", streak: 2 },
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
it("round-trips the budget, its counters, and the no-progress state", () => {
|
|
620
|
+
const { workdir, runId } = setupRun("budget-roundtrip");
|
|
621
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), BUDGET);
|
|
622
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
623
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
624
|
+
assert.ok(loaded.status === "ok");
|
|
625
|
+
assert.equal(loaded.checkpoint.execution?.reviewBudget, "unlimited");
|
|
626
|
+
assert.equal(loaded.checkpoint.execution?.reviewBudgetDefaulted, true);
|
|
627
|
+
assert.equal(loaded.checkpoint.execution?.reviewRoundsTotal, 7);
|
|
628
|
+
assert.equal(loaded.checkpoint.execution?.reviewCapExtension, 50);
|
|
629
|
+
assert.deepEqual(loaded.checkpoint.execution?.reviewNoProgress, { key: "VC-002|VC-001|F-001", streak: 2 });
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("accepts a numeric budget and rejects malformed values with the key name", () => {
|
|
633
|
+
const { workdir, runId } = setupRun("budget-validation");
|
|
634
|
+
const base = executingCheckpoint(workdir, runId);
|
|
635
|
+
assert.equal(applyExecutionProgress(base, { reviewBudget: 1 }).execution?.reviewBudget, 1);
|
|
636
|
+
// The write path stores what it is given (like every other progress
|
|
637
|
+
// field); the READ path validates and names the offending key.
|
|
638
|
+
for (const bad of [0, -1, 2.5, true, { rounds: 3 }]) {
|
|
639
|
+
const cp = applyExecutionProgress(base, { reviewBudget: bad as never });
|
|
640
|
+
assert.throws(
|
|
641
|
+
() => mutateCheckpoint(workdir, runId, () => cp),
|
|
642
|
+
/reviewBudget/,
|
|
643
|
+
`reviewBudget ${JSON.stringify(bad)} rejected`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
assert.throws(() => mutateCheckpoint(workdir, runId, () => applyExecutionProgress(base, { reviewBudget: "infinite" as never })), /reviewBudget/);
|
|
647
|
+
assert.throws(() => mutateCheckpoint(workdir, runId, () => applyExecutionProgress(base, { reviewRoundsTotal: -1 })), /reviewRoundsTotal/);
|
|
648
|
+
assert.throws(() => mutateCheckpoint(workdir, runId, () => applyExecutionProgress(base, { reviewCapExtension: -3 })), /reviewCapExtension/);
|
|
649
|
+
assert.throws(
|
|
650
|
+
() => mutateCheckpoint(workdir, runId, () => applyExecutionProgress(base, { reviewNoProgress: { key: "x" } as never })),
|
|
651
|
+
/reviewNoProgress/,
|
|
652
|
+
);
|
|
653
|
+
assert.throws(
|
|
654
|
+
() => mutateCheckpoint(workdir, runId, () => applyExecutionProgress(base, { reviewNoProgress: { key: "x", streak: -1 } })),
|
|
655
|
+
/reviewNoProgress.streak/,
|
|
656
|
+
);
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
it("null clears the budget and the valve state (delete-on-null)", () => {
|
|
660
|
+
const { workdir, runId } = setupRun("budget-null");
|
|
661
|
+
let cp = applyExecutionProgress(executingCheckpoint(workdir, runId), BUDGET);
|
|
662
|
+
cp = applyExecutionProgress(cp, { reviewBudget: null, reviewBudgetDefaulted: null, reviewNoProgress: null });
|
|
663
|
+
assert.equal(cp.execution?.reviewBudget, undefined);
|
|
664
|
+
assert.equal(cp.execution?.reviewBudgetDefaulted, undefined);
|
|
665
|
+
assert.equal(cp.execution?.reviewNoProgress, undefined);
|
|
666
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
667
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
668
|
+
assert.ok(loaded.status === "ok");
|
|
669
|
+
assert.equal(loaded.checkpoint.execution?.reviewBudget, undefined);
|
|
670
|
+
assert.equal(loaded.checkpoint.execution?.reviewNoProgress, undefined);
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it("loads a checkpoint written before the fields existed (absent, not corrupt)", () => {
|
|
674
|
+
const { workdir, runId } = setupRun("budget-legacy");
|
|
675
|
+
const cp = applyExecutionProgress(executingCheckpoint(workdir, runId), { reviewBudget: null });
|
|
676
|
+
mutateCheckpoint(workdir, runId, () => cp);
|
|
677
|
+
const raw = JSON.parse(fs.readFileSync(checkpointFilePath(workdir, runId), "utf8"));
|
|
678
|
+
assert.equal(raw.execution.reviewBudget, undefined);
|
|
679
|
+
const loaded = loadCheckpoint(workdir, runId);
|
|
680
|
+
assert.ok(loaded.status === "ok");
|
|
681
|
+
assert.equal(loaded.checkpoint.execution?.reviewBudget, undefined);
|
|
682
|
+
// The live state resolves a legacy shape to the 5-round bound; that
|
|
683
|
+
// resolution is exercised in tests/review-budget.test.ts.
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it("a stop and a completion both drop the budget and its valve state", () => {
|
|
687
|
+
const { workdir, runId } = setupRun("budget-cleanup");
|
|
688
|
+
const withBudget = applyExecutionProgress(executingCheckpoint(workdir, runId), BUDGET);
|
|
689
|
+
const stopped = applyExecutionStopped(withBudget, "user stop");
|
|
690
|
+
assert.equal(stopped.execution?.reviewBudget, undefined);
|
|
691
|
+
assert.equal(stopped.execution?.reviewRoundsTotal, undefined);
|
|
692
|
+
assert.equal(stopped.execution?.reviewCapExtension, undefined);
|
|
693
|
+
assert.equal(stopped.execution?.reviewNoProgress, undefined);
|
|
694
|
+
const completed = applyExecutionCompleted(withBudget);
|
|
695
|
+
assert.equal(completed.execution?.reviewBudget, undefined);
|
|
696
|
+
assert.equal(completed.execution?.reviewRoundsTotal, undefined);
|
|
697
|
+
assert.equal(completed.execution?.reviewNoProgress, undefined);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("a migration preserves the budget but resets the invalidated execution state", () => {
|
|
701
|
+
const { workdir, runId } = setupRun("budget-migration");
|
|
702
|
+
const migrated = applyMigration(applyExecutionProgress(executingCheckpoint(workdir, runId), BUDGET), {
|
|
703
|
+
workdir,
|
|
704
|
+
worktreeRoot: workdir,
|
|
705
|
+
commonDir: path.join(workdir, ".git"),
|
|
706
|
+
});
|
|
707
|
+
assert.equal(migrated.execution?.reviewBudget, "unlimited");
|
|
708
|
+
assert.equal(migrated.execution?.reviewRoundsTotal, 7);
|
|
709
|
+
assert.deepEqual(migrated.execution?.tasks, {}, "task progress still does not migrate");
|
|
710
|
+
assert.equal(migrated.execution?.audit?.rounds, 0, "audit rounds still restart at 0");
|
|
711
|
+
});
|
|
485
712
|
});
|
package/tools/analyze-refs.ts
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
|
|
16
16
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
17
17
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
-
import { truncateHead } from "@earendil-works/pi-coding-agent";
|
|
19
18
|
import { Text } from "@earendil-works/pi-tui";
|
|
20
19
|
import { Type } from "typebox";
|
|
21
20
|
import * as fs from "node:fs";
|
|
@@ -37,6 +36,8 @@ import { resolveActiveRun } from "../src/run-context.ts";
|
|
|
37
36
|
import { buildRefAnalystTask, type RefAnalystTaskInput } from "../src/refine-prompts.ts";
|
|
38
37
|
import { runPiSubagent, stripFrontmatter } from "../src/subagent.ts";
|
|
39
38
|
import { RefineOverlayController, refineOverlayContext } from "../src/refine-ui.ts";
|
|
39
|
+
import { matchesTerminalKey } from "../src/terminal-keys.ts";
|
|
40
|
+
import { toggleDashboardExpanded } from "../src/exec.ts";
|
|
40
41
|
import { resolveUiLanguage } from "../src/ui-language.ts";
|
|
41
42
|
|
|
42
43
|
const BATCH_SIZE = 3;
|
|
@@ -80,7 +81,7 @@ async function ensureRefAnalystModelReady(
|
|
|
80
81
|
): Promise<{ mode: string; model_selector: string | null; thinking_level: string | null; confirmed_at: string | null; name_prefix: string }> {
|
|
81
82
|
if (role.confirmed_at !== null && role.model_selector !== null) return role as never;
|
|
82
83
|
let outcome = await runFirstUseFlow(host, role.thinking_level);
|
|
83
|
-
if (outcome.status === "confirmed" &&
|
|
84
|
+
if (outcome.status === "confirmed" && availableModels(host).length > 0 && findModel(host, outcome.modelSelector) === null) {
|
|
84
85
|
// F-008: a manually entered selector that the registry does not know —
|
|
85
86
|
// one re-pick, then let spawn-side errors surface precisely.
|
|
86
87
|
outcome = await runFirstUseFlow(host, outcome.role.thinking_level);
|
|
@@ -226,7 +227,16 @@ export function registerAnalyzeRefsTool(ext: ExtensionAPI, baseDir: string): voi
|
|
|
226
227
|
|
|
227
228
|
const overlay =
|
|
228
229
|
ctx.mode === "tui"
|
|
229
|
-
? new RefineOverlayController(
|
|
230
|
+
? new RefineOverlayController(
|
|
231
|
+
"refs",
|
|
232
|
+
batch.map((job) => ({ id: job.laneId, label: job.laneId })),
|
|
233
|
+
relayAbort,
|
|
234
|
+
resolveUiLanguage(workdir),
|
|
235
|
+
// Forward the dashboard toggle (no key bubbling in pi-tui).
|
|
236
|
+
(data) => {
|
|
237
|
+
if (matchesTerminalKey(data, "ctrl+shift+t")) toggleDashboardExpanded(ctx);
|
|
238
|
+
},
|
|
239
|
+
)
|
|
230
240
|
: undefined;
|
|
231
241
|
overlay?.open(refineOverlayContext(ctx), modelLabel);
|
|
232
242
|
try {
|
|
@@ -264,9 +274,10 @@ export function registerAnalyzeRefsTool(ext: ExtensionAPI, baseDir: string): voi
|
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
const combined = sections.join("\n\n---\n\n");
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
// v0.8.1: no head-truncation — the full combined analysis flows into the
|
|
278
|
+
// tool result (and REF_ANALYSIS.md) verbatim; the previous 2000-line/
|
|
279
|
+
// 50KB cap silently dropped the tail of large reference analyses.
|
|
280
|
+
const text = combined;
|
|
270
281
|
|
|
271
282
|
return {
|
|
272
283
|
content: [
|
package/tools/execute-plan.ts
CHANGED
|
@@ -157,19 +157,26 @@ export async function executeCommand(ctx: ExtensionContext, planPathArg?: string
|
|
|
157
157
|
const activeExecution = getExecution();
|
|
158
158
|
const planPath = planPathArg ? path.resolve(ctx.cwd, planPathArg.replace(/^@/, "")) : activeExecution?.planPath;
|
|
159
159
|
if (activeExecution && planPath && path.resolve(activeExecution.planPath) === path.resolve(planPath)) {
|
|
160
|
-
|
|
160
|
+
// v0.9.3: the resume is async because a review-pause grant re-opens the
|
|
161
|
+
// budget picker; the message below is produced AFTER that panel resolves.
|
|
162
|
+
const resumed = await resumeActiveExecution(ctx);
|
|
161
163
|
// v0.8 phase-aware response: a verifying run continues its review loop
|
|
162
|
-
// (this tool is also the ONLY budget-granting surface at a
|
|
164
|
+
// (this tool is also the ONLY budget-granting surface at a review pause).
|
|
163
165
|
const statusText = activeExecution.review?.inFlight
|
|
164
166
|
? "Execution review in progress (status verifying); the reviewer round runs in the overlay."
|
|
165
167
|
: (getExecution()?.stall.paused ?? false)
|
|
166
|
-
? "Execution review paused
|
|
167
|
-
:
|
|
168
|
+
? "Execution review paused — run /plans-execute and pick a round budget to continue."
|
|
169
|
+
: resumed.grantedBudget !== undefined
|
|
170
|
+
? `Review budget granted (${resumed.grantedBudget === "unlimited" ? "unlimited" : `${resumed.grantedBudget} rounds`}); the review resumes now.`
|
|
171
|
+
: "Execution resumed; task progress preserved.";
|
|
172
|
+
const declined = resumed.budgetDeclined === true;
|
|
168
173
|
return {
|
|
169
174
|
status: "executing",
|
|
170
175
|
planPath,
|
|
171
176
|
itemCount: activeExecution.items.length,
|
|
172
|
-
message:
|
|
177
|
+
message: declined
|
|
178
|
+
? "Review budget unchanged — the run stays paused. Call /plans-execute again and pick a round count to continue."
|
|
179
|
+
: resumed.resumed ? statusText : "This plan is already executing.",
|
|
173
180
|
};
|
|
174
181
|
}
|
|
175
182
|
return executeHandoff(ctx, planPathArg);
|
package/tools/refine.ts
CHANGED
|
@@ -44,6 +44,8 @@ import { buildReviewerTask, reviewerLanes } from "../src/refine-prompts.ts";
|
|
|
44
44
|
import { graphBlockForRefiner } from "../src/code-graph/prompts.ts";
|
|
45
45
|
import { runPiSubagent, stripFrontmatter } from "../src/subagent.ts";
|
|
46
46
|
import { RefineOverlayController, refineOverlayContext } from "../src/refine-ui.ts";
|
|
47
|
+
import { matchesTerminalKey } from "../src/terminal-keys.ts";
|
|
48
|
+
import { toggleDashboardExpanded } from "../src/exec.ts";
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
const RefineParams = Type.Object({
|
|
@@ -90,10 +92,15 @@ async function ensureReviewerReady(
|
|
|
90
92
|
): Promise<{ mode: string; model_selector: string | null; thinking_level: string | null; confirmed_at: string | null; name_prefix: string }> {
|
|
91
93
|
if (role.mode === "current-session" || reviewerReady(role as never)) return role as never;
|
|
92
94
|
let outcome: FirstUseOutcome = await runFirstUseFlow(host, role.thinking_level);
|
|
93
|
-
if (outcome.status === "confirmed"
|
|
95
|
+
if (outcome.status === "confirmed") {
|
|
94
96
|
// F-008: validate the freshly chosen selector against the registry when
|
|
95
97
|
// one is present, so a typo'd manual entry fails here, not at spawn.
|
|
96
|
-
|
|
98
|
+
// (v0.8.1 field-drift fix: the outcome's top-level field is camelCase
|
|
99
|
+
// `modelSelector` — reading snake_case `model_selector` yielded
|
|
100
|
+
// undefined, passed the old `!== null` guard, and crashed findModel
|
|
101
|
+
// with "Cannot read properties of undefined (reading 'indexOf')"
|
|
102
|
+
// right after the first-use panel confirmed.)
|
|
103
|
+
if (availableModels(host).length > 0 && findModel(host, outcome.modelSelector) === null) {
|
|
97
104
|
outcome = await runFirstUseFlow(host, outcome.role.thinking_level);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
@@ -117,7 +124,19 @@ function setupRefinementExecution(
|
|
|
117
124
|
if (parentSignal?.aborted) controller.abort();
|
|
118
125
|
else parentSignal?.addEventListener("abort", relayAbort, { once: true });
|
|
119
126
|
|
|
120
|
-
const overlay = ctx.mode === "tui"
|
|
127
|
+
const overlay = ctx.mode === "tui"
|
|
128
|
+
? new RefineOverlayController(
|
|
129
|
+
"reviewer",
|
|
130
|
+
lanes,
|
|
131
|
+
relayAbort,
|
|
132
|
+
lang,
|
|
133
|
+
// pi-tui has no key bubbling: forward the dashboard toggle so
|
|
134
|
+
// Ctrl+Shift+T keeps working while the refine overlay holds focus.
|
|
135
|
+
(data) => {
|
|
136
|
+
if (matchesTerminalKey(data, "ctrl+shift+t")) toggleDashboardExpanded(ctx);
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
: undefined;
|
|
121
140
|
overlay?.open(refineOverlayContext(ctx), modelLabel);
|
|
122
141
|
|
|
123
142
|
return {
|