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.
- package/README.md +90 -26
- package/agents/ref-analyst.md +18 -0
- package/index.ts +121 -9
- package/package.json +16 -1
- package/references/pi-planning-workflow.md +21 -6
- package/references/state-and-config.md +52 -5
- package/scripts/validate.ts +5 -0
- package/skills/plan-with-refs/SKILL.md +3 -3
- package/src/code-graph/commands.ts +483 -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 +361 -0
- package/src/exec.ts +508 -693
- package/src/guard.ts +14 -1
- package/src/refine-prompts.ts +109 -0
- package/src/refine-ui-helpers.ts +71 -18
- package/src/refine-ui-state.ts +88 -22
- package/src/refine-ui.ts +210 -102
- package/src/state.ts +36 -7
- package/src/subagent.ts +164 -61
- package/src/termination-prompt.ts +22 -0
- package/tests/analyze-refs.test.ts +265 -0
- package/tests/ask-choice.test.ts +264 -0
- package/tests/autocomplete.test.ts +6 -1
- package/tests/code-graph-apply-action.test.ts +173 -0
- 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 +263 -0
- package/tests/exec.test.ts +808 -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/guard.test.ts +27 -1
- package/tests/plans.test.ts +10 -0
- package/tests/refine-prompts.test.ts +101 -2
- package/tests/refine-ui.test.ts +371 -72
- package/tests/state.test.ts +32 -0
- package/tests/subagent.test.ts +48 -20
- package/tools/analyze-refs.ts +263 -0
- package/tools/ask-choice.ts +159 -11
- package/tools/code-graph.ts +277 -0
- package/tools/graph-aware-file-tools.ts +392 -0
- package/tools/plans.ts +97 -2
- package/tools/refine.ts +61 -15
package/tests/compaction.test.ts
CHANGED
|
@@ -1,74 +1,388 @@
|
|
|
1
1
|
import * as assert from "node:assert/strict";
|
|
2
|
-
import
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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,
|
|
13
|
-
return { id, type: "message",
|
|
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
|
-
|
|
17
|
-
|
|
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("
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
textEntry("
|
|
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
|
|
27
|
-
assert.equal(
|
|
28
|
-
assert.equal(
|
|
29
|
-
assert.ok(
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
assert.equal(
|
|
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("
|
|
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
|
-
|
|
38
|
-
textEntry("
|
|
39
|
-
|
|
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
|
|
42
|
-
assert.equal(
|
|
43
|
-
assert.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
62
|
-
assert.equal(
|
|
63
|
-
assert.equal(
|
|
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("
|
|
67
|
-
assert.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
});
|