pi-plans 0.2.0 → 0.3.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/README.md +74 -21
- package/index.ts +115 -9
- package/package.json +7 -1
- package/references/pi-planning-workflow.md +18 -3
- package/references/state-and-config.md +34 -2
- package/scripts/validate.ts +4 -0
- package/src/code-graph/commands.ts +437 -0
- package/src/code-graph/discovery.ts +118 -0
- package/src/code-graph/git.ts +108 -0
- package/src/code-graph/identity.ts +59 -0
- package/src/code-graph/indexer.ts +281 -0
- package/src/code-graph/materialize.ts +166 -0
- package/src/code-graph/mode.ts +28 -0
- package/src/code-graph/mutations.ts +160 -0
- package/src/code-graph/parser.ts +51 -0
- package/src/code-graph/parsers/javascript.ts +35 -0
- package/src/code-graph/parsers/python.ts +160 -0
- package/src/code-graph/parsers/tree-sitter.ts +316 -0
- package/src/code-graph/paths.ts +85 -0
- package/src/code-graph/prompts.ts +18 -0
- package/src/code-graph/resolver.ts +69 -0
- package/src/code-graph/runtime.ts +158 -0
- package/src/code-graph/schema.ts +135 -0
- package/src/code-graph/screening.ts +82 -0
- package/src/code-graph/store.ts +278 -0
- package/src/code-graph/summary.ts +435 -0
- package/src/code-graph/types.ts +163 -0
- package/src/compaction.ts +1125 -371
- package/src/config-command.ts +326 -0
- package/src/exec.ts +356 -686
- package/src/refine-prompts.ts +50 -0
- package/src/refine-ui-helpers.ts +71 -18
- package/src/refine-ui-state.ts +87 -21
- package/src/refine-ui.ts +210 -102
- package/src/state.ts +19 -6
- package/src/subagent.ts +163 -61
- package/tests/ask-choice.test.ts +263 -0
- package/tests/autocomplete.test.ts +6 -1
- package/tests/code-graph-apply.test.ts +185 -0
- package/tests/code-graph-commands.test.ts +211 -0
- package/tests/code-graph-db.test.ts +166 -0
- package/tests/code-graph-discovery.test.ts +38 -0
- package/tests/code-graph-git.test.ts +94 -0
- package/tests/code-graph-index.test.ts +175 -0
- package/tests/code-graph-loop.e2e.test.ts +159 -0
- package/tests/code-graph-mutations.test.ts +117 -0
- package/tests/code-graph-parser.test.ts +85 -0
- package/tests/code-graph-rollback.test.ts +100 -0
- package/tests/code-graph-summary-batching.test.ts +518 -0
- package/tests/code-graph-summary.test.ts +148 -0
- package/tests/compaction.test.ts +371 -57
- package/tests/config-command.test.ts +255 -0
- package/tests/exec.test.ts +665 -241
- package/tests/fixtures/code-graph/sample.js +36 -0
- package/tests/fixtures/code-graph/sample.py +20 -0
- package/tests/fixtures/code-graph/sample.ts +15 -0
- package/tests/graph-aware-file-tools.test.ts +411 -0
- package/tests/refine-prompts.test.ts +67 -2
- package/tests/refine-ui.test.ts +337 -72
- package/tests/subagent.test.ts +26 -20
- package/tools/ask-choice.ts +158 -11
- package/tools/code-graph.ts +254 -0
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +84 -1
- package/tools/refine.ts +61 -15
package/tests/exec.test.ts
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
handleExecutionCompact,
|
|
24
24
|
handleExecutionTurnCompaction,
|
|
25
25
|
handleExecutionCompactFailed,
|
|
26
|
+
compactionInFlight,
|
|
27
|
+
noteCompactionStarted,
|
|
28
|
+
noteCompactionEnded,
|
|
26
29
|
handlePlanningBeforeCompact,
|
|
27
30
|
handlePlanningCompact,
|
|
28
31
|
handlePlanningCompactFailed,
|
|
@@ -40,7 +43,7 @@ import {
|
|
|
40
43
|
updateStatusWidget,
|
|
41
44
|
} from "../src/exec.ts";
|
|
42
45
|
import type { CheckItem } from "../src/plan.ts";
|
|
43
|
-
import { initState, setRunStatus, startRun } from "../src/state.ts";
|
|
46
|
+
import { initState, setGraphEnabled, setRunStatus, startRun } from "../src/state.ts";
|
|
44
47
|
|
|
45
48
|
interface Recorded {
|
|
46
49
|
entries: { type: string; customType?: string; data?: unknown }[];
|
|
@@ -55,6 +58,7 @@ interface Recorded {
|
|
|
55
58
|
selectAnswer?: string;
|
|
56
59
|
current: { provider: string; id: string } | null;
|
|
57
60
|
thinking: string | null;
|
|
61
|
+
userMessages: string[];
|
|
58
62
|
compacts?: { customInstructions?: string }[];
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -78,6 +82,7 @@ function makeHarness(workdir: string): Harness {
|
|
|
78
82
|
selects: [],
|
|
79
83
|
current: { provider: "p", id: "m" },
|
|
80
84
|
thinking: "high",
|
|
85
|
+
userMessages: [],
|
|
81
86
|
};
|
|
82
87
|
let contextPercent: number | null = 0;
|
|
83
88
|
const registryModels = [
|
|
@@ -108,7 +113,9 @@ function makeHarness(workdir: string): Harness {
|
|
|
108
113
|
sendMessage: (message: { customType: string; content: string }, options?: { triggerTurn?: boolean }) => {
|
|
109
114
|
recorded.messages.push({ ...message, options });
|
|
110
115
|
},
|
|
111
|
-
sendUserMessage: async () => {
|
|
116
|
+
sendUserMessage: async (content: string) => {
|
|
117
|
+
recorded.userMessages.push(content);
|
|
118
|
+
},
|
|
112
119
|
setModel: async (model: { provider: string; id: string }) => {
|
|
113
120
|
recorded.models.push({ provider: model.provider, id: model.id });
|
|
114
121
|
recorded.current = { provider: model.provider, id: model.id };
|
|
@@ -177,6 +184,22 @@ function items(...ids: string[]): CheckItem[] {
|
|
|
177
184
|
return ids.map((id) => ({ id, text: `\`${id}\` demo item`, done: false }));
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
function compactableBranchEntries(): any[] {
|
|
188
|
+
return [
|
|
189
|
+
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "start the work" }] } },
|
|
190
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "made progress" }] } },
|
|
191
|
+
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "continue from here" }] } },
|
|
192
|
+
{ id: "a-2", type: "message", message: { role: "assistant", content: [{ type: "text", text: "tail work" }] } },
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function startActiveRun(workdir: string, status: "planning" | "executing" = "planning") {
|
|
197
|
+
initState(workdir);
|
|
198
|
+
const { run } = startRun(workdir, { topic: "compact", skill: "plan-normal", requestText: "x" });
|
|
199
|
+
if (status !== "planning") setRunStatus(workdir, run.run_id, status);
|
|
200
|
+
return run;
|
|
201
|
+
}
|
|
202
|
+
|
|
180
203
|
describe("execution loop", () => {
|
|
181
204
|
let tmpRoot: string;
|
|
182
205
|
let counter = 0;
|
|
@@ -206,7 +229,7 @@ describe("execution loop", () => {
|
|
|
206
229
|
assert.match(recorded.status ?? "", /out-toks/);
|
|
207
230
|
|
|
208
231
|
assert.ok(getExecution());
|
|
209
|
-
const rules = executionContextMessage()!;
|
|
232
|
+
const rules = executionContextMessage(ctx)!;
|
|
210
233
|
assert.match(rules, /PI-PLANS EXECUTION/);
|
|
211
234
|
assert.match(rules, /VC-001/);
|
|
212
235
|
assert.match(rules, /subprocess-backed verification/);
|
|
@@ -234,6 +257,86 @@ describe("execution loop", () => {
|
|
|
234
257
|
assert.ok(recorded.messages.some((message) => message.customType === "pi-plans-complete"));
|
|
235
258
|
});
|
|
236
259
|
|
|
260
|
+
it("completion enters goal-running continuation and triggers a turn in interactive sessions", async () => {
|
|
261
|
+
const workdir = freshWorkdir();
|
|
262
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
263
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
264
|
+
await startExecution(pi, ctx, planPath, items("VC-001", "VC-002"));
|
|
265
|
+
assert.deepEqual(applyDoneMarkers("[DONE:VC-001] [DONE:VC-002]"), ["VC-001", "VC-002"]);
|
|
266
|
+
|
|
267
|
+
await completeExecution(pi, ctx);
|
|
268
|
+
|
|
269
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
270
|
+
assert.ok(completeMessage);
|
|
271
|
+
assert.match(completeMessage.content, /Goal-running continuation/);
|
|
272
|
+
assert.match(completeMessage.content, /termination condition of the implementation-review loop/);
|
|
273
|
+
assert.doesNotMatch(completeMessage.content, /Run a post-execution amelioration round/);
|
|
274
|
+
assert.equal(completeMessage.options?.triggerTurn, true);
|
|
275
|
+
const ameliorateEntry = recorded.entries.find((entry) => entry.customType === "pi-plans-ameliorate");
|
|
276
|
+
assert.ok(ameliorateEntry);
|
|
277
|
+
const data = ameliorateEntry.data as Record<string, unknown>;
|
|
278
|
+
assert.equal(data.phase, "goal-started");
|
|
279
|
+
assert.equal(data.rounds, null);
|
|
280
|
+
assert.equal(data.currentRound, 0);
|
|
281
|
+
assert.equal(data.planPath, planPath);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("completion stays silent in headless sessions", async () => {
|
|
285
|
+
const workdir = freshWorkdir();
|
|
286
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
287
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001"));
|
|
288
|
+
assert.deepEqual(applyDoneMarkers("[DONE:VC-001]"), ["VC-001"]);
|
|
289
|
+
(ctx as any).hasUI = false;
|
|
290
|
+
|
|
291
|
+
await completeExecution(pi, ctx);
|
|
292
|
+
|
|
293
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
294
|
+
assert.ok(completeMessage);
|
|
295
|
+
assert.doesNotMatch(completeMessage.content, /Goal-running continuation/);
|
|
296
|
+
assert.equal(completeMessage.options?.triggerTurn, false);
|
|
297
|
+
assert.equal(
|
|
298
|
+
recorded.entries.some((entry) => entry.customType === "pi-plans-ameliorate"),
|
|
299
|
+
false,
|
|
300
|
+
"headless completion must not append the ameliorate entry",
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("restoreFromSession completion triggers the same goal-running continuation in interactive sessions", async () => {
|
|
305
|
+
const workdir = freshWorkdir();
|
|
306
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
307
|
+
const planPath = path.join(workdir, "PLAN_v1.md");
|
|
308
|
+
fs.writeFileSync(planPath, "# plan");
|
|
309
|
+
const snapshot = {
|
|
310
|
+
planPath,
|
|
311
|
+
items: items("VC-001", "VC-002"),
|
|
312
|
+
startedAt: "2026-08-25T00:00:00Z",
|
|
313
|
+
usage: { inToks: 0, outToks: 0 },
|
|
314
|
+
implItems: [],
|
|
315
|
+
implStatus: {},
|
|
316
|
+
compaction: {
|
|
317
|
+
inFlight: true,
|
|
318
|
+
resumeGuard: true,
|
|
319
|
+
cooldownActive: true,
|
|
320
|
+
lastAttemptReason: "threshold",
|
|
321
|
+
lastSuccessfulUsagePercent: 100,
|
|
322
|
+
lastSuccessfulAt: "2026-08-25T00:01:00Z",
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
const entries = [
|
|
326
|
+
{ type: "custom", customType: "pi-plans-exec", data: snapshot },
|
|
327
|
+
{
|
|
328
|
+
type: "message",
|
|
329
|
+
message: { role: "assistant", content: [{ type: "text", text: "did [DONE:VC-001] [DONE:VC-002]" }] },
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
await restoreFromSession(pi, ctx, entries as any);
|
|
333
|
+
const completeMessage = recorded.messages.find((message) => message.customType === "pi-plans-complete");
|
|
334
|
+
assert.ok(completeMessage, "restore path must fire completeExecution");
|
|
335
|
+
assert.match(completeMessage.content, /Goal-running continuation/);
|
|
336
|
+
assert.equal(completeMessage.options?.triggerTurn, true);
|
|
337
|
+
assert.ok(recorded.entries.some((entry) => entry.customType === "pi-plans-ameliorate"));
|
|
338
|
+
});
|
|
339
|
+
|
|
237
340
|
it("restores progress from session entries and rescans messages", async () => {
|
|
238
341
|
const workdir = freshWorkdir();
|
|
239
342
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
@@ -392,36 +495,51 @@ describe("execution loop", () => {
|
|
|
392
495
|
assert.equal(getExecution(), null);
|
|
393
496
|
});
|
|
394
497
|
|
|
395
|
-
it("
|
|
498
|
+
it("returns control to Pi core without active execution or planning state", async () => {
|
|
396
499
|
const workdir = freshWorkdir();
|
|
397
|
-
const { pi, ctx
|
|
398
|
-
await startExecution(pi, ctx, path.join(workdir, "PLAN_v8.md"), items("VC-001", "VC-002"));
|
|
500
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
399
501
|
|
|
400
|
-
|
|
401
|
-
const
|
|
502
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v0.md"), items("VC-001"));
|
|
503
|
+
const executionResult = handleExecutionBeforeCompact(pi, ctx, {
|
|
402
504
|
type: "session_before_compact",
|
|
403
505
|
preparation: makePreparation("threshold", null),
|
|
404
|
-
branchEntries:
|
|
506
|
+
branchEntries: compactableBranchEntries(),
|
|
405
507
|
reason: "threshold",
|
|
406
508
|
willRetry: false,
|
|
407
509
|
signal: new AbortController().signal,
|
|
408
510
|
});
|
|
409
|
-
assert.equal(
|
|
410
|
-
|
|
511
|
+
assert.equal(executionResult, undefined);
|
|
512
|
+
await stopExecution(pi, ctx, "no-active-run");
|
|
411
513
|
|
|
412
|
-
|
|
413
|
-
const above = handleExecutionBeforeCompact(pi, ctx, {
|
|
514
|
+
const planningResult = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
414
515
|
type: "session_before_compact",
|
|
415
516
|
preparation: makePreparation("threshold", null),
|
|
416
|
-
branchEntries:
|
|
517
|
+
branchEntries: compactableBranchEntries(),
|
|
417
518
|
reason: "threshold",
|
|
418
519
|
willRetry: false,
|
|
419
520
|
signal: new AbortController().signal,
|
|
420
521
|
});
|
|
421
|
-
assert.equal(
|
|
422
|
-
|
|
522
|
+
assert.equal(planningResult, undefined);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("lets Pi core own execution compaction scheduling and customizes safe reasons", async () => {
|
|
526
|
+
const workdir = freshWorkdir();
|
|
527
|
+
startActiveRun(workdir);
|
|
528
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
529
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v8.md"), items("VC-001", "VC-002"));
|
|
423
530
|
|
|
424
|
-
const
|
|
531
|
+
const threshold = handleExecutionBeforeCompact(pi, ctx, {
|
|
532
|
+
type: "session_before_compact",
|
|
533
|
+
preparation: makePreparation("threshold", null),
|
|
534
|
+
branchEntries: compactableBranchEntries(),
|
|
535
|
+
reason: "threshold",
|
|
536
|
+
willRetry: false,
|
|
537
|
+
signal: new AbortController().signal,
|
|
538
|
+
});
|
|
539
|
+
assert.equal(threshold?.cancel, undefined);
|
|
540
|
+
assert.ok(threshold?.compaction, "threshold compaction should use the VCC summary when a legal cut exists");
|
|
541
|
+
|
|
542
|
+
const overflowRetry = handleExecutionBeforeCompact(pi, ctx, {
|
|
425
543
|
type: "session_before_compact",
|
|
426
544
|
preparation: makePreparation("overflow", null),
|
|
427
545
|
branchEntries: [],
|
|
@@ -429,8 +547,7 @@ describe("execution loop", () => {
|
|
|
429
547
|
willRetry: true,
|
|
430
548
|
signal: new AbortController().signal,
|
|
431
549
|
});
|
|
432
|
-
assert.equal(
|
|
433
|
-
assert.ok(overflow?.compaction);
|
|
550
|
+
assert.equal(overflowRetry, undefined, "overflow retry falls back to Pi core when VCC has no safe cut");
|
|
434
551
|
|
|
435
552
|
const manual = handleExecutionBeforeCompact(pi, ctx, {
|
|
436
553
|
type: "session_before_compact",
|
|
@@ -440,54 +557,29 @@ describe("execution loop", () => {
|
|
|
440
557
|
willRetry: false,
|
|
441
558
|
signal: new AbortController().signal,
|
|
442
559
|
});
|
|
443
|
-
assert.equal(manual?.cancel,
|
|
444
|
-
assert.ok(
|
|
560
|
+
assert.equal(manual?.cancel, true, "manual compaction cancels instead of discarding unsafe context");
|
|
561
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("Nothing to compact")));
|
|
445
562
|
|
|
446
563
|
await stopExecution(pi, ctx, "test-done");
|
|
447
564
|
});
|
|
448
565
|
|
|
449
|
-
it("
|
|
566
|
+
it("does not proactively request execution compaction", async () => {
|
|
450
567
|
const workdir = freshWorkdir();
|
|
568
|
+
startActiveRun(workdir);
|
|
451
569
|
const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
452
570
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v9.md"), items("VC-001", "VC-002"));
|
|
453
571
|
|
|
454
|
-
setUsagePercent(
|
|
455
|
-
handleExecutionTurnCompaction(ctx);
|
|
456
|
-
assert.equal(recorded.compacts?.length ?? 0, 1, "first high-watermark turn should request one compact");
|
|
457
|
-
|
|
458
|
-
handleExecutionTurnCompaction(ctx);
|
|
459
|
-
assert.equal(recorded.compacts?.length ?? 0, 1, "in-flight compact must not be requested twice");
|
|
460
|
-
|
|
461
|
-
handleExecutionCompact(pi, ctx, {
|
|
462
|
-
type: "session_compact",
|
|
463
|
-
compactionEntry: { type: "compaction" } as never,
|
|
464
|
-
fromExtension: true,
|
|
465
|
-
reason: "threshold",
|
|
466
|
-
willRetry: false,
|
|
467
|
-
});
|
|
468
|
-
assert.equal(
|
|
469
|
-
recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length,
|
|
470
|
-
1,
|
|
471
|
-
"non-retry execution compaction should queue one hidden resume",
|
|
472
|
-
);
|
|
473
|
-
|
|
474
|
-
setUsagePercent(96);
|
|
572
|
+
setUsagePercent(99);
|
|
475
573
|
handleExecutionTurnCompaction(ctx);
|
|
476
|
-
assert.equal(recorded.compacts?.length ?? 0, 1, "resume guard should suppress the immediate follow-up turn");
|
|
477
|
-
|
|
478
|
-
setUsagePercent(79);
|
|
479
574
|
handleExecutionTurnCompaction(ctx);
|
|
480
|
-
assert.equal(recorded.compacts?.length ?? 0,
|
|
481
|
-
|
|
482
|
-
setUsagePercent(96);
|
|
483
|
-
handleExecutionTurnCompaction(ctx);
|
|
484
|
-
assert.equal(recorded.compacts?.length ?? 0, 2, "high watermark should re-arm after the low-watermark drop");
|
|
575
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "execution scheduling is owned by Pi core");
|
|
485
576
|
|
|
486
577
|
await stopExecution(pi, ctx, "test-done");
|
|
487
578
|
});
|
|
488
579
|
|
|
489
|
-
it("
|
|
580
|
+
it("ignores current-I growth for proactive execution compaction", async () => {
|
|
490
581
|
const workdir = freshWorkdir();
|
|
582
|
+
startActiveRun(workdir);
|
|
491
583
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
492
584
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v22.md"), items("VC-001"), [
|
|
493
585
|
{ id: "I-001", text: "First item." },
|
|
@@ -499,52 +591,82 @@ describe("execution loop", () => {
|
|
|
499
591
|
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
500
592
|
await stopExecution(pi, ctx, "no-prefix");
|
|
501
593
|
});
|
|
502
|
-
it("wires execution compaction without restoring
|
|
594
|
+
it("wires execution compaction without restoring proactive requests or model helpers", () => {
|
|
503
595
|
const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
|
|
504
596
|
const execSource = fs.readFileSync(path.join(process.cwd(), "src/exec.ts"), "utf8");
|
|
505
597
|
assert.match(indexSource, /handleExecutionTurnCompaction/);
|
|
506
598
|
assert.match(execSource, /shouldTriggerExecutionCompaction/);
|
|
507
|
-
assert.
|
|
599
|
+
assert.doesNotMatch(execSource, /ctx\.compact\(/);
|
|
508
600
|
assert.doesNotMatch(
|
|
509
601
|
indexSource,
|
|
510
602
|
/setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel/,
|
|
511
603
|
);
|
|
512
604
|
assert.doesNotMatch(
|
|
513
605
|
execSource,
|
|
514
|
-
/setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel/,
|
|
606
|
+
/setExecutionModel|chooseExecutionModelSelection|snapshotCurrentModelSelector|ensureExecutionModelActive|restorePlanningModel|buildModelExecutionCompactionResult|modelRegistry\.complete/,
|
|
515
607
|
);
|
|
516
608
|
});
|
|
517
609
|
|
|
518
|
-
it("
|
|
610
|
+
it("auto-continues execution only for threshold/overflow and honors manual follow-up prompts", async () => {
|
|
519
611
|
const workdir = freshWorkdir();
|
|
612
|
+
startActiveRun(workdir);
|
|
520
613
|
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
614
|
+
(ctx as any).piVersion = "0.84.3";
|
|
521
615
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v9.md"), items("VC-001"));
|
|
522
616
|
|
|
523
|
-
const
|
|
524
|
-
|
|
617
|
+
const threshold = handleExecutionBeforeCompact(pi, ctx, {
|
|
618
|
+
type: "session_before_compact",
|
|
619
|
+
preparation: makePreparation("threshold", null),
|
|
620
|
+
branchEntries: compactableBranchEntries(),
|
|
621
|
+
reason: "threshold",
|
|
622
|
+
willRetry: false,
|
|
623
|
+
signal: new AbortController().signal,
|
|
624
|
+
});
|
|
625
|
+
assert.ok(threshold?.compaction);
|
|
626
|
+
await handleExecutionCompact(pi, ctx, {
|
|
525
627
|
type: "session_compact",
|
|
526
628
|
compactionEntry: { type: "compaction" } as never,
|
|
527
|
-
fromExtension:
|
|
629
|
+
fromExtension: false,
|
|
528
630
|
reason: "threshold",
|
|
529
631
|
willRetry: false,
|
|
530
632
|
});
|
|
531
|
-
|
|
532
|
-
assert.
|
|
533
|
-
assert.equal(resumes[0]?.options?.triggerTurn, true);
|
|
534
|
-
assert.equal(recorded.compacts?.length ?? 0, beforeRequests, "compaction hook must not invoke ctx.compact");
|
|
633
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length, 1);
|
|
634
|
+
assert.ok(recorded.notifies.some((note) => note.message.startsWith("pi-vcc: kept")));
|
|
535
635
|
|
|
536
|
-
|
|
636
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
637
|
+
type: "session_before_compact",
|
|
638
|
+
preparation: makePreparation("manual", null),
|
|
639
|
+
branchEntries: compactableBranchEntries(),
|
|
640
|
+
reason: "manual",
|
|
641
|
+
willRetry: false,
|
|
642
|
+
signal: new AbortController().signal,
|
|
643
|
+
});
|
|
644
|
+
await handleExecutionCompact(pi, ctx, {
|
|
537
645
|
type: "session_compact",
|
|
538
646
|
compactionEntry: { type: "compaction" } as never,
|
|
539
|
-
fromExtension:
|
|
540
|
-
reason: "
|
|
541
|
-
willRetry:
|
|
647
|
+
fromExtension: false,
|
|
648
|
+
reason: "manual",
|
|
649
|
+
willRetry: false,
|
|
542
650
|
});
|
|
543
|
-
assert.equal(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
651
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-exec-resume").length, 1);
|
|
652
|
+
|
|
653
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
654
|
+
type: "session_before_compact",
|
|
655
|
+
preparation: makePreparation("manual", null),
|
|
656
|
+
branchEntries: compactableBranchEntries(),
|
|
657
|
+
customInstructions: "Run focused tests keep:1",
|
|
658
|
+
reason: "manual",
|
|
659
|
+
willRetry: false,
|
|
660
|
+
signal: new AbortController().signal,
|
|
661
|
+
});
|
|
662
|
+
await handleExecutionCompact(pi, ctx, {
|
|
663
|
+
type: "session_compact",
|
|
664
|
+
compactionEntry: { type: "compaction" } as never,
|
|
665
|
+
fromExtension: false,
|
|
666
|
+
reason: "manual",
|
|
667
|
+
willRetry: false,
|
|
668
|
+
});
|
|
669
|
+
assert.deepEqual(recorded.userMessages, ["Run focused tests"]);
|
|
548
670
|
|
|
549
671
|
handleExecutionCompactFailed(pi, ctx, {
|
|
550
672
|
type: "session_compact_failed",
|
|
@@ -560,29 +682,30 @@ describe("execution loop", () => {
|
|
|
560
682
|
await stopExecution(pi, ctx, "test-done");
|
|
561
683
|
});
|
|
562
684
|
|
|
563
|
-
it("builds a
|
|
685
|
+
it("builds a VCC execution summary with phase context and previous summary", async () => {
|
|
564
686
|
const workdir = freshWorkdir();
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
687
|
+
startActiveRun(workdir);
|
|
688
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
689
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v10.md"), items("VC-001", "VC-002"), [
|
|
690
|
+
{ id: "I-001", text: "First item." },
|
|
691
|
+
{ id: "I-002", text: "Second item." },
|
|
692
|
+
]);
|
|
569
693
|
|
|
570
|
-
const previousSummary = "##
|
|
694
|
+
const previousSummary = "## Legacy Summary\nDeliver auto-compact in execution phase.";
|
|
695
|
+
applyCurrentIMarker("[I-002:current]");
|
|
571
696
|
const preparation = makePreparation("threshold", previousSummary);
|
|
572
697
|
const branchEntries: any[] = [
|
|
573
|
-
{ id: "exec-start", type: "custom", customType: "pi-plans-exec-start" },
|
|
574
698
|
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "implement VC-001" }] } },
|
|
575
|
-
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "wrote helper [
|
|
699
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "wrote helper [I-002:current]" }] } },
|
|
576
700
|
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "implement VC-002" }] } },
|
|
577
701
|
{ id: "a-2", type: "message", message: { role: "assistant", content: [{ type: "text", text: "almost done" }] } },
|
|
578
|
-
{ id: "exec-ctx", type: "custom", customType: "pi-plans-exec-context" },
|
|
579
702
|
];
|
|
580
703
|
const result = buildExecutionCompactionResult(
|
|
581
704
|
{
|
|
582
705
|
type: "session_before_compact",
|
|
583
706
|
preparation,
|
|
584
707
|
branchEntries,
|
|
585
|
-
customInstructions: "keep
|
|
708
|
+
customInstructions: "keep:1",
|
|
586
709
|
reason: "threshold",
|
|
587
710
|
willRetry: false,
|
|
588
711
|
signal: new AbortController().signal,
|
|
@@ -590,71 +713,51 @@ describe("execution loop", () => {
|
|
|
590
713
|
ctx,
|
|
591
714
|
);
|
|
592
715
|
assert.ok(result);
|
|
593
|
-
assert.
|
|
594
|
-
assert.match(result!.summary,
|
|
595
|
-
assert.match(result!.summary,
|
|
596
|
-
assert.match(result!.summary,
|
|
597
|
-
assert.match(result!.summary,
|
|
598
|
-
assert.match(result!.summary,
|
|
599
|
-
assert.match(result!.summary,
|
|
600
|
-
assert.
|
|
601
|
-
assert.
|
|
602
|
-
|
|
603
|
-
setUsagePercent(110);
|
|
604
|
-
handleExecutionBeforeCompact(pi, ctx, {
|
|
605
|
-
type: "session_before_compact",
|
|
606
|
-
preparation,
|
|
607
|
-
branchEntries,
|
|
608
|
-
reason: "threshold",
|
|
609
|
-
willRetry: false,
|
|
610
|
-
signal: new AbortController().signal,
|
|
611
|
-
});
|
|
716
|
+
assert.equal(result!.firstKeptEntryId, "u-2");
|
|
717
|
+
assert.match(result!.summary, /\[Session Goal\]/);
|
|
718
|
+
assert.match(result!.summary, /Execute accepted plan/);
|
|
719
|
+
assert.match(result!.summary, /\[Outstanding Context\]/);
|
|
720
|
+
assert.match(result!.summary, /Current implementation item: I-002/);
|
|
721
|
+
assert.match(result!.summary, /Remaining verifier items: VC-001, VC-002/);
|
|
722
|
+
assert.match(result!.summary, /Previous compact summary: Legacy Summary Deliver auto-compact/);
|
|
723
|
+
assert.equal((result!.details as any).compactor, "pi-vcc");
|
|
724
|
+
assert.equal((result!.details as any).phase, "execution");
|
|
612
725
|
|
|
613
726
|
await stopExecution(pi, ctx, "test-done");
|
|
614
727
|
});
|
|
615
728
|
|
|
616
|
-
it("
|
|
729
|
+
it("does not call model helpers and respects overrideDefaultCompaction=false", async () => {
|
|
617
730
|
const workdir = freshWorkdir();
|
|
618
|
-
|
|
731
|
+
startActiveRun(workdir);
|
|
732
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
619
733
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v21.md"), items("VC-001"), [
|
|
620
734
|
{ id: "I-001", text: "First item." },
|
|
621
735
|
]);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
content: [{ type: "text", text: "## Implementation Items\n- I-001\n\n## Current I\n- I-001\n\n## Read Records\n- none\n\n## Compaction Boundary\n- a-2\n\n## Decisions\n- preserved\n\n## Open Questions\n- none\n\n## Next Steps\n- continue" }],
|
|
627
|
-
usage: { input: 12, output: 34 },
|
|
628
|
-
};
|
|
629
|
-
(ctx.modelRegistry as any).complete = async (...args: unknown[]) => {
|
|
630
|
-
calls.push(args);
|
|
631
|
-
return response;
|
|
736
|
+
let completeCalled = false;
|
|
737
|
+
(ctx.modelRegistry as any).complete = async () => {
|
|
738
|
+
completeCalled = true;
|
|
739
|
+
return {};
|
|
632
740
|
};
|
|
633
741
|
const event: any = {
|
|
634
742
|
type: "session_before_compact",
|
|
635
743
|
preparation: makePreparation("threshold", null),
|
|
636
|
-
branchEntries:
|
|
637
|
-
{ id: "i-1", type: "message", tokens: 100, message: { role: "assistant", content: [{ type: "text", text: "[I-001:current] work" }] } },
|
|
638
|
-
{ id: "a-1", type: "message", tokens: 100, message: { role: "assistant", content: [{ type: "text", text: "details" }] } },
|
|
639
|
-
],
|
|
744
|
+
branchEntries: compactableBranchEntries(),
|
|
640
745
|
reason: "threshold",
|
|
641
746
|
willRetry: false,
|
|
642
|
-
signal,
|
|
747
|
+
signal: new AbortController().signal,
|
|
643
748
|
};
|
|
644
|
-
const valid =
|
|
749
|
+
const valid = handleExecutionBeforeCompact(pi, ctx, event);
|
|
645
750
|
assert.ok(valid?.compaction);
|
|
646
|
-
assert.equal(
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
assert.equal(
|
|
652
|
-
|
|
653
|
-
response = { stopReason: "length", content: [{ type: "text", text: "## Implementation Items\npartial" }] };
|
|
654
|
-
const invalid = await handleExecutionBeforeCompact(pi, ctx, { ...event, signal: new AbortController().signal });
|
|
655
|
-
assert.equal(invalid, undefined, "incomplete model output must return control to Pi default compaction");
|
|
751
|
+
assert.equal(completeCalled, false);
|
|
752
|
+
|
|
753
|
+
const configPath = path.join(workdir, ".git", "pi_plans", "pi-vcc-config.json");
|
|
754
|
+
fs.writeFileSync(configPath, JSON.stringify({ overrideDefaultCompaction: false }), "utf8");
|
|
755
|
+
const fallback = handleExecutionBeforeCompact(pi, ctx, { ...event, signal: new AbortController().signal });
|
|
756
|
+
assert.equal(fallback, undefined, "override-disabled should return control to Pi default compaction");
|
|
757
|
+
|
|
656
758
|
await stopExecution(pi, ctx, "model-test");
|
|
657
|
-
});
|
|
759
|
+
});
|
|
760
|
+
it("filters the hidden resume message out of the model context payload", () => {
|
|
658
761
|
const messages = [
|
|
659
762
|
{ customType: "user", content: "real prompt" },
|
|
660
763
|
{ customType: "pi-plans-exec-resume", content: "Continue execution." },
|
|
@@ -665,37 +768,27 @@ describe("execution loop", () => {
|
|
|
665
768
|
assert.equal(filterPlanningResumeMessages(messages).length, 3);
|
|
666
769
|
});
|
|
667
770
|
|
|
668
|
-
it("
|
|
771
|
+
it("builds a VCC planning summary from active-run session context", () => {
|
|
669
772
|
const workdir = freshWorkdir();
|
|
670
773
|
const { ctx } = makeHarness(workdir);
|
|
671
774
|
initState(workdir);
|
|
672
775
|
const { run } = startRun(workdir, { topic: "planning compact", skill: "plan-normal", requestText: "demo" });
|
|
673
|
-
|
|
776
|
+
const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
|
|
777
|
+
fs.writeFileSync(planPath, "# plan");
|
|
674
778
|
|
|
675
|
-
const
|
|
676
|
-
firstKeptEntryId: "fallback",
|
|
677
|
-
messagesToSummarize: [],
|
|
678
|
-
turnPrefixMessages: [],
|
|
679
|
-
isSplitTurn: false,
|
|
680
|
-
tokensBefore: 50000,
|
|
681
|
-
previousSummary,
|
|
682
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
683
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
684
|
-
});
|
|
685
|
-
|
|
686
|
-
// Case A: plan written → cut at next non-internal entry after plan-written; QA section included.
|
|
687
|
-
const branchEntriesWithPlan = [
|
|
779
|
+
const branchEntries = [
|
|
688
780
|
{ id: "rs", type: "custom", customType: PLANNING_RUN_START_CUSTOM_TYPE, data: { runId: run.run_id, artifactDir: run.artifact_dir } },
|
|
689
781
|
{ id: "u-1", type: "message", message: { role: "user", content: [{ type: "text", text: "background question?" }] } },
|
|
690
|
-
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "some context" }] } },
|
|
691
|
-
{ id: "pw", type: "custom", customType: PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, data: { runId: run.run_id, planPath
|
|
782
|
+
{ id: "a-1", type: "message", message: { role: "assistant", content: [{ type: "text", text: "some context [I-004:current]" }] } },
|
|
783
|
+
{ id: "pw", type: "custom", customType: PLANNING_PLAN_WRITTEN_CUSTOM_TYPE, data: { runId: run.run_id, planPath } },
|
|
692
784
|
{ id: "u-2", type: "message", message: { role: "user", content: [{ type: "text", text: "review please" }] } },
|
|
693
785
|
] as any;
|
|
694
786
|
const withPlan = buildPlanningCompactionResult(
|
|
695
787
|
{
|
|
696
788
|
type: "session_before_compact",
|
|
697
|
-
preparation:
|
|
698
|
-
branchEntries
|
|
789
|
+
preparation: makePreparation("threshold", "## Previous\nEarlier summary."),
|
|
790
|
+
branchEntries,
|
|
791
|
+
customInstructions: "keep:1",
|
|
699
792
|
reason: "threshold",
|
|
700
793
|
willRetry: false,
|
|
701
794
|
signal: new AbortController().signal,
|
|
@@ -704,95 +797,57 @@ describe("execution loop", () => {
|
|
|
704
797
|
);
|
|
705
798
|
assert.ok(withPlan);
|
|
706
799
|
assert.deepEqual(withPlan!.firstKeptEntryId, "u-2");
|
|
707
|
-
assert.match(withPlan!.summary,
|
|
708
|
-
assert.match(withPlan!.summary,
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const withoutPlan = buildPlanningCompactionResult(
|
|
717
|
-
{
|
|
718
|
-
type: "session_before_compact",
|
|
719
|
-
preparation: makePlanningPreparation(null),
|
|
720
|
-
branchEntries: branchEntriesWithoutPlan,
|
|
721
|
-
reason: "manual",
|
|
722
|
-
willRetry: false,
|
|
723
|
-
signal: new AbortController().signal,
|
|
724
|
-
},
|
|
725
|
-
ctx,
|
|
726
|
-
);
|
|
727
|
-
assert.ok(withoutPlan);
|
|
728
|
-
assert.deepEqual(withoutPlan!.firstKeptEntryId, "u-1");
|
|
729
|
-
assert.doesNotMatch(withoutPlan!.summary, /## Q&A During Planning/);
|
|
730
|
-
|
|
731
|
-
// Case C: no markers → fallback to preparation.firstKeptEntryId and no QA section.
|
|
732
|
-
const fallback = buildPlanningCompactionResult(
|
|
733
|
-
{
|
|
734
|
-
type: "session_before_compact",
|
|
735
|
-
preparation: makePlanningPreparation("## Previous\nEarlier summary."),
|
|
736
|
-
branchEntries: [],
|
|
737
|
-
reason: "threshold",
|
|
738
|
-
willRetry: false,
|
|
739
|
-
signal: new AbortController().signal,
|
|
740
|
-
},
|
|
741
|
-
ctx,
|
|
742
|
-
);
|
|
743
|
-
assert.ok(fallback);
|
|
744
|
-
assert.deepEqual(fallback!.firstKeptEntryId, "fallback");
|
|
745
|
-
assert.doesNotMatch(fallback!.summary, /## Q&A During Planning/);
|
|
746
|
-
assert.match(fallback!.summary, /## Previous Compact Summary/);
|
|
800
|
+
assert.match(withPlan!.summary, /\[Session Goal\]/);
|
|
801
|
+
assert.match(withPlan!.summary, new RegExp(run.run_id));
|
|
802
|
+
assert.match(withPlan!.summary, /\[Outstanding Context\]/);
|
|
803
|
+
assert.match(withPlan!.summary, /Latest plan path from session/);
|
|
804
|
+
assert.match(withPlan!.summary, /Planning artifact directory from session/);
|
|
805
|
+
assert.match(withPlan!.summary, /Current implementation marker observed during planning: I-004/);
|
|
806
|
+
assert.match(withPlan!.summary, /Previous compact summary: Previous Earlier summary\./);
|
|
807
|
+
assert.equal((withPlan!.details as any).compactor, "pi-vcc");
|
|
808
|
+
assert.equal((withPlan!.details as any).phase, "planning");
|
|
747
809
|
});
|
|
748
810
|
|
|
749
|
-
it("planning hook is gated by run.status=planning and defers
|
|
811
|
+
it("planning hook is gated by run.status=planning and defers while execution is running", async () => {
|
|
750
812
|
const workdir = freshWorkdir();
|
|
751
|
-
const {
|
|
813
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
752
814
|
initState(workdir);
|
|
753
815
|
const { run } = startRun(workdir, { topic: "planning gate", skill: "plan-small", requestText: "x" });
|
|
754
816
|
|
|
755
|
-
|
|
756
|
-
const resultPlanning = handlePlanningBeforeCompact({} as any, ctx as any, {
|
|
817
|
+
const resultPlanning = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
757
818
|
type: "session_before_compact",
|
|
758
|
-
preparation:
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
turnPrefixMessages: [],
|
|
762
|
-
isSplitTurn: false,
|
|
763
|
-
tokensBefore: 1,
|
|
764
|
-
previousSummary: null,
|
|
765
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
766
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
767
|
-
},
|
|
768
|
-
branchEntries: [],
|
|
819
|
+
preparation: makePreparation("threshold", null),
|
|
820
|
+
branchEntries: compactableBranchEntries(),
|
|
821
|
+
customInstructions: "keep:1",
|
|
769
822
|
reason: "threshold",
|
|
770
823
|
willRetry: false,
|
|
771
824
|
signal: new AbortController().signal,
|
|
772
825
|
});
|
|
773
|
-
assert.ok(resultPlanning?.compaction
|
|
826
|
+
assert.ok(resultPlanning?.compaction);
|
|
774
827
|
|
|
775
|
-
// Flip status to done; planning hook should refuse.
|
|
776
828
|
setRunStatus(workdir, run.run_id, "done");
|
|
777
|
-
|
|
778
|
-
const resultDone = handlePlanningBeforeCompact({} as any, ctx as any, {
|
|
829
|
+
const resultDone = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
779
830
|
type: "session_before_compact",
|
|
780
|
-
preparation:
|
|
781
|
-
|
|
782
|
-
messagesToSummarize: [],
|
|
783
|
-
turnPrefixMessages: [],
|
|
784
|
-
isSplitTurn: false,
|
|
785
|
-
tokensBefore: 1,
|
|
786
|
-
previousSummary: null,
|
|
787
|
-
fileOps: { read: [], written: [], edited: [] },
|
|
788
|
-
settings: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
789
|
-
},
|
|
790
|
-
branchEntries: [],
|
|
831
|
+
preparation: makePreparation("threshold", null),
|
|
832
|
+
branchEntries: compactableBranchEntries(),
|
|
791
833
|
reason: "threshold",
|
|
792
834
|
willRetry: false,
|
|
793
835
|
signal: new AbortController().signal,
|
|
794
836
|
});
|
|
795
837
|
assert.equal(resultDone, undefined);
|
|
838
|
+
|
|
839
|
+
setRunStatus(workdir, run.run_id, "planning");
|
|
840
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v14.md"), items("VC-001"));
|
|
841
|
+
const duringExecution = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
842
|
+
type: "session_before_compact",
|
|
843
|
+
preparation: makePreparation("threshold", null),
|
|
844
|
+
branchEntries: compactableBranchEntries(),
|
|
845
|
+
reason: "threshold",
|
|
846
|
+
willRetry: false,
|
|
847
|
+
signal: new AbortController().signal,
|
|
848
|
+
});
|
|
849
|
+
assert.equal(duringExecution, undefined);
|
|
850
|
+
await stopExecution(pi, ctx, "planning-gate");
|
|
796
851
|
});
|
|
797
852
|
|
|
798
853
|
it("turn_end writes are unconditionally deferred even when isIdle reads true", async () => {
|
|
@@ -970,7 +1025,7 @@ describe("execution loop", () => {
|
|
|
970
1025
|
await startExecution(pi, ctx, path.join(workdir, "PLAN_v18.md"), items("VC-001"), [
|
|
971
1026
|
{ id: "I-001", text: "First item." },
|
|
972
1027
|
]);
|
|
973
|
-
const rules = executionContextMessage()!;
|
|
1028
|
+
const rules = executionContextMessage(ctx)!;
|
|
974
1029
|
assert.match(rules, /\[I-001:implemented\]/);
|
|
975
1030
|
assert.match(rules, /\[I-001:validating\]/);
|
|
976
1031
|
assert.match(rules, /subprocess-backed verification/);
|
|
@@ -981,42 +1036,411 @@ describe("execution loop", () => {
|
|
|
981
1036
|
await stopExecution(pi, ctx, "done");
|
|
982
1037
|
});
|
|
983
1038
|
|
|
984
|
-
it("
|
|
1039
|
+
it("does not proactively request planning compaction", () => {
|
|
985
1040
|
const workdir = freshWorkdir();
|
|
986
|
-
const {
|
|
1041
|
+
const { ctx, setUsagePercent, recorded } = makeHarness(workdir);
|
|
987
1042
|
initState(workdir);
|
|
988
|
-
startRun(workdir, { topic: "planning
|
|
1043
|
+
startRun(workdir, { topic: "planning no-op", skill: "plan-normal", requestText: "x" });
|
|
989
1044
|
|
|
990
1045
|
setUsagePercent(120);
|
|
991
|
-
assert.equal(shouldTriggerPlanningCompaction(ctx as any), true);
|
|
992
|
-
requestPlanningCompaction(ctx as any);
|
|
993
|
-
// In flight, second trigger ignored.
|
|
994
|
-
requestPlanningCompaction(ctx as any);
|
|
995
1046
|
assert.equal(shouldTriggerPlanningCompaction(ctx as any), false);
|
|
1047
|
+
requestPlanningCompaction(ctx as any);
|
|
1048
|
+
refreshPlanningCompactionCooldown(ctx as any);
|
|
1049
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "planning scheduling is owned by Pi core");
|
|
1050
|
+
});
|
|
996
1051
|
|
|
997
|
-
|
|
998
|
-
|
|
1052
|
+
it("auto-continues planning only for threshold/overflow and honors manual follow-up prompts", async () => {
|
|
1053
|
+
const workdir = freshWorkdir();
|
|
1054
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1055
|
+
(ctx as any).piVersion = "0.84.3";
|
|
1056
|
+
initState(workdir);
|
|
1057
|
+
startRun(workdir, { topic: "planning success", skill: "plan-normal", requestText: "x" });
|
|
1058
|
+
|
|
1059
|
+
const threshold = handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1060
|
+
type: "session_before_compact",
|
|
1061
|
+
preparation: makePreparation("threshold", null),
|
|
1062
|
+
branchEntries: compactableBranchEntries(),
|
|
1063
|
+
customInstructions: "keep:1",
|
|
1064
|
+
reason: "threshold",
|
|
1065
|
+
willRetry: false,
|
|
1066
|
+
signal: new AbortController().signal,
|
|
1067
|
+
});
|
|
1068
|
+
assert.ok(threshold?.compaction);
|
|
1069
|
+
await handlePlanningCompact(pi as any, ctx as any, {
|
|
999
1070
|
type: "session_compact",
|
|
1000
1071
|
compactionEntry: { type: "compaction" } as never,
|
|
1001
|
-
fromExtension:
|
|
1002
|
-
reason: "
|
|
1072
|
+
fromExtension: false,
|
|
1073
|
+
reason: "threshold",
|
|
1003
1074
|
willRetry: false,
|
|
1004
1075
|
});
|
|
1005
|
-
|
|
1006
|
-
assert.ok(resume);
|
|
1076
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-plan-resume").length, 1);
|
|
1007
1077
|
assert.equal(consumePlanningCompactionResumeGuard(ctx as any), true);
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1078
|
+
|
|
1079
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1080
|
+
type: "session_before_compact",
|
|
1081
|
+
preparation: makePreparation("manual", null),
|
|
1082
|
+
branchEntries: compactableBranchEntries(),
|
|
1083
|
+
customInstructions: "Ask the next question keep:1",
|
|
1084
|
+
reason: "manual",
|
|
1085
|
+
willRetry: false,
|
|
1086
|
+
signal: new AbortController().signal,
|
|
1087
|
+
});
|
|
1088
|
+
await handlePlanningCompact(pi as any, ctx as any, {
|
|
1089
|
+
type: "session_compact",
|
|
1090
|
+
compactionEntry: { type: "compaction" } as never,
|
|
1091
|
+
fromExtension: false,
|
|
1092
|
+
reason: "manual",
|
|
1093
|
+
willRetry: false,
|
|
1094
|
+
});
|
|
1095
|
+
assert.deepEqual(recorded.userMessages, ["Ask the next question"]);
|
|
1096
|
+
assert.equal(recorded.messages.filter((message) => message.customType === "pi-plans-plan-resume").length, 1);
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
it("planning request helper remains a no-op regardless of idleness", () => {
|
|
1100
|
+
const workdir = freshWorkdir();
|
|
1101
|
+
const { ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
1102
|
+
initState(workdir);
|
|
1103
|
+
startRun(workdir, { topic: "idle no-op", skill: "plan-normal", requestText: "x" });
|
|
1013
1104
|
setUsagePercent(120);
|
|
1014
|
-
|
|
1105
|
+
|
|
1106
|
+
(ctx as any).isIdle = () => false;
|
|
1107
|
+
requestPlanningCompaction(ctx as any);
|
|
1108
|
+
(ctx as any).isIdle = () => true;
|
|
1109
|
+
(ctx as any).hasPendingMessages = () => false;
|
|
1110
|
+
requestPlanningCompaction(ctx as any);
|
|
1111
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
1112
|
+
assert.equal((ctx as any).sessionManager.__planningCompaction, undefined);
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1115
|
+
it("planning compact failures notify but do not re-request proactively", () => {
|
|
1116
|
+
const workdir = freshWorkdir();
|
|
1117
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1118
|
+
initState(workdir);
|
|
1119
|
+
startRun(workdir, { topic: "failure notify", skill: "plan-normal", requestText: "x" });
|
|
1120
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1121
|
+
type: "session_before_compact",
|
|
1122
|
+
preparation: makePreparation("threshold", null),
|
|
1123
|
+
branchEntries: compactableBranchEntries(),
|
|
1124
|
+
customInstructions: "keep:1",
|
|
1125
|
+
reason: "threshold",
|
|
1126
|
+
willRetry: false,
|
|
1127
|
+
signal: new AbortController().signal,
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1131
|
+
type: "session_compact_failed",
|
|
1132
|
+
reason: "manual",
|
|
1133
|
+
errorMessage: "Compaction failed: Nothing to compact (session too small)",
|
|
1134
|
+
aborted: false,
|
|
1135
|
+
willRetry: false,
|
|
1136
|
+
fromExtension: false,
|
|
1137
|
+
});
|
|
1138
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("nothing to summarize")));
|
|
1139
|
+
requestPlanningCompaction(ctx as any);
|
|
1140
|
+
assert.equal(recorded.compacts?.length ?? 0, 0);
|
|
1141
|
+
|
|
1142
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1143
|
+
type: "session_before_compact",
|
|
1144
|
+
preparation: makePreparation("threshold", null),
|
|
1145
|
+
branchEntries: compactableBranchEntries(),
|
|
1146
|
+
customInstructions: "keep:1",
|
|
1147
|
+
reason: "threshold",
|
|
1148
|
+
willRetry: false,
|
|
1149
|
+
signal: new AbortController().signal,
|
|
1150
|
+
});
|
|
1151
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1152
|
+
type: "session_compact_failed",
|
|
1153
|
+
reason: "manual",
|
|
1154
|
+
errorMessage: "network down",
|
|
1155
|
+
aborted: false,
|
|
1156
|
+
willRetry: false,
|
|
1157
|
+
fromExtension: false,
|
|
1158
|
+
});
|
|
1159
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("will try again")));
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("execution turn compaction helper remains a no-op and failures keep execution active", async () => {
|
|
1163
|
+
const workdir = freshWorkdir();
|
|
1164
|
+
startActiveRun(workdir);
|
|
1165
|
+
const { pi, ctx, recorded, setUsagePercent } = makeHarness(workdir);
|
|
1166
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v23.md"), items("VC-001"), [
|
|
1167
|
+
{ id: "I-001", text: "First item." },
|
|
1168
|
+
]);
|
|
1169
|
+
|
|
1170
|
+
setUsagePercent(96);
|
|
1171
|
+
handleExecutionTurnCompaction(ctx);
|
|
1172
|
+
assert.equal(recorded.compacts?.length ?? 0, 0, "execution scheduling is owned by Pi core");
|
|
1173
|
+
|
|
1174
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1175
|
+
type: "session_before_compact",
|
|
1176
|
+
preparation: makePreparation("threshold", null),
|
|
1177
|
+
branchEntries: compactableBranchEntries(),
|
|
1178
|
+
customInstructions: "keep:1",
|
|
1179
|
+
reason: "threshold",
|
|
1180
|
+
willRetry: false,
|
|
1181
|
+
signal: new AbortController().signal,
|
|
1182
|
+
});
|
|
1183
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1184
|
+
type: "session_compact_failed",
|
|
1185
|
+
reason: "manual",
|
|
1186
|
+
errorMessage: "Compaction failed: Already compacted",
|
|
1187
|
+
aborted: false,
|
|
1188
|
+
willRetry: false,
|
|
1189
|
+
fromExtension: false,
|
|
1190
|
+
});
|
|
1191
|
+
assert.ok(getExecution());
|
|
1192
|
+
assert.ok(recorded.notifies.some((note) => note.message.includes("nothing to summarize")));
|
|
1193
|
+
|
|
1194
|
+
await stopExecution(pi, ctx, "test-done");
|
|
1195
|
+
});
|
|
1196
|
+
|
|
1197
|
+
it("treats planning abort/stream compact failures as terminal and notifies explicitly", () => {
|
|
1198
|
+
const workdir = freshWorkdir();
|
|
1199
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1200
|
+
initState(workdir);
|
|
1201
|
+
startRun(workdir, { topic: "abort-stream classification", skill: "plan-normal", requestText: "x" });
|
|
1202
|
+
|
|
1203
|
+
const abortMessages = [
|
|
1204
|
+
"Auto-compaction failed: Turn prefix summarization failed: This operation was aborted",
|
|
1205
|
+
"Error: OpenAI Responses stream ended before a terminal response event",
|
|
1206
|
+
"Auto-compaction failed: context overflow recovery failed",
|
|
1207
|
+
"this operation was aborted",
|
|
1208
|
+
"aborted",
|
|
1209
|
+
];
|
|
1210
|
+
for (const errorMessage of abortMessages) {
|
|
1211
|
+
recorded.notifies.length = 0;
|
|
1212
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1213
|
+
type: "session_before_compact",
|
|
1214
|
+
preparation: makePreparation("threshold", null),
|
|
1215
|
+
branchEntries: compactableBranchEntries(),
|
|
1216
|
+
customInstructions: "keep:1",
|
|
1217
|
+
reason: "threshold",
|
|
1218
|
+
willRetry: false,
|
|
1219
|
+
signal: new AbortController().signal,
|
|
1220
|
+
});
|
|
1221
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1222
|
+
type: "session_compact_failed",
|
|
1223
|
+
reason: "threshold",
|
|
1224
|
+
errorMessage,
|
|
1225
|
+
aborted: errorMessage.includes("aborted"),
|
|
1226
|
+
willRetry: false,
|
|
1227
|
+
fromExtension: false,
|
|
1228
|
+
});
|
|
1229
|
+
const backoffNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1230
|
+
assert.ok(backoffNote, `expected abort-class notify for: ${errorMessage}`);
|
|
1231
|
+
assert.match(backoffNote!.message, /provider interruption|competing manual/);
|
|
1232
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1233
|
+
requestPlanningCompaction(ctx as any);
|
|
1234
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
it("event.aborted=true with empty errorMessage still enters abort-class handling", () => {
|
|
1239
|
+
const workdir = freshWorkdir();
|
|
1240
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1241
|
+
initState(workdir);
|
|
1242
|
+
startRun(workdir, { topic: "aborted-no-message", skill: "plan-normal", requestText: "x" });
|
|
1243
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1244
|
+
type: "session_before_compact",
|
|
1245
|
+
preparation: makePreparation("threshold", null),
|
|
1246
|
+
branchEntries: compactableBranchEntries(),
|
|
1247
|
+
customInstructions: "keep:1",
|
|
1248
|
+
reason: "threshold",
|
|
1249
|
+
willRetry: false,
|
|
1250
|
+
signal: new AbortController().signal,
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1254
|
+
type: "session_compact_failed",
|
|
1255
|
+
reason: "threshold",
|
|
1256
|
+
errorMessage: undefined,
|
|
1257
|
+
aborted: true,
|
|
1258
|
+
willRetry: false,
|
|
1259
|
+
fromExtension: false,
|
|
1260
|
+
});
|
|
1261
|
+
const backoffNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1262
|
+
assert.ok(backoffNote);
|
|
1263
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1264
|
+
requestPlanningCompaction(ctx as any);
|
|
1265
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
it("network-style planning failures stay retryable without proactive requests", () => {
|
|
1269
|
+
const workdir = freshWorkdir();
|
|
1270
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1271
|
+
initState(workdir);
|
|
1272
|
+
startRun(workdir, { topic: "network-retryable", skill: "plan-normal", requestText: "x" });
|
|
1273
|
+
handlePlanningBeforeCompact(pi as any, ctx as any, {
|
|
1274
|
+
type: "session_before_compact",
|
|
1275
|
+
preparation: makePreparation("threshold", null),
|
|
1276
|
+
branchEntries: compactableBranchEntries(),
|
|
1277
|
+
customInstructions: "keep:1",
|
|
1278
|
+
reason: "threshold",
|
|
1279
|
+
willRetry: false,
|
|
1280
|
+
signal: new AbortController().signal,
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
handlePlanningCompactFailed(pi as any, ctx as any, {
|
|
1284
|
+
type: "session_compact_failed",
|
|
1285
|
+
reason: "threshold",
|
|
1286
|
+
errorMessage: "network down",
|
|
1287
|
+
aborted: false,
|
|
1288
|
+
willRetry: false,
|
|
1289
|
+
fromExtension: false,
|
|
1290
|
+
});
|
|
1291
|
+
const abortNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1292
|
+
assert.equal(abortNote, undefined, "network down must not be classified as abort-class");
|
|
1293
|
+
const retryNote = recorded.notifies.find((n) => n.message.includes("will try again"));
|
|
1294
|
+
assert.ok(retryNote, "network down must keep the retryable path");
|
|
1295
|
+
const before = recorded.compacts?.length ?? 0;
|
|
1296
|
+
requestPlanningCompaction(ctx as any);
|
|
1297
|
+
assert.equal((recorded.compacts?.length ?? 0) - before, 0);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
it("lifecycle flags distinguish unhinted and phase-attributed compactions", () => {
|
|
1301
|
+
const workdir = freshWorkdir();
|
|
1302
|
+
const { ctx } = makeHarness(workdir);
|
|
1303
|
+
|
|
1304
|
+
noteCompactionStarted(ctx as any, undefined);
|
|
1305
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), true, "unhinted compaction marks planning inFlight");
|
|
1306
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), true, "unhinted compaction marks execution inFlight");
|
|
1307
|
+
noteCompactionEnded(ctx as any, undefined);
|
|
1308
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1309
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1310
|
+
|
|
1311
|
+
noteCompactionStarted(ctx as any, "pi-plans planning auto compact");
|
|
1312
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), true);
|
|
1313
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1314
|
+
noteCompactionEnded(ctx as any, "pi-plans planning auto compact");
|
|
1315
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1316
|
+
|
|
1317
|
+
noteCompactionStarted(ctx as any, "pi-plans execution auto compact");
|
|
1318
|
+
assert.equal(compactionInFlight(ctx as any, "planning"), false);
|
|
1319
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), true);
|
|
1320
|
+
noteCompactionEnded(ctx as any, "pi-plans execution auto compact");
|
|
1321
|
+
assert.equal(compactionInFlight(ctx as any, "execution"), false);
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
it("execution handler classifies abort/stream failures as terminal", async () => {
|
|
1325
|
+
const workdir = freshWorkdir();
|
|
1326
|
+
startActiveRun(workdir);
|
|
1327
|
+
const { pi, ctx, recorded } = makeHarness(workdir);
|
|
1328
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v4.md"), items("VC-001"));
|
|
1329
|
+
|
|
1330
|
+
const abortMessages = [
|
|
1331
|
+
"Auto-compaction failed: Turn prefix summarization failed: This operation was aborted",
|
|
1332
|
+
"Error: OpenAI Responses stream ended before a terminal response event",
|
|
1333
|
+
"Auto-compaction failed: context overflow recovery failed",
|
|
1334
|
+
"aborted",
|
|
1335
|
+
];
|
|
1336
|
+
for (const errorMessage of abortMessages) {
|
|
1337
|
+
recorded.notifies.length = 0;
|
|
1338
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1339
|
+
type: "session_before_compact",
|
|
1340
|
+
preparation: makePreparation("threshold", null),
|
|
1341
|
+
branchEntries: compactableBranchEntries(),
|
|
1342
|
+
customInstructions: "keep:1",
|
|
1343
|
+
reason: "threshold",
|
|
1344
|
+
willRetry: false,
|
|
1345
|
+
signal: new AbortController().signal,
|
|
1346
|
+
});
|
|
1347
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1348
|
+
type: "session_compact_failed",
|
|
1349
|
+
reason: "threshold",
|
|
1350
|
+
errorMessage,
|
|
1351
|
+
aborted: errorMessage.includes("aborted"),
|
|
1352
|
+
willRetry: false,
|
|
1353
|
+
fromExtension: false,
|
|
1354
|
+
});
|
|
1355
|
+
const note = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1356
|
+
assert.ok(note, `expected abort-class notify for: ${errorMessage}`);
|
|
1357
|
+
assert.match(note!.message, /provider interruption|competing manual/);
|
|
1358
|
+
}
|
|
1359
|
+
handleExecutionBeforeCompact(pi, ctx, {
|
|
1360
|
+
type: "session_before_compact",
|
|
1361
|
+
preparation: makePreparation("threshold", null),
|
|
1362
|
+
branchEntries: compactableBranchEntries(),
|
|
1363
|
+
customInstructions: "keep:1",
|
|
1364
|
+
reason: "threshold",
|
|
1365
|
+
willRetry: false,
|
|
1366
|
+
signal: new AbortController().signal,
|
|
1367
|
+
});
|
|
1368
|
+
handleExecutionCompactFailed(pi, ctx, {
|
|
1369
|
+
type: "session_compact_failed",
|
|
1370
|
+
reason: "threshold",
|
|
1371
|
+
errorMessage: undefined,
|
|
1372
|
+
aborted: true,
|
|
1373
|
+
willRetry: false,
|
|
1374
|
+
fromExtension: false,
|
|
1375
|
+
});
|
|
1376
|
+
const cleanNote = recorded.notifies.find((n) => n.message.includes("was aborted"));
|
|
1377
|
+
assert.ok(cleanNote, "aborted-without-message must enter abort-class handling on execution side too");
|
|
1378
|
+
await stopExecution(pi, ctx, "test-done");
|
|
1379
|
+
});
|
|
1380
|
+
|
|
1381
|
+
it("index.ts wires session_compact/session_compact_failed to clear inFlight flags (F-003/F-004 coverage)", () => {
|
|
1382
|
+
const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
|
|
1383
|
+
assert.match(indexSource, /await handleExecutionCompact\(pi, ctx, event\)/);
|
|
1384
|
+
assert.match(indexSource, /await handlePlanningCompact\(pi, ctx, event\)/);
|
|
1385
|
+
|
|
1386
|
+
const workdir = freshWorkdir();
|
|
1387
|
+
// Use a minimal harness that exposes sessionManager so we can inspect the
|
|
1388
|
+
// in-flight store after dispatching the registered event handlers.
|
|
1389
|
+
const ctx: any = {
|
|
1390
|
+
cwd: workdir,
|
|
1391
|
+
hasUI: false,
|
|
1392
|
+
sessionManager: {},
|
|
1393
|
+
};
|
|
1394
|
+
// Simulate the lifecycle that index.ts wires in production:
|
|
1395
|
+
noteCompactionStarted(ctx, undefined);
|
|
1396
|
+
assert.equal(compactionInFlight(ctx, "planning"), true);
|
|
1397
|
+
assert.equal(compactionInFlight(ctx, "execution"), true);
|
|
1398
|
+
noteCompactionEnded(ctx, undefined);
|
|
1399
|
+
assert.equal(compactionInFlight(ctx, "planning"), false);
|
|
1400
|
+
assert.equal(compactionInFlight(ctx, "execution"), false);
|
|
1401
|
+
// And a planning-attributed start + end still clears both (end has no hint).
|
|
1402
|
+
noteCompactionStarted(ctx, "pi-plans planning auto compact");
|
|
1403
|
+
assert.equal(compactionInFlight(ctx, "planning"), true);
|
|
1404
|
+
noteCompactionEnded(ctx, "pi-plans planning auto compact");
|
|
1405
|
+
assert.equal(compactionInFlight(ctx, "planning"), false);
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
describe("execution injection reads graph config live", () => {
|
|
1410
|
+
it("reflects graph_enabled flips on the next assembly and surfaces config failures", async (t) => {
|
|
1411
|
+
const workdir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-exec-graph-"));
|
|
1412
|
+
t.after(() => {
|
|
1413
|
+
try {
|
|
1414
|
+
fs.rmSync(workdir, { recursive: true, force: true });
|
|
1415
|
+
} catch {
|
|
1416
|
+
/* ignore */
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
const { pi, ctx } = makeHarness(workdir);
|
|
1420
|
+
setGraphEnabled(workdir, true);
|
|
1421
|
+
await startExecution(pi, ctx, path.join(workdir, "PLAN_v1.md"), items("VC-001"));
|
|
1422
|
+
|
|
1423
|
+
const enabledRules = executionContextMessage(ctx)!;
|
|
1424
|
+
assert.match(enabledRules, /Code graph loop: indexed code files read as a function digest/);
|
|
1425
|
+
assert.doesNotMatch(enabledRules, /Code graph disabled/);
|
|
1426
|
+
|
|
1427
|
+
setGraphEnabled(workdir, false);
|
|
1428
|
+
assert.match(executionContextMessage(ctx)!, /Code graph disabled/);
|
|
1429
|
+
|
|
1430
|
+
fs.writeFileSync(path.join(workdir, ".git", "pi_plans", "config.json"), "{broken");
|
|
1431
|
+
const brokenRules = executionContextMessage(ctx)!;
|
|
1432
|
+
assert.match(brokenRules, /Code graph disabled/);
|
|
1433
|
+
assert.match(brokenRules, /config unreadable this turn/);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
it("keeps no graphEnabled snapshot in the execution state source", () => {
|
|
1437
|
+
const source = fs.readFileSync(path.join(process.cwd(), "src", "exec.ts"), "utf8");
|
|
1438
|
+
assert.doesNotMatch(source, /graphEnabled/);
|
|
1439
|
+
assert.match(source, /resolveGraphMode/);
|
|
1015
1440
|
});
|
|
1016
1441
|
});
|
|
1017
1442
|
|
|
1018
|
-
function makePreparation(reason: "manual" | "threshold" | "overflow", previousSummary: string | null): any {
|
|
1019
|
-
return {
|
|
1443
|
+
function makePreparation(reason: "manual" | "threshold" | "overflow", previousSummary: string | null): any { return {
|
|
1020
1444
|
firstKeptEntryId: "a-2",
|
|
1021
1445
|
messagesToSummarize: [],
|
|
1022
1446
|
turnPrefixMessages: [],
|