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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates summary record validation and cache behavior. Does not require
|
|
3
|
+
* a real model — uses a fake completion handle.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { test } from "node:test";
|
|
7
|
+
import * as assert from "node:assert/strict";
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as os from "node:os";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { Store } from "../src/code-graph/store.ts";
|
|
12
|
+
import {
|
|
13
|
+
generateSummaries,
|
|
14
|
+
type CompletionHandle,
|
|
15
|
+
clearSummaryCache,
|
|
16
|
+
summaryCacheStats,
|
|
17
|
+
} from "../src/code-graph/summary.ts";
|
|
18
|
+
|
|
19
|
+
async function openTemp(): Promise<{ store: Store; cleanup: () => void } | null> {
|
|
20
|
+
try {
|
|
21
|
+
const sqlite = await import("node:sqlite");
|
|
22
|
+
const worktreeRoot = fs.realpathSync(os.tmpdir());
|
|
23
|
+
const dbPath = path.join(os.tmpdir(), `code-graph-summary-${process.pid}-${Date.now()}.db`);
|
|
24
|
+
const store = new Store({ dbPath, worktreeRoot, gitCommonDir: worktreeRoot }, sqlite);
|
|
25
|
+
return { store, cleanup: () => {
|
|
26
|
+
store.close();
|
|
27
|
+
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
28
|
+
} };
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function seedFunction(store: Store): void {
|
|
35
|
+
store.tx(() => {
|
|
36
|
+
store.db
|
|
37
|
+
.prepare(
|
|
38
|
+
`INSERT INTO functions (file_dir, file_name, function_name, language, kind,
|
|
39
|
+
full_code, full_code_hash, render_code, render_code_hash,
|
|
40
|
+
move_supported, is_primary,
|
|
41
|
+
provenance_start_byte, provenance_end_byte, provenance_start_line,
|
|
42
|
+
provenance_start_col, provenance_end_line, provenance_end_col, version)
|
|
43
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
|
44
|
+
)
|
|
45
|
+
.run(
|
|
46
|
+
"src",
|
|
47
|
+
"a.js",
|
|
48
|
+
"alpha",
|
|
49
|
+
"javascript",
|
|
50
|
+
"declaration",
|
|
51
|
+
"function alpha() { return 1; }",
|
|
52
|
+
"deadbeef",
|
|
53
|
+
"function alpha() { return 1; }",
|
|
54
|
+
"deadbeef",
|
|
55
|
+
1,
|
|
56
|
+
1,
|
|
57
|
+
0,
|
|
58
|
+
25,
|
|
59
|
+
1,
|
|
60
|
+
0,
|
|
61
|
+
1,
|
|
62
|
+
0,
|
|
63
|
+
1,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
test("generateSummaries writes ok status and validates schema", async (t) => {
|
|
69
|
+
const opened = await openTemp();
|
|
70
|
+
if (!opened) return;
|
|
71
|
+
t.after(() => opened.cleanup());
|
|
72
|
+
const { store } = opened;
|
|
73
|
+
clearSummaryCache();
|
|
74
|
+
seedFunction(store);
|
|
75
|
+
const handle: CompletionHandle = {
|
|
76
|
+
complete: async () => ({
|
|
77
|
+
content: [{ type: "text", text: '{"description":"adds two numbers","inputs":["a: number","b: number"],"outputs":["number"]}' }],
|
|
78
|
+
stopReason: "stop",
|
|
79
|
+
}),
|
|
80
|
+
model: () => ({ provider: "test", id: "test-model", api: "openai-completions", reasoning: true }),
|
|
81
|
+
thinkingLevel: () => "low",
|
|
82
|
+
hasUI: true,
|
|
83
|
+
confirm: async () => true,
|
|
84
|
+
notify: () => {},
|
|
85
|
+
};
|
|
86
|
+
const report = await generateSummaries({ store, ctx: handle });
|
|
87
|
+
assert.equal(report.ok, 1);
|
|
88
|
+
assert.equal(report.failed, 0);
|
|
89
|
+
const row = store.read(() => store.db.prepare("SELECT summary_status, summary_inputs FROM functions WHERE function_name='alpha'").get()) as { summary_status: string; summary_inputs: string };
|
|
90
|
+
assert.equal(row.summary_status, "ok");
|
|
91
|
+
assert.equal(JSON.parse(row.summary_inputs)[0], "a: number");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("declined consent marks summaries as declined without sending source", async (t) => {
|
|
95
|
+
const opened = await openTemp();
|
|
96
|
+
if (!opened) return;
|
|
97
|
+
t.after(() => opened.cleanup());
|
|
98
|
+
const { store } = opened;
|
|
99
|
+
clearSummaryCache();
|
|
100
|
+
seedFunction(store);
|
|
101
|
+
let sendCalled = false;
|
|
102
|
+
const handle: CompletionHandle = {
|
|
103
|
+
complete: async () => {
|
|
104
|
+
sendCalled = true;
|
|
105
|
+
return { content: [{ type: "text", text: "" }] };
|
|
106
|
+
},
|
|
107
|
+
model: () => ({ provider: "test", id: "test-model", api: "openai-completions", reasoning: false }),
|
|
108
|
+
thinkingLevel: () => "low",
|
|
109
|
+
hasUI: true,
|
|
110
|
+
confirm: async () => false,
|
|
111
|
+
notify: () => {},
|
|
112
|
+
};
|
|
113
|
+
const report = await generateSummaries({ store, ctx: handle });
|
|
114
|
+
assert.equal(report.declined, 1);
|
|
115
|
+
assert.equal(sendCalled, false);
|
|
116
|
+
const row = store.read(() => store.db.prepare("SELECT summary_status FROM functions WHERE function_name='alpha'").get()) as { summary_status: string };
|
|
117
|
+
assert.equal(row.summary_status, "declined");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("headless consent auto-declines", async (t) => {
|
|
121
|
+
const opened = await openTemp();
|
|
122
|
+
if (!opened) return;
|
|
123
|
+
t.after(() => opened.cleanup());
|
|
124
|
+
const { store } = opened;
|
|
125
|
+
clearSummaryCache();
|
|
126
|
+
seedFunction(store);
|
|
127
|
+
let sendCalled = false;
|
|
128
|
+
const handle: CompletionHandle = {
|
|
129
|
+
complete: async () => {
|
|
130
|
+
sendCalled = true;
|
|
131
|
+
return { content: [{ type: "text", text: "" }] };
|
|
132
|
+
},
|
|
133
|
+
model: () => ({ provider: "test", id: "test-model", api: "openai-completions", reasoning: false }),
|
|
134
|
+
thinkingLevel: () => "low",
|
|
135
|
+
hasUI: false,
|
|
136
|
+
notify: () => {},
|
|
137
|
+
};
|
|
138
|
+
const report = await generateSummaries({ store, ctx: handle });
|
|
139
|
+
assert.equal(report.declined, 1);
|
|
140
|
+
assert.equal(sendCalled, false);
|
|
141
|
+
const row = store.read(() => store.db.prepare("SELECT summary_status FROM functions WHERE function_name='alpha'").get()) as { summary_status: string };
|
|
142
|
+
assert.equal(row.summary_status, "declined");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("summary cache stats are non-negative", () => {
|
|
146
|
+
clearSummaryCache();
|
|
147
|
+
assert.equal(summaryCacheStats().size, 0);
|
|
148
|
+
});
|
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
|
});
|