pi-plans 0.2.0 → 0.3.1

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.
Files changed (75) hide show
  1. package/README.md +90 -26
  2. package/agents/ref-analyst.md +18 -0
  3. package/index.ts +121 -9
  4. package/package.json +16 -1
  5. package/references/pi-planning-workflow.md +21 -6
  6. package/references/state-and-config.md +52 -5
  7. package/scripts/validate.ts +5 -0
  8. package/skills/plan-with-refs/SKILL.md +3 -3
  9. package/src/code-graph/commands.ts +483 -0
  10. package/src/code-graph/discovery.ts +118 -0
  11. package/src/code-graph/git.ts +108 -0
  12. package/src/code-graph/identity.ts +59 -0
  13. package/src/code-graph/indexer.ts +281 -0
  14. package/src/code-graph/materialize.ts +166 -0
  15. package/src/code-graph/mode.ts +28 -0
  16. package/src/code-graph/mutations.ts +160 -0
  17. package/src/code-graph/parser.ts +51 -0
  18. package/src/code-graph/parsers/javascript.ts +35 -0
  19. package/src/code-graph/parsers/python.ts +160 -0
  20. package/src/code-graph/parsers/tree-sitter.ts +316 -0
  21. package/src/code-graph/paths.ts +85 -0
  22. package/src/code-graph/prompts.ts +18 -0
  23. package/src/code-graph/resolver.ts +69 -0
  24. package/src/code-graph/runtime.ts +158 -0
  25. package/src/code-graph/schema.ts +135 -0
  26. package/src/code-graph/screening.ts +82 -0
  27. package/src/code-graph/store.ts +278 -0
  28. package/src/code-graph/summary.ts +435 -0
  29. package/src/code-graph/types.ts +163 -0
  30. package/src/compaction.ts +1125 -371
  31. package/src/config-command.ts +361 -0
  32. package/src/exec.ts +508 -693
  33. package/src/guard.ts +14 -1
  34. package/src/refine-prompts.ts +109 -0
  35. package/src/refine-ui-helpers.ts +71 -18
  36. package/src/refine-ui-state.ts +88 -22
  37. package/src/refine-ui.ts +210 -102
  38. package/src/state.ts +36 -7
  39. package/src/subagent.ts +164 -61
  40. package/src/termination-prompt.ts +22 -0
  41. package/tests/analyze-refs.test.ts +265 -0
  42. package/tests/ask-choice.test.ts +264 -0
  43. package/tests/autocomplete.test.ts +6 -1
  44. package/tests/code-graph-apply-action.test.ts +173 -0
  45. package/tests/code-graph-apply.test.ts +185 -0
  46. package/tests/code-graph-commands.test.ts +211 -0
  47. package/tests/code-graph-db.test.ts +166 -0
  48. package/tests/code-graph-discovery.test.ts +38 -0
  49. package/tests/code-graph-git.test.ts +94 -0
  50. package/tests/code-graph-index.test.ts +175 -0
  51. package/tests/code-graph-loop.e2e.test.ts +159 -0
  52. package/tests/code-graph-mutations.test.ts +117 -0
  53. package/tests/code-graph-parser.test.ts +85 -0
  54. package/tests/code-graph-rollback.test.ts +100 -0
  55. package/tests/code-graph-summary-batching.test.ts +518 -0
  56. package/tests/code-graph-summary.test.ts +148 -0
  57. package/tests/compaction.test.ts +371 -57
  58. package/tests/config-command.test.ts +263 -0
  59. package/tests/exec.test.ts +808 -241
  60. package/tests/fixtures/code-graph/sample.js +36 -0
  61. package/tests/fixtures/code-graph/sample.py +20 -0
  62. package/tests/fixtures/code-graph/sample.ts +15 -0
  63. package/tests/graph-aware-file-tools.test.ts +411 -0
  64. package/tests/guard.test.ts +27 -1
  65. package/tests/plans.test.ts +10 -0
  66. package/tests/refine-prompts.test.ts +101 -2
  67. package/tests/refine-ui.test.ts +371 -72
  68. package/tests/state.test.ts +32 -0
  69. package/tests/subagent.test.ts +48 -20
  70. package/tools/analyze-refs.ts +263 -0
  71. package/tools/ask-choice.ts +159 -11
  72. package/tools/code-graph.ts +277 -0
  73. package/tools/graph-aware-file-tools.ts +392 -0
  74. package/tools/plans.ts +97 -2
  75. package/tools/refine.ts +61 -15
@@ -1,74 +1,388 @@
1
1
  import * as assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
2
+ import * as fs from "node:fs";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { after, before, describe, it } from "node:test";
3
6
  import {
4
- extractReadRecords,
5
- formatReadRecord,
6
- legalFirstKeptEntryIndex,
7
- mergeCompactionDetails,
8
- planIAwareCompaction,
9
- currentIExceedsTrigger,
7
+ buildOwnCut,
8
+ buildPiPlansVccCompaction,
9
+ compactionCurrentI,
10
+ DEFAULT_VCC_SETTINGS,
11
+ loadVccSettings,
12
+ parseCompactionInstructions,
13
+ PI_VCC_COMPACT_INSTRUCTION,
14
+ scaffoldVccSettings,
15
+ shouldScheduleAutoContinue,
16
+ vccSettingsPath,
17
+ type CompactionEntryLike,
10
18
  } from "../src/compaction.ts";
11
19
 
12
- function textEntry(id: string, text: string, tokens = 100) {
13
- return { id, type: "message", tokens, message: { role: "assistant", content: [{ type: "text", text }] } };
20
+ function textEntry(id: string, role: string, text: string): CompactionEntryLike {
21
+ return { id, type: "message", message: { role, content: [{ type: "text", text }] } };
14
22
  }
15
23
 
16
- describe("I-aware compaction policy", () => {
17
- it("keeps a legal current-I suffix and never starts at a tool result", () => {
24
+ function toolCallEntry(id: string, name: string, args: Record<string, unknown>): CompactionEntryLike {
25
+ return { id, type: "message", message: { role: "assistant", content: [{ type: "toolCall", name, arguments: args }] } };
26
+ }
27
+
28
+ function toolResultEntry(id: string, name: string, text: string): CompactionEntryLike {
29
+ return { id, type: "message", message: { role: "toolResult", toolName: name, content: [{ type: "text", text }] } };
30
+ }
31
+
32
+ describe("pi-vcc compaction", () => {
33
+ let tmpRoot: string;
34
+
35
+ before(() => {
36
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-vcc-"));
37
+ });
38
+
39
+ after(() => {
40
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
41
+ });
42
+
43
+ it("builds an own cut that keeps the requested recent user turns", () => {
18
44
  const entries = [
19
- textEntry("i1", "[I-001:current] completed"),
20
- { id: "call", type: "message", tokens: 100, message: { role: "assistant", content: [{ type: "toolCall", id: "read-1", name: "read", arguments: { path: "/repo/a.ts", offset: 4, limit: 2 } }] } },
21
- { id: "result", type: "message", tokens: 100, message: { role: "toolResult", toolCallId: "read-1", content: [{ type: "text", text: "private source output that must be bounded" }] } },
22
- textEntry("i2", "[I-002:current] active"),
23
- { id: "u", type: "message", tokens: 100, message: { role: "user", content: [{ type: "text", text: "latest question" }] } },
24
- textEntry("a", "latest answer"),
45
+ textEntry("u-1", "user", "implement compact support"),
46
+ textEntry("a-1", "assistant", "started the helper"),
47
+ textEntry("u-2", "user", "continue from the current task"),
48
+ textEntry("a-2", "assistant", "working on tests"),
25
49
  ];
26
- const plan = planIAwareCompaction({ entries, currentI: "I-002", knownIIds: ["I-001", "I-002"], contextWindow: 1000, tokensBefore: 600 });
27
- assert.equal(plan.currentI, "I-002");
28
- assert.equal(plan.slices.filter((slice) => slice.id !== null).length, 2);
29
- assert.ok(plan.firstKeptEntryId);
30
- assert.notEqual(plan.firstKeptEntryId, "result");
31
- assert.ok(plan.summaryEntries.every((entry) => !plan.keptEntries.includes(entry)));
32
- assert.equal(legalFirstKeptEntryIndex(entries, 2), 1);
50
+ const cut = buildOwnCut(entries, 1);
51
+ assert.equal(cut.ok, true);
52
+ assert.equal(cut.ok ? cut.firstKeptEntryId : undefined, "u-2");
53
+ assert.deepEqual(cut.ok ? cut.messages.map((message) => message.role) : [], ["user", "assistant"]);
54
+
55
+ const compactAll = buildOwnCut(entries, 0);
56
+ assert.equal(compactAll.ok, true);
57
+ assert.equal(compactAll.ok ? compactAll.compactAll : false, true);
58
+ assert.equal(compactAll.ok ? compactAll.firstKeptEntryId : "not-ok", "");
59
+ assert.deepEqual(buildOwnCut([textEntry("u", "user", "only one turn")], 1), { ok: false, reason: "too_few_live_messages" });
33
60
  });
34
61
 
35
- it("starts a new current-I slice after a prior compaction snapshot", () => {
62
+ it("reads legacy compaction details without writing the old schema", () => {
63
+ const legacyCompaction: CompactionEntryLike = {
64
+ id: "old-compact",
65
+ type: "compaction",
66
+ details: {
67
+ kind: "pi-plans-execution-compaction",
68
+ readRecords: [
69
+ { path: "src/legacy.ts", lineStart: 5, lineEnd: 9, range: "5-9", summary: "legacy facts", key: "src/legacy.ts|5-9" },
70
+ ],
71
+ metrics: {
72
+ currentI: "I-007",
73
+ firstKeptEntryId: "u-2",
74
+ targetMet: false,
75
+ hardFloorReason: "single oversized tool result",
76
+ },
77
+ },
78
+ };
79
+ assert.equal(compactionCurrentI(legacyCompaction), "I-007");
80
+
36
81
  const entries = [
37
- { id: "old-summary", type: "compaction", details: { currentI: "I-002" } },
38
- textEntry("u", "new question", 100),
39
- textEntry("a", "new answer", 100),
82
+ textEntry("u-1", "user", "old prefix"),
83
+ textEntry("a-1", "assistant", "old answer"),
84
+ legacyCompaction,
85
+ textEntry("u-2", "user", "retained user from old boundary"),
86
+ textEntry("a-2", "assistant", "live answer"),
87
+ textEntry("u-3", "user", "latest user"),
88
+ textEntry("a-3", "assistant", "latest answer"),
40
89
  ];
41
- const plan = planIAwareCompaction({ entries, currentI: "I-002", knownIIds: ["I-001", "I-002"], contextWindow: 1000, tokensBefore: 200 });
42
- assert.equal(plan.currentStartIndex, 1);
43
- assert.equal(plan.slices.at(-1)?.id, "I-002");
44
- assert.equal(plan.slices.at(-1)?.current, true);
45
- assert.equal(plan.slices.at(-1)?.entries[0]?.id, "u");
46
- }); it("extracts bounded paired Read records and merges by path/range", () => {
47
- const raw = "x".repeat(500);
48
- const entries = [
49
- { id: "call", type: "message", message: { role: "assistant", content: [{ type: "toolCall", id: "read-1", name: "read", arguments: { path: "/repo/a.ts", offset: 7, limit: 3 } }] } },
50
- { id: "result", type: "message", message: { role: "toolResult", toolCallId: "read-1", content: [{ type: "text", text: raw }] } },
90
+ const cut = buildOwnCut(entries, 1);
91
+ assert.equal(cut.ok, true);
92
+ assert.deepEqual(cut.ok ? cut.messages.map((message) => message.content) : [], [
93
+ [{ type: "text", text: "retained user from old boundary" }],
94
+ [{ type: "text", text: "live answer" }],
95
+ ]);
96
+
97
+ const result = buildPiPlansVccCompaction({
98
+ branchEntries: entries,
99
+ preparation: { tokensBefore: 20_000 },
100
+ reason: "threshold",
101
+ willRetry: false,
102
+ settings: DEFAULT_VCC_SETTINGS,
103
+ phaseContext: { phase: "planning" },
104
+ });
105
+ assert.equal(result.kind, "compaction");
106
+ if (result.kind !== "compaction") return;
107
+ assert.match(result.compaction.summary, /Read: src\/legacy\.ts line 5-9 Extracted information summary: legacy facts/);
108
+ assert.match(result.compaction.summary, /Previous compaction hard floor: single oversized tool result/);
109
+ assert.equal((result.compaction.details as any).kind, undefined);
110
+ assert.equal((result.compaction.details as any).readRecords, undefined);
111
+ assert.equal((result.compaction.details as any).metrics?.currentI, undefined);
112
+ assert.equal(result.compaction.details.compactor, "pi-vcc");
113
+ });
114
+
115
+ it("applies smart keep, explicit keep, budget cuts, and tool-result-safe boundaries", () => {
116
+ const smallTurns = [
117
+ textEntry("u-1", "user", "first task"),
118
+ textEntry("a-1", "assistant", "first answer"),
119
+ textEntry("u-2", "user", "second task"),
120
+ textEntry("a-2", "assistant", "second answer"),
121
+ textEntry("u-3", "user", "third task"),
122
+ textEntry("a-3", "assistant", "third answer"),
51
123
  ];
52
- const records = extractReadRecords(entries);
53
- assert.equal(records.length, 1);
54
- assert.equal(records[0]?.range, "7-9");
55
- assert.equal(records[0]?.formatted, formatReadRecord(records[0]!));
56
- assert.match(records[0]?.formatted ?? "", /^Read: \/repo\/a\.ts line 7-9 Extracted information summary: /);
57
- assert.ok((records[0]?.formatted.length ?? 0) < raw.length);
58
- const merged = mergeCompactionDetails(
59
- { readRecords: records },
60
- { readRecords: [{ ...records[0]!, summary: "new extraction", formatted: "" }] },
61
- );
62
- assert.equal(merged.readRecords?.length, 1);
63
- assert.equal(merged.readRecords?.[0]?.summary, "new extraction");
124
+ const smart = buildPiPlansVccCompaction({
125
+ branchEntries: smallTurns,
126
+ preparation: { tokensBefore: 10_000 },
127
+ reason: "threshold",
128
+ willRetry: false,
129
+ settings: DEFAULT_VCC_SETTINGS,
130
+ phaseContext: { phase: "planning" },
131
+ });
132
+ assert.equal(smart.kind, "compaction");
133
+ if (smart.kind !== "compaction") return;
134
+ assert.equal(smart.stats.smartKeepAdjusted, true);
135
+ assert.equal(smart.stats.smartFromKeep, 1);
136
+ assert.equal(smart.stats.requestedKeepUserTurns, 2);
137
+ assert.equal(smart.compaction.firstKeptEntryId, "u-2");
138
+
139
+ const explicit = buildPiPlansVccCompaction({
140
+ branchEntries: smallTurns,
141
+ preparation: { tokensBefore: 10_000 },
142
+ customInstructions: `${PI_VCC_COMPACT_INSTRUCTION} keep:1`,
143
+ reason: "threshold",
144
+ willRetry: false,
145
+ settings: DEFAULT_VCC_SETTINGS,
146
+ phaseContext: { phase: "planning" },
147
+ });
148
+ assert.equal(explicit.kind, "compaction");
149
+ if (explicit.kind !== "compaction") return;
150
+ assert.equal(explicit.stats.smartKeepAdjusted, false);
151
+ assert.equal(explicit.stats.requestedKeepUserTurns, 1);
152
+ assert.equal(explicit.compaction.firstKeptEntryId, "u-3");
153
+
154
+ const huge = "x".repeat(300_000);
155
+ const noAnchor = buildPiPlansVccCompaction({
156
+ branchEntries: [
157
+ textEntry("u-1", "user", "single user turn"),
158
+ textEntry("a-1", "assistant", huge),
159
+ textEntry("a-2", "assistant", "safe assistant boundary"),
160
+ ],
161
+ preparation: { tokensBefore: 200_000 },
162
+ reason: "threshold",
163
+ willRetry: false,
164
+ settings: DEFAULT_VCC_SETTINGS,
165
+ phaseContext: { phase: "planning" },
166
+ });
167
+ assert.equal(noAnchor.kind, "compaction");
168
+ if (noAnchor.kind !== "compaction") return;
169
+ assert.equal(noAnchor.stats.budgetCut, "no_anchor");
170
+ assert.equal(noAnchor.compaction.firstKeptEntryId, "a-1");
171
+
172
+ const oversizedTail = buildPiPlansVccCompaction({
173
+ branchEntries: [
174
+ textEntry("u-1", "user", "one"),
175
+ textEntry("a-1", "assistant", "done"),
176
+ textEntry("u-2", "user", "two"),
177
+ textEntry("a-2", "assistant", "done"),
178
+ textEntry("u-3", "user", "three"),
179
+ textEntry("a-3", "assistant", huge),
180
+ ],
181
+ preparation: { tokensBefore: 250_000 },
182
+ reason: "threshold",
183
+ willRetry: false,
184
+ settings: { ...DEFAULT_VCC_SETTINGS, smartKeepTail: false },
185
+ phaseContext: { phase: "planning" },
186
+ });
187
+ assert.equal(oversizedTail.kind, "compaction");
188
+ if (oversizedTail.kind !== "compaction") return;
189
+ assert.equal(oversizedTail.stats.budgetCut, "oversized_tail");
190
+ assert.equal(oversizedTail.compaction.firstKeptEntryId, "a-3");
191
+
192
+ const toolBoundary = buildPiPlansVccCompaction({
193
+ branchEntries: [
194
+ textEntry("u-1", "user", "single user turn"),
195
+ toolCallEntry("tc-1", "read", { path: "src/exec.ts" }),
196
+ toolResultEntry("tr-1", "read", huge),
197
+ textEntry("a-after", "assistant", "safe boundary after the tool result"),
198
+ ],
199
+ preparation: { tokensBefore: 250_000 },
200
+ reason: "threshold",
201
+ willRetry: false,
202
+ settings: DEFAULT_VCC_SETTINGS,
203
+ phaseContext: { phase: "planning" },
204
+ });
205
+ assert.equal(toolBoundary.kind, "compaction");
206
+ if (toolBoundary.kind !== "compaction") return;
207
+ assert.equal(toolBoundary.stats.budgetCut, "no_anchor");
208
+ assert.equal(toolBoundary.compaction.firstKeptEntryId, "a-after");
209
+ });
210
+
211
+ it("compiles a deterministic five-section summary with phase context and file activity", () => {
212
+ const result = buildPiPlansVccCompaction({
213
+ branchEntries: [
214
+ textEntry("u-1", "user", "Please implement repo-private VCC compaction. Always keep ASCII output."),
215
+ toolCallEntry("tc-1", "edit", { path: "src/compaction.ts" }),
216
+ textEntry("a-1", "assistant", "Updated src/compaction.ts and ran npm test."),
217
+ textEntry("u-2", "user", "Continue execution."),
218
+ textEntry("a-2", "assistant", "Current work is in the retained tail."),
219
+ ],
220
+ preparation: {
221
+ firstKeptEntryId: "fallback",
222
+ tokensBefore: 40_000,
223
+ previousSummary: "## Legacy Summary\nEarlier compact facts.",
224
+ fileOps: { read: ["src/exec.ts"], written: ["src/compaction.ts"], edited: [] },
225
+ },
226
+ customInstructions: `${PI_VCC_COMPACT_INSTRUCTION} keep:1`,
227
+ reason: "threshold",
228
+ willRetry: false,
229
+ settings: DEFAULT_VCC_SETTINGS,
230
+ phaseContext: {
231
+ phase: "execution",
232
+ planPath: "/repo/PLAN_v3.md",
233
+ currentI: "I-002",
234
+ remainingVerifierIds: ["VC-002"],
235
+ implementationIds: ["I-001", "I-002"],
236
+ },
237
+ });
238
+ assert.equal(result.kind, "compaction");
239
+ if (result.kind !== "compaction") return;
240
+ assert.equal(result.compaction.firstKeptEntryId, "u-2");
241
+ assert.match(result.compaction.summary, /\[Session Goal\]/);
242
+ assert.match(result.compaction.summary, /Execute accepted plan \/repo\/PLAN_v3\.md/);
243
+ assert.match(result.compaction.summary, /\[Files And Changes\]/);
244
+ assert.match(result.compaction.summary, /Modified: src\/compaction\.ts/);
245
+ assert.match(result.compaction.summary, /Read: src\/exec\.ts/);
246
+ assert.match(result.compaction.summary, /\[Outstanding Context\]/);
247
+ assert.match(result.compaction.summary, /Current implementation item: I-002/);
248
+ assert.match(result.compaction.summary, /Previous compact summary: Legacy Summary Earlier compact facts\./);
249
+ assert.match(result.compaction.summary, /\[User Preferences\]/);
250
+ assert.match(result.compaction.summary, /Always keep ASCII output/);
251
+ assert.equal(result.compaction.details.compactor, "pi-vcc");
252
+ assert.equal(result.compaction.details.phase, "execution");
253
+ assert.equal(result.stats.keptUserTurns, 1);
254
+ assert.equal(result.followUpPrompt, null);
255
+ });
256
+
257
+ it("cancels unsafe manual cuts but falls back to Pi core for overflow retry", () => {
258
+ const manual = buildPiPlansVccCompaction({
259
+ branchEntries: [],
260
+ preparation: { firstKeptEntryId: "fallback", tokensBefore: 100 },
261
+ reason: "manual",
262
+ willRetry: false,
263
+ settings: DEFAULT_VCC_SETTINGS,
264
+ phaseContext: { phase: "planning" },
265
+ });
266
+ assert.equal(manual.kind, "cancel");
267
+
268
+ const overflowRetry = buildPiPlansVccCompaction({
269
+ branchEntries: [],
270
+ preparation: { firstKeptEntryId: "fallback", tokensBefore: 100 },
271
+ reason: "overflow",
272
+ willRetry: true,
273
+ settings: DEFAULT_VCC_SETTINGS,
274
+ phaseContext: { phase: "planning" },
275
+ });
276
+ assert.deepEqual(overflowRetry, { kind: "fallback", reason: "no_live_messages" });
277
+
278
+ const overrideDisabled = buildPiPlansVccCompaction({
279
+ branchEntries: [
280
+ textEntry("u-1", "user", "old"),
281
+ textEntry("a-1", "assistant", "old answer"),
282
+ textEntry("u-2", "user", "new"),
283
+ ],
284
+ preparation: { firstKeptEntryId: "fallback", tokensBefore: 100 },
285
+ settings: { ...DEFAULT_VCC_SETTINGS, overrideDefaultCompaction: false },
286
+ phaseContext: { phase: "planning" },
287
+ });
288
+ assert.deepEqual(overrideDisabled, { kind: "fallback", reason: "override-disabled" });
64
289
  });
65
290
 
66
- it("uses strict trigger and records a hard floor when the retained suffix cannot fit", () => {
67
- assert.equal(currentIExceedsTrigger(200, 1000), false);
68
- assert.equal(currentIExceedsTrigger(201, 1000), true);
69
- const entries = [textEntry("i1", "[I-001:current] " + "work ".repeat(40), 900), textEntry("u", "latest", 200), textEntry("a", "answer", 200)];
70
- const plan = planIAwareCompaction({ entries, currentI: "I-001", contextWindow: 1000, tokensBefore: 1300 });
71
- assert.equal(plan.metrics.targetMet, false);
72
- assert.ok(plan.metrics.hardFloorReason);
291
+ it("parses manual keep/follow-up instructions and gates auto-continue by version", () => {
292
+ assert.deepEqual(parseCompactionInstructions("pi-plans execution auto compact"), {
293
+ isPiVcc: false,
294
+ isInternalPiPlans: true,
295
+ keepUserTurns: 1,
296
+ keepUserTurnsExplicit: false,
297
+ followUpPrompt: null,
298
+ });
299
+ assert.deepEqual(parseCompactionInstructions("keep:2 Continue with tests"), {
300
+ isPiVcc: false,
301
+ isInternalPiPlans: false,
302
+ keepUserTurns: 2,
303
+ keepUserTurnsExplicit: true,
304
+ followUpPrompt: "Continue with tests",
305
+ });
306
+ assert.deepEqual(parseCompactionInstructions(`${PI_VCC_COMPACT_INSTRUCTION} keep:3`), {
307
+ isPiVcc: true,
308
+ isInternalPiPlans: false,
309
+ keepUserTurns: 3,
310
+ keepUserTurnsExplicit: true,
311
+ followUpPrompt: null,
312
+ });
313
+ assert.deepEqual(parseCompactionInstructions("Continue with tests keep:2"), {
314
+ isPiVcc: false,
315
+ isInternalPiPlans: false,
316
+ keepUserTurns: 2,
317
+ keepUserTurnsExplicit: true,
318
+ followUpPrompt: "Continue with tests",
319
+ });
320
+ assert.equal(shouldScheduleAutoContinue(true, "0.84.3"), true);
321
+ assert.equal(shouldScheduleAutoContinue(true, "0.84.4"), false);
322
+ assert.equal(shouldScheduleAutoContinue(false, "0.84.3"), false);
323
+ });
324
+
325
+ it("scaffolds and loads repo-private VCC settings", () => {
326
+ const stateRoot = path.join(tmpRoot, "state");
327
+ scaffoldVccSettings(stateRoot);
328
+ assert.equal(fs.existsSync(vccSettingsPath(stateRoot)), true);
329
+ assert.deepEqual(loadVccSettings(stateRoot), DEFAULT_VCC_SETTINGS);
330
+
331
+ fs.writeFileSync(vccSettingsPath(stateRoot), JSON.stringify({ overrideDefaultCompaction: false }), "utf8");
332
+ scaffoldVccSettings(stateRoot);
333
+ assert.deepEqual(loadVccSettings(stateRoot), {
334
+ ...DEFAULT_VCC_SETTINGS,
335
+ overrideDefaultCompaction: false,
336
+ });
337
+
338
+ const invalidRoot = path.join(tmpRoot, "invalid-state");
339
+ fs.mkdirSync(invalidRoot, { recursive: true });
340
+ const invalidPath = vccSettingsPath(invalidRoot);
341
+ fs.writeFileSync(invalidPath, "{ invalid json", "utf8");
342
+ scaffoldVccSettings(invalidRoot);
343
+ assert.equal(fs.readFileSync(invalidPath, "utf8"), "{ invalid json");
344
+ assert.deepEqual(loadVccSettings(invalidRoot), DEFAULT_VCC_SETTINGS);
345
+
346
+ const envRoot = path.join(tmpRoot, "env-state");
347
+ const envConfig = path.join(tmpRoot, "external-pi-vcc-config.json");
348
+ const previousEnv = process.env.PI_VCC_CONFIG_PATH;
349
+ try {
350
+ process.env.PI_VCC_CONFIG_PATH = envConfig;
351
+ fs.writeFileSync(envConfig, JSON.stringify({ smartKeepTail: false, continueAfterThresholdCompact: false }), "utf8");
352
+ assert.deepEqual(loadVccSettings(envRoot), DEFAULT_VCC_SETTINGS);
353
+ } finally {
354
+ if (previousEnv === undefined) delete process.env.PI_VCC_CONFIG_PATH;
355
+ else process.env.PI_VCC_CONFIG_PATH = previousEnv;
356
+ }
357
+ });
358
+
359
+ it("writes debug snapshots only when enabled", () => {
360
+ const debugPath = "/tmp/pi-vcc-debug.json";
361
+ const previous = fs.existsSync(debugPath) ? fs.readFileSync(debugPath, "utf8") : null;
362
+ try {
363
+ fs.rmSync(debugPath, { force: true });
364
+ const event = {
365
+ branchEntries: [
366
+ textEntry("u-1", "user", "implement debug test"),
367
+ textEntry("a-1", "assistant", "working"),
368
+ textEntry("u-2", "user", "continue"),
369
+ textEntry("a-2", "assistant", "tail"),
370
+ ],
371
+ preparation: { tokensBefore: 10_000 },
372
+ reason: "threshold" as const,
373
+ willRetry: false,
374
+ phaseContext: { phase: "planning" as const },
375
+ };
376
+ buildPiPlansVccCompaction({ ...event, settings: DEFAULT_VCC_SETTINGS });
377
+ assert.equal(fs.existsSync(debugPath), false);
378
+
379
+ buildPiPlansVccCompaction({ ...event, settings: { ...DEFAULT_VCC_SETTINGS, debug: true } });
380
+ const debug = JSON.parse(fs.readFileSync(debugPath, "utf8"));
381
+ assert.equal(debug.usedOwnCut, true);
382
+ assert.equal(debug.phase, "planning");
383
+ } finally {
384
+ if (previous === null) fs.rmSync(debugPath, { force: true });
385
+ else fs.writeFileSync(debugPath, previous, "utf8");
386
+ }
73
387
  });
74
388
  });