pi-plans 0.3.1 → 0.3.3
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 +7 -5
- package/index.ts +100 -22
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +28 -1
- package/references/plan-artifact-template.md +4 -0
- package/references/state-and-config.md +19 -0
- package/src/autocomplete.ts +2 -1
- package/src/code-graph/commands.ts +21 -6
- package/src/compaction.ts +10 -0
- package/src/config-command.ts +2 -0
- package/src/exec.ts +394 -61
- package/src/guard.ts +7 -1
- package/src/resume-command.ts +450 -0
- package/src/resume.ts +205 -0
- package/src/run-context.ts +97 -0
- package/src/run-ownership.ts +310 -0
- package/src/state.ts +24 -1
- package/src/termination-prompt.ts +8 -0
- package/src/workflow-state.ts +1159 -0
- package/tests/ask-choice.test.ts +113 -0
- package/tests/compaction.test.ts +39 -0
- package/tests/exec-lifecycle.test.ts +137 -0
- package/tests/exec.test.ts +237 -117
- package/tests/goal-wait.test.ts +269 -0
- package/tests/guard.test.ts +46 -0
- package/tests/plans.test.ts +71 -0
- package/tests/refine-resume.test.ts +324 -0
- package/tests/resume-lifecycle.test.ts +385 -0
- package/tests/resume.test.ts +384 -0
- package/tests/run-context.test.ts +119 -0
- package/tests/run-ownership.test.ts +170 -0
- package/tests/state.test.ts +16 -0
- package/tests/workflow-state.test.ts +432 -0
- package/tools/analyze-refs.ts +2 -1
- package/tools/ask-choice.ts +61 -3
- package/tools/code-graph.ts +5 -1
- package/tools/execute-plan.ts +20 -1
- package/tools/plans.ts +119 -0
- package/tools/refine.ts +91 -7
package/tests/exec.test.ts
CHANGED
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
completeExecution,
|
|
16
16
|
consumePendingExecutionFlush,
|
|
17
17
|
consumePlanningCompactionResumeGuard,
|
|
18
|
+
consumePrePlanCompactPending,
|
|
18
19
|
drainExecutionFlush,
|
|
19
20
|
executionContextMessage,
|
|
20
21
|
filterExecutionResumeMessages,
|
|
21
22
|
filterPlanningResumeMessages,
|
|
22
23
|
getExecution,
|
|
23
|
-
resumeGoalWaitIfPaused,
|
|
24
24
|
handleExecutionBeforeCompact,
|
|
25
25
|
handleExecutionCompact,
|
|
26
26
|
handleExecutionTurnCompaction,
|
|
@@ -32,12 +32,15 @@ import {
|
|
|
32
32
|
handlePlanningCompact,
|
|
33
33
|
handlePlanningCompactFailed,
|
|
34
34
|
isExecutionComplete,
|
|
35
|
+
markPrePlanCompactPending,
|
|
35
36
|
PLANNING_PLAN_WRITTEN_CUSTOM_TYPE,
|
|
37
|
+
PLANNING_PREPLAN_COMPACT_HINT,
|
|
38
|
+
PLANNING_PREPLAN_RESUME_CUSTOM_TYPE,
|
|
36
39
|
PLANNING_RUN_START_CUSTOM_TYPE,
|
|
37
40
|
refreshPlanningCompactionCooldown,
|
|
38
41
|
requestPlanningCompaction,
|
|
39
42
|
restoreFromSession,
|
|
40
|
-
|
|
43
|
+
sendPrePlanCompactResume,
|
|
41
44
|
shouldTriggerPlanningCompaction,
|
|
42
45
|
startExecution,
|
|
43
46
|
recordExecutionTurn,
|
|
@@ -45,6 +48,7 @@ import {
|
|
|
45
48
|
stopExecution,
|
|
46
49
|
updateStatusWidget,
|
|
47
50
|
} from "../src/exec.ts";
|
|
51
|
+
import { buildPiPlansVccCompaction, loadVccSettings } from "../src/compaction.ts";
|
|
48
52
|
import type { CheckItem } from "../src/plan.ts";
|
|
49
53
|
import { initState, setGraphEnabled, setRunStatus, startRun } from "../src/state.ts";
|
|
50
54
|
|
|
@@ -1459,138 +1463,254 @@ function makePreparation(reason: "manual" | "threshold" | "overflow", previousSu
|
|
|
1459
1463
|
};
|
|
1460
1464
|
}
|
|
1461
1465
|
|
|
1462
|
-
describe("
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
};
|
|
1466
|
+
describe("amelioration termination prompt", () => {
|
|
1467
|
+
it("recommends goal-wait first and keeps the round options", () => {
|
|
1468
|
+
assert.match(AMELIORATION_PROMPT_TEXT, /goal wait: continue until no unpassed VCs remain/);
|
|
1469
|
+
assert.match(AMELIORATION_PROMPT_TEXT, /until no high-severity finding \(hard cap 5 rounds\)/);
|
|
1470
|
+
assert.match(AMELIORATION_PROMPT_TEXT, /How should the implementation-review loop terminate\?/);
|
|
1471
|
+
});
|
|
1472
|
+
});
|
|
1470
1473
|
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
const {
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1474
|
+
describe("checkpoint-backed execution restore (I-005)", () => {
|
|
1475
|
+
it("startExecution records approval (plan digest + HEAD) and progress in the checkpoint", async () => {
|
|
1476
|
+
const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
|
|
1477
|
+
const { createCheckpoint, loadCheckpoint } = await import("../src/workflow-state.ts");
|
|
1478
|
+
const { resetRunBindingForTests } = await import("../src/run-context.ts");
|
|
1479
|
+
resetRunBindingForTests();
|
|
1480
|
+
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-exec-cp-"));
|
|
1481
|
+
try {
|
|
1482
|
+
const { spawnSync } = await import("node:child_process");
|
|
1483
|
+
spawnSync("git", ["init"], { cwd: workdir });
|
|
1484
|
+
spawnSync("git", ["config", "user.email", "t@e.com"], { cwd: workdir });
|
|
1485
|
+
spawnSync("git", ["config", "user.name", "T"], { cwd: workdir });
|
|
1486
|
+
initState(workdir);
|
|
1487
|
+
const { run } = startRun(workdir, { topic: "exec-cp", skill: "plan-normal", requestText: "t" });
|
|
1488
|
+
createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
|
|
1489
|
+
const artifactDir = run.artifact_dir;
|
|
1490
|
+
fs.mkdirSync(artifactDir, { recursive: true });
|
|
1491
|
+
const planPath = path.join(artifactDir, "PLAN_v1.md");
|
|
1492
|
+
fs.writeFileSync(
|
|
1493
|
+
planPath,
|
|
1494
|
+
"# Plan\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n- [ ] `VC-002` covers `I-002`; pass condition: y.\n",
|
|
1495
|
+
"utf8",
|
|
1496
|
+
);
|
|
1497
|
+
spawnSync("git", ["add", "-A"], { cwd: workdir });
|
|
1498
|
+
spawnSync("git", ["commit", "-m", "init"], { cwd: workdir });
|
|
1499
|
+
|
|
1500
|
+
const harness = makeHarness(workdir);
|
|
1501
|
+
const items: CheckItem[] = [
|
|
1502
|
+
{ id: "VC-001", text: "covers I-001", done: false },
|
|
1503
|
+
{ id: "VC-002", text: "covers I-002", done: false },
|
|
1504
|
+
];
|
|
1505
|
+
await startExecution(harness.pi, harness.ctx, planPath, items);
|
|
1506
|
+
|
|
1507
|
+
const loaded = loadCheckpoint(workdir, run.run_id);
|
|
1508
|
+
assert.equal(loaded.status, "ok");
|
|
1509
|
+
if (loaded.status === "ok") {
|
|
1510
|
+
assert.equal(loaded.checkpoint.phase, "executing");
|
|
1511
|
+
const approval = loaded.checkpoint.execution?.approval;
|
|
1512
|
+
assert.ok(approval, "approval recorded");
|
|
1513
|
+
assert.match(approval!.headAtApproval ?? "", /^[0-9a-f]{40}$/, "HEAD at approval");
|
|
1514
|
+
assert.equal(approval!.plan.path, planPath);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
// Progress lands in the checkpoint.
|
|
1518
|
+
applyDoneMarkers("[DONE:VC-001]");
|
|
1519
|
+
recordExecutionTurn(harness.pi, harness.ctx, ["VC-001"], { input: 5, output: 2 });
|
|
1520
|
+
const afterProgress = loadCheckpoint(workdir, run.run_id);
|
|
1521
|
+
if (afterProgress.status === "ok") {
|
|
1522
|
+
assert.deepEqual(afterProgress.checkpoint.execution?.doneVcIds, ["VC-001"]);
|
|
1523
|
+
assert.equal(afterProgress.checkpoint.execution?.usage.inToks, 5);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// Cross-session restore: fresh harness (new session), load from checkpoint.
|
|
1527
|
+
resetRunBindingForTests();
|
|
1528
|
+
const harness2 = makeHarness(workdir);
|
|
1529
|
+
const restore = loadExecutionFromCheckpoint(harness2.pi, harness2.ctx, run.run_id);
|
|
1530
|
+
assert.equal(restore.status, "loaded");
|
|
1531
|
+
assert.deepEqual(restore.doneVcIds, ["VC-001"]);
|
|
1532
|
+
assert.equal(restore.reverifyAll, false, "same HEAD keeps verified VCs");
|
|
1533
|
+
assert.equal(getExecution()?.items.find((item) => item.id === "VC-001")?.done, true);
|
|
1534
|
+
// F-002: an immediate session snapshot was appended.
|
|
1535
|
+
const snapshots = harness2.recorded.entries.filter((entry) => entry.customType === "pi-plans-exec");
|
|
1536
|
+
assert.ok(snapshots.length >= 1, "session snapshot written on load");
|
|
1537
|
+
|
|
1538
|
+
// Stop keeps progress with pausedReason (D-008).
|
|
1539
|
+
await stopExecution(harness2.pi, harness2.ctx, "stopped by user");
|
|
1540
|
+
const stopped = loadCheckpoint(workdir, run.run_id);
|
|
1541
|
+
if (stopped.status === "ok") {
|
|
1542
|
+
assert.equal(stopped.checkpoint.execution?.pausedReason, "stopped by user");
|
|
1543
|
+
assert.deepEqual(stopped.checkpoint.execution?.doneVcIds, ["VC-001"]);
|
|
1544
|
+
}
|
|
1545
|
+
} finally {
|
|
1546
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
1547
|
+
}
|
|
1482
1548
|
});
|
|
1483
1549
|
|
|
1484
|
-
it("
|
|
1485
|
-
const
|
|
1486
|
-
const {
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1550
|
+
it("HEAD change after approval keeps authorization but re-verifies VCs (D-011)", async () => {
|
|
1551
|
+
const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
|
|
1552
|
+
const { createCheckpoint, loadCheckpoint } = await import("../src/workflow-state.ts");
|
|
1553
|
+
const { resetRunBindingForTests } = await import("../src/run-context.ts");
|
|
1554
|
+
resetRunBindingForTests();
|
|
1555
|
+
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-exec-head-"));
|
|
1556
|
+
try {
|
|
1557
|
+
const { spawnSync } = await import("node:child_process");
|
|
1558
|
+
spawnSync("git", ["init"], { cwd: workdir });
|
|
1559
|
+
spawnSync("git", ["config", "user.email", "t@e.com"], { cwd: workdir });
|
|
1560
|
+
spawnSync("git", ["config", "user.name", "T"], { cwd: workdir });
|
|
1561
|
+
initState(workdir);
|
|
1562
|
+
const { run } = startRun(workdir, { topic: "exec-head", skill: "plan-normal", requestText: "t" });
|
|
1563
|
+
createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
|
|
1564
|
+
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
1565
|
+
const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
|
|
1566
|
+
fs.writeFileSync(
|
|
1567
|
+
planPath,
|
|
1568
|
+
"# Plan\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n",
|
|
1569
|
+
"utf8",
|
|
1570
|
+
);
|
|
1571
|
+
spawnSync("git", ["add", "-A"], { cwd: workdir });
|
|
1572
|
+
spawnSync("git", ["commit", "-m", "one"], { cwd: workdir });
|
|
1573
|
+
|
|
1574
|
+
const harness = makeHarness(workdir);
|
|
1575
|
+
await startExecution(harness.pi, harness.ctx, planPath, [
|
|
1576
|
+
{ id: "VC-001", text: "covers I-001", done: false },
|
|
1577
|
+
]);
|
|
1578
|
+
applyDoneMarkers("[DONE:VC-001]");
|
|
1579
|
+
recordExecutionTurn(harness.pi, harness.ctx, ["VC-001"]);
|
|
1580
|
+
|
|
1581
|
+
// Simulate a branch switch / reset under the unchanged plan.
|
|
1582
|
+
spawnSync("git", ["commit", "--allow-empty", "-m", "two"], { cwd: workdir });
|
|
1583
|
+
|
|
1584
|
+
resetRunBindingForTests();
|
|
1585
|
+
const harness2 = makeHarness(workdir);
|
|
1586
|
+
const restore = loadExecutionFromCheckpoint(harness2.pi, harness2.ctx, run.run_id);
|
|
1587
|
+
assert.equal(restore.status, "loaded");
|
|
1588
|
+
assert.deepEqual(restore.doneVcIds, ["VC-001"], "historical evidence kept");
|
|
1589
|
+
assert.equal(restore.reverifyAll, true, "HEAD change forces re-verification");
|
|
1590
|
+
assert.equal(getExecution()?.items[0]?.done, false, "old VC not auto-passed");
|
|
1591
|
+
// Authorization survives.
|
|
1592
|
+
const cp = loadCheckpoint(workdir, run.run_id);
|
|
1593
|
+
if (cp.status === "ok") {
|
|
1594
|
+
assert.ok(cp.checkpoint.execution?.approval, "authorization kept");
|
|
1595
|
+
assert.equal(cp.checkpoint.execution?.reverifyAll, true);
|
|
1596
|
+
}
|
|
1597
|
+
} finally {
|
|
1598
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
1599
|
+
}
|
|
1493
1600
|
});
|
|
1601
|
+
});
|
|
1494
1602
|
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
assert.equal(recorded.userMessages.length, 0);
|
|
1502
|
-
assert.ok(recorded.messages.some((message) => message.customType === "pi-plans-complete"));
|
|
1603
|
+
describe("pre-plan compaction (start-run trigger)", () => {
|
|
1604
|
+
let tmpRoot: string;
|
|
1605
|
+
let counter = 0;
|
|
1606
|
+
|
|
1607
|
+
before(() => {
|
|
1608
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-preplan-"));
|
|
1503
1609
|
});
|
|
1504
1610
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
{ inFlight: true, resumeGuard: false, pendingFollowUpPrompt: null },
|
|
1508
|
-
{ inFlight: false, resumeGuard: true, pendingFollowUpPrompt: null },
|
|
1509
|
-
{ inFlight: false, resumeGuard: false, pendingFollowUpPrompt: "compaction follow-up" },
|
|
1510
|
-
];
|
|
1511
|
-
for (const flags of variants) {
|
|
1512
|
-
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-goal-wait-"));
|
|
1513
|
-
const { pi, ctx, recorded, emit } = setup(workdir);
|
|
1514
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001"));
|
|
1515
|
-
(ctx.sessionManager as any).__executionCompaction = { ...flags, cooldownActive: false };
|
|
1516
|
-
recorded.userMessages.length = 0;
|
|
1517
|
-
await emit("turn_end", { message: { role: "assistant", content: [{ type: "text", text: "working" }] } });
|
|
1518
|
-
assert.equal(recorded.userMessages.length, 0, `flags ${JSON.stringify(flags)} must skip goal-wait`);
|
|
1519
|
-
}
|
|
1611
|
+
after(() => {
|
|
1612
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
1520
1613
|
});
|
|
1521
1614
|
|
|
1522
|
-
|
|
1523
|
-
const
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1615
|
+
async function preplanHarness() {
|
|
1616
|
+
const piPlansExtension = (await import("../index.ts")).default;
|
|
1617
|
+
const harness = makeHarness(path.join(tmpRoot, `ws-${++counter}`));
|
|
1618
|
+
fs.mkdirSync(harness.ctx.cwd, { recursive: true });
|
|
1619
|
+
piPlansExtension(harness.pi as never);
|
|
1620
|
+
return harness;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
it("routes the pre-plan hint through the planning VCC path for a fresh run", () => {
|
|
1624
|
+
// Pure-builder assertion (order-independent): the getExecution() gate on
|
|
1625
|
+
// buildPlanningCompactionResult is covered by the planning-hook tests.
|
|
1626
|
+
const workdir = path.join(tmpRoot, `hint-${++counter}`);
|
|
1627
|
+
fs.mkdirSync(workdir, { recursive: true });
|
|
1628
|
+
initState(workdir);
|
|
1629
|
+
const { run } = startRun(workdir, { topic: "preplan hint", skill: "plan-normal", requestText: "x" });
|
|
1630
|
+
const built = buildPiPlansVccCompaction({
|
|
1631
|
+
branchEntries: compactableBranchEntries(),
|
|
1632
|
+
preparation: makePreparation("manual", null),
|
|
1633
|
+
customInstructions: PLANNING_PREPLAN_COMPACT_HINT,
|
|
1634
|
+
reason: "manual",
|
|
1635
|
+
willRetry: false,
|
|
1636
|
+
settings: loadVccSettings(path.join(workdir, ".git", "pi_plans")),
|
|
1637
|
+
phaseContext: { phase: "planning", runId: run.run_id, artifactDir: run.artifact_dir },
|
|
1638
|
+
});
|
|
1639
|
+
assert.equal(built.kind, "compaction", "pre-plan hint must produce a VCC compaction, not a cancel/fallback");
|
|
1640
|
+
if (built.kind !== "compaction") return;
|
|
1641
|
+
assert.equal((built.compaction.details as { compactor: string }).compactor, "pi-vcc");
|
|
1642
|
+
assert.equal((built.compaction.details as { phase: string }).phase, "planning");
|
|
1643
|
+
assert.match(built.compaction.summary, /\[Session Goal\]/);
|
|
1539
1644
|
});
|
|
1540
1645
|
|
|
1541
|
-
it("
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
assert.equal(getExecution()?.goalWait?.paused, false, `round ${round} must not pause`);
|
|
1548
|
-
}
|
|
1549
|
-
await emit("turn_end", { message: { role: "assistant", content: [{ type: "text", text: "waiting for CI (6)" }] } });
|
|
1550
|
-
assert.equal(getExecution()?.goalWait?.paused, true);
|
|
1551
|
-
assert.ok(recorded.notifies.some((entry) => /waiting without progress for 6 rounds/.test(entry.message)));
|
|
1646
|
+
it("marks and consumes the pending flag exactly once", () => {
|
|
1647
|
+
const harness = makeHarness(path.join(tmpRoot, `flag-${++counter}`));
|
|
1648
|
+
assert.equal(consumePrePlanCompactPending(harness.ctx), null);
|
|
1649
|
+
markPrePlanCompactPending(harness.ctx, "run-1");
|
|
1650
|
+
assert.deepEqual(consumePrePlanCompactPending(harness.ctx), { runId: "run-1" });
|
|
1651
|
+
assert.equal(consumePrePlanCompactPending(harness.ctx), null);
|
|
1552
1652
|
});
|
|
1553
1653
|
|
|
1554
|
-
it("
|
|
1555
|
-
const
|
|
1556
|
-
const {
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
await emit("
|
|
1561
|
-
assert.equal(
|
|
1562
|
-
assert.equal(
|
|
1654
|
+
it("compacts once from the plans tool_result and resumes exactly once on success", async () => {
|
|
1655
|
+
const harness = await preplanHarness();
|
|
1656
|
+
const { ctx, recorded, emit } = harness;
|
|
1657
|
+
markPrePlanCompactPending(ctx, "run-a");
|
|
1658
|
+
|
|
1659
|
+
// Non-plans tool results never consume the pending flag.
|
|
1660
|
+
await emit("tool_result", { type: "tool_result", toolName: "edit", input: { path: "x" }, isError: false });
|
|
1661
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
1662
|
+
assert.equal(recorded.messages.length, 0);
|
|
1663
|
+
|
|
1664
|
+
await emit("tool_result", { type: "tool_result", toolName: "plans", input: { action: "start-run" }, isError: false });
|
|
1665
|
+
assert.equal(recorded.compacts?.length, 1);
|
|
1666
|
+
assert.equal(recorded.compacts?.[0]?.customInstructions, PLANNING_PREPLAN_COMPACT_HINT);
|
|
1667
|
+
assert.equal(recorded.messages.length, 0, "no resume before the compaction settles");
|
|
1668
|
+
|
|
1669
|
+
recorded.compacts?.[0]?.onComplete?.({} as never);
|
|
1670
|
+
assert.equal(recorded.messages.length, 1);
|
|
1671
|
+
assert.equal(recorded.messages[0]?.customType, PLANNING_PREPLAN_RESUME_CUSTOM_TYPE);
|
|
1672
|
+
assert.equal(recorded.messages[0]?.content, "Continue planning.");
|
|
1673
|
+
assert.equal((recorded.messages[0] as { display?: boolean }).display, false);
|
|
1674
|
+
assert.equal((recorded.messages[0] as { options?: { triggerTurn?: boolean } }).options?.triggerTurn, true);
|
|
1675
|
+
|
|
1676
|
+
// Pending flag consumed: a second plans result neither compacts nor resumes.
|
|
1677
|
+
await emit("tool_result", { type: "tool_result", toolName: "plans", input: { action: "record-decision" }, isError: false });
|
|
1678
|
+
assert.equal(recorded.compacts?.length, 1);
|
|
1679
|
+
recorded.compacts?.[0]?.onComplete?.({} as never);
|
|
1680
|
+
assert.equal(recorded.messages.length, 1);
|
|
1563
1681
|
});
|
|
1564
1682
|
|
|
1565
|
-
it("
|
|
1566
|
-
const
|
|
1567
|
-
const {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
const snapshot = {
|
|
1571
|
-
planPath,
|
|
1572
|
-
items: items("VC-001"),
|
|
1573
|
-
startedAt: "2026-08-25T00:00:00Z",
|
|
1574
|
-
usage: { inToks: 0, outToks: 0 },
|
|
1575
|
-
implItems: [],
|
|
1576
|
-
implStatus: {},
|
|
1577
|
-
goalWait: { noProgressRounds: 2, waitRounds: 1, lastMarkers: null, paused: false },
|
|
1683
|
+
it("resumes exactly once with an info notice when compaction fails", async () => {
|
|
1684
|
+
const harness = await preplanHarness();
|
|
1685
|
+
const { ctx, recorded } = harness;
|
|
1686
|
+
(ctx as { compact?: unknown }).compact = (options: { onError?: (error: Error) => void }) => {
|
|
1687
|
+
options.onError?.(new Error("Nothing to compact (session too small)"));
|
|
1578
1688
|
};
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
];
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1689
|
+
markPrePlanCompactPending(ctx, "run-b");
|
|
1690
|
+
await harness.emit("tool_result", { type: "tool_result", toolName: "plans", input: { action: "start-run" }, isError: false });
|
|
1691
|
+
assert.equal(recorded.messages.length, 1, "resume exactly once despite the failure");
|
|
1692
|
+
assert.equal(recorded.messages[0]?.customType, PLANNING_PREPLAN_RESUME_CUSTOM_TYPE);
|
|
1693
|
+
assert.deepEqual(recorded.notifies.at(-1), {
|
|
1694
|
+
message: "pi-plans: pre-plan compaction skipped; continuing planning.",
|
|
1695
|
+
severity: "info",
|
|
1696
|
+
});
|
|
1587
1697
|
});
|
|
1588
|
-
});
|
|
1589
1698
|
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1699
|
+
it("skips silently and still resumes when ctx.compact is unavailable (older Pi)", async () => {
|
|
1700
|
+
const harness = await preplanHarness();
|
|
1701
|
+
(harness.ctx as { compact?: unknown }).compact = undefined;
|
|
1702
|
+
markPrePlanCompactPending(harness.ctx, "run-c");
|
|
1703
|
+
await harness.emit("tool_result", { type: "tool_result", toolName: "plans", input: { action: "start-run" }, isError: false });
|
|
1704
|
+
assert.equal(harness.recorded.messages.length, 1);
|
|
1705
|
+
assert.equal(harness.recorded.messages[0]?.customType, PLANNING_PREPLAN_RESUME_CUSTOM_TYPE);
|
|
1706
|
+
});
|
|
1707
|
+
|
|
1708
|
+
it("filters the pre-plan resume message out of the model context payload", () => {
|
|
1709
|
+
const messages = [
|
|
1710
|
+
{ customType: "user", content: "real prompt" },
|
|
1711
|
+
{ customType: PLANNING_PREPLAN_RESUME_CUSTOM_TYPE, content: "Continue planning." },
|
|
1712
|
+
{ customType: "assistant", content: "ok" },
|
|
1713
|
+
];
|
|
1714
|
+
assert.equal(filterPlanningResumeMessages(messages).length, 2);
|
|
1595
1715
|
});
|
|
1596
1716
|
});
|