pi-plans 0.6.0 → 0.8.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/AGENTS.md +58 -0
- package/CONTRIBUTING.md +8 -15
- package/README.md +39 -37
- package/agents/execution-reviewer.md +40 -0
- package/agents/reviewer.md +12 -3
- package/index.ts +55 -58
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +50 -60
- package/references/plan-artifact-template.md +81 -60
- package/references/state-and-config.md +60 -44
- package/scripts/bench/pi-adapter/pi_plans_bench.py +33 -22
- package/scripts/bench/pi-adapter/rpc_driver.mjs +4 -4
- package/scripts/run-tests.ts +12 -1
- package/scripts/validate.ts +39 -11
- package/skills/debug-and-plan/SKILL.md +3 -3
- package/skills/plan-big/SKILL.md +3 -3
- package/skills/plan-normal/SKILL.md +3 -3
- package/skills/plan-small/SKILL.md +4 -4
- package/skills/plan-with-refs/SKILL.md +6 -6
- package/skills/planning/SKILL.md +1 -1
- package/src/ask-form.ts +4 -4
- package/src/auditor.ts +227 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +8 -3
- package/src/code-graph/community.ts +1 -1
- package/src/code-graph/paths.ts +1 -1
- package/src/code-graph/watch.ts +2 -2
- package/src/compaction.ts +3 -3
- package/src/config-command.ts +146 -73
- package/src/dashboard.ts +303 -0
- package/src/exec.ts +1185 -924
- package/src/global-state.ts +304 -0
- package/src/guard.ts +18 -19
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +12 -70
- package/src/refine-ui-helpers.ts +24 -5
- package/src/refine-ui-state.ts +1 -1
- package/src/refine-ui.ts +19 -3
- package/src/resume-command.ts +45 -129
- package/src/resume.ts +5 -1
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/staleness.ts +53 -0
- package/src/state.ts +273 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +223 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +7 -54
- package/src/workflow-state.ts +76 -58
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-schema.test.ts +0 -12
- package/tests/ask-choice.test.ts +2 -49
- package/tests/ask-form-tool.test.ts +4 -5
- package/tests/ask-form.test.ts +2 -2
- package/tests/auditor.test.ts +210 -0
- package/tests/auto-approve.test.ts +7 -10
- package/tests/autocomplete.test.ts +8 -11
- package/tests/code-graph-apply-action.test.ts +2 -2
- package/tests/code-graph-commands.test.ts +2 -2
- package/tests/code-graph-index.test.ts +2 -2
- package/tests/code-graph-loop.e2e.test.ts +1 -1
- package/tests/code-graph-mutations.test.ts +1 -1
- package/tests/code-graph-rollback.test.ts +1 -1
- package/tests/code-graph-v05.test.ts +2 -2
- package/tests/compaction.test.ts +1 -1
- package/tests/config-command.test.ts +103 -100
- package/tests/dashboard.test.ts +402 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec-review-loop.test.ts +331 -0
- package/tests/exec.test.ts +771 -1706
- package/tests/execute-plan.test.ts +44 -19
- package/tests/extension-load.test.ts +48 -0
- package/tests/global-state.test.ts +371 -0
- package/tests/graph-aware-file-tools.test.ts +5 -5
- package/tests/guard.test.ts +1 -1
- package/tests/multi-run.test.ts +3 -103
- package/tests/plan.test.ts +139 -62
- package/tests/plans.test.ts +7 -79
- package/tests/refine-prompts.test.ts +20 -71
- package/tests/refine-resume.test.ts +27 -22
- package/tests/refine-ui.test.ts +6 -15
- package/tests/resume-lifecycle.test.ts +41 -22
- package/tests/resume.test.ts +39 -81
- package/tests/role-panels.test.ts +391 -0
- package/tests/run-context.test.ts +1 -1
- package/tests/run-ownership.test.ts +1 -1
- package/tests/stale-ctx.test.ts +218 -0
- package/tests/staleness.test.ts +76 -0
- package/tests/state.test.ts +155 -32
- package/tests/subagent-thinking.test.ts +65 -0
- package/tests/subagent-usage.test.ts +1 -1
- package/tests/task-tool.test.ts +61 -0
- package/tests/tasks.test.ts +142 -0
- package/tests/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +73 -90
- package/tools/analyze-refs.ts +67 -32
- package/tools/ask-choice.ts +7 -53
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +55 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +41 -67
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- package/agents/executor.md +0 -26
- package/scripts/bench/pi-adapter/__pycache__/pi_plans_bench.cpython-312.pyc +0 -0
- package/src/panel.ts +0 -473
- package/src/termination-prompt.ts +0 -73
- package/tests/goal-wait.test.ts +0 -269
- package/tests/panel-i-zero.test.ts +0 -420
- package/tests/panel.test.ts +0 -355
|
@@ -12,13 +12,22 @@ import { initState, setRole, setLanguage, startRun, readActive } from "../src/st
|
|
|
12
12
|
const ROOT = path.dirname(path.dirname(url.fileURLToPath(import.meta.url)));
|
|
13
13
|
|
|
14
14
|
let tmpRoot: string;
|
|
15
|
+
let globalDir: string;
|
|
16
|
+
let previousGlobalDir: string | undefined;
|
|
15
17
|
|
|
16
18
|
before(() => {
|
|
17
19
|
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-analyze-refs-"));
|
|
20
|
+
// Isolate the global reviewer config (F-002): never touch ~/.pi/pi-plans.
|
|
21
|
+
previousGlobalDir = process.env.PI_PLANS_GLOBAL_DIR;
|
|
22
|
+
globalDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-global-analyze-refs-"));
|
|
23
|
+
process.env.PI_PLANS_GLOBAL_DIR = globalDir;
|
|
18
24
|
});
|
|
19
25
|
|
|
20
26
|
after(() => {
|
|
27
|
+
if (previousGlobalDir === undefined) delete process.env.PI_PLANS_GLOBAL_DIR;
|
|
28
|
+
else process.env.PI_PLANS_GLOBAL_DIR = previousGlobalDir;
|
|
21
29
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
30
|
+
fs.rmSync(globalDir, { recursive: true, force: true });
|
|
22
31
|
});
|
|
23
32
|
|
|
24
33
|
function mkWorkdir(name: string): string {
|
|
@@ -102,7 +111,7 @@ function subagentLines(workdir: string): Array<any> {
|
|
|
102
111
|
.map((line) => JSON.parse(line));
|
|
103
112
|
}
|
|
104
113
|
|
|
105
|
-
describe("analyze_refs gates", () => {
|
|
114
|
+
describe("analyze_refs gates", () => {
|
|
106
115
|
it("refuses when no pi-plans state exists", async () => {
|
|
107
116
|
const workdir = mkWorkdir("gates-no-state");
|
|
108
117
|
const tool = loadTool();
|
|
@@ -112,35 +121,43 @@ describe("analyze_refs gates", () => {
|
|
|
112
121
|
);
|
|
113
122
|
});
|
|
114
123
|
|
|
115
|
-
it("refuses with
|
|
116
|
-
const workdir = mkWorkdir("gates-
|
|
124
|
+
it("refuses with embedded text guidance when the reviewer model is unconfirmed (headless)", async () => {
|
|
125
|
+
const workdir = mkWorkdir("gates-unconfirmed");
|
|
117
126
|
initState(workdir);
|
|
118
|
-
const configPath = path.join(workdir, ".git", "pi_plans", "config.json");
|
|
119
|
-
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
120
|
-
config.reviewer.mode = "bogus";
|
|
121
|
-
fs.writeFileSync(configPath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
|
|
122
127
|
const tool = loadTool();
|
|
123
128
|
await assert.rejects(
|
|
124
129
|
tool.execute("c1", { refs: [{ id: "ref-1", localPath: "." }] }, undefined, undefined, headlessCtx(workdir)),
|
|
125
|
-
/
|
|
130
|
+
/model was never confirmed/,
|
|
126
131
|
);
|
|
127
132
|
});
|
|
128
133
|
|
|
129
|
-
it("
|
|
134
|
+
it("ignores the reviewer mode entirely (Q-4=B): current-session proceeds once a model is confirmed", async () => {
|
|
130
135
|
const workdir = mkWorkdir("gates-current-session");
|
|
131
136
|
initState(workdir);
|
|
132
|
-
setRole(workdir, { role: "reviewer", mode: "current-session" });
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
setRole(workdir, { role: "reviewer", mode: "current-session", modelSelector: "fake/model", confirmed: true });
|
|
138
|
+
const refDir = path.join(workdir, "refs", "solo");
|
|
139
|
+
fs.mkdirSync(refDir, { recursive: true });
|
|
140
|
+
const restore = withFakePi(
|
|
141
|
+
fakePiScript(
|
|
142
|
+
`emit({ type: "message_end", message: { role: "assistant", model: "fake/model", content: [{ type: "text", text: "OK" }] } });`,
|
|
143
|
+
),
|
|
137
144
|
);
|
|
145
|
+
const tool = loadTool();
|
|
146
|
+
try {
|
|
147
|
+
const result = await tool.execute("c1", { refs: [{ id: "ref-1", localPath: refDir }] }, undefined, undefined, headlessCtx(workdir));
|
|
148
|
+
assert.match(result.content[0]!.text, /mode is current-session, but analyze_refs always spawns/);
|
|
149
|
+
assert.match(result.content[0]!.text, /OK/);
|
|
150
|
+
} finally {
|
|
151
|
+
restore();
|
|
152
|
+
}
|
|
138
153
|
});
|
|
139
154
|
|
|
140
|
-
it("
|
|
141
|
-
const workdir = mkWorkdir("gates-unconfirmed");
|
|
155
|
+
it("requires a confirmed model even in current-session mode (analysis always spawns)", async () => {
|
|
156
|
+
const workdir = mkWorkdir("gates-current-session-unconfirmed");
|
|
142
157
|
initState(workdir);
|
|
143
|
-
|
|
158
|
+
// 'inherit' fully resets selector AND confirmation — the prior test's
|
|
159
|
+
// confirmed selector must not carry over (the global config is shared).
|
|
160
|
+
setRole(workdir, { role: "reviewer", mode: "current-session", modelSelector: "inherit" });
|
|
144
161
|
const tool = loadTool();
|
|
145
162
|
await assert.rejects(
|
|
146
163
|
tool.execute("c1", { refs: [{ id: "ref-1", localPath: "." }] }, undefined, undefined, headlessCtx(workdir)),
|
|
@@ -310,7 +327,7 @@ describe("analyze_refs fanout", () => {
|
|
|
310
327
|
const result = await tool.execute("c1", { refs: [{ id: "ref-1", localPath: refDir }] }, undefined, undefined, headlessCtx(workdir));
|
|
311
328
|
assert.match(result.content[0]!.text, /### pi-plans-refs-adhoc-ref-1/);
|
|
312
329
|
assert.equal(readActive(workdir), null);
|
|
313
|
-
const ledger = path.join(workdir, ".git", "
|
|
330
|
+
const ledger = path.join(workdir, ".git", "pi-plans", "runs");
|
|
314
331
|
const runs = fs.existsSync(ledger) ? fs.readdirSync(ledger) : [];
|
|
315
332
|
const spawnFiles = runs.flatMap((run) =>
|
|
316
333
|
fs.existsSync(path.join(ledger, run, "subagents.jsonl")) ? [fs.readFileSync(path.join(ledger, run, "subagents.jsonl"), "utf8")] : [],
|
|
@@ -157,18 +157,6 @@ describe("VC-2 · hardening does not wound legitimate single-question shapes", (
|
|
|
157
157
|
assert.equal(Value.Check(AskChoiceParams as never, wellFormed), true);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("single question with trailing + autoComplete:false passes Value.Check", () => {
|
|
161
|
-
const wellFormed = {
|
|
162
|
-
question: "实现评审循环如何终止?",
|
|
163
|
-
options: [
|
|
164
|
-
{ label: "goal wait:直到无未通过 VC", recommended: true },
|
|
165
|
-
{ label: "直到无高危发现(硬帽 5 轮)" },
|
|
166
|
-
],
|
|
167
|
-
trailing: "auto-refine-loop",
|
|
168
|
-
autoComplete: false,
|
|
169
|
-
};
|
|
170
|
-
assert.equal(Value.Check(AskChoiceParams as never, wellFormed), true);
|
|
171
|
-
});
|
|
172
160
|
});
|
|
173
161
|
|
|
174
162
|
describe("VC-3 · zero-recommended batches reject loudly with zero side effects", () => {
|
package/tests/ask-choice.test.ts
CHANGED
|
@@ -66,41 +66,7 @@ const OPTIONS = [
|
|
|
66
66
|
];
|
|
67
67
|
|
|
68
68
|
describe("ask_choice trailing option", () => {
|
|
69
|
-
it("
|
|
70
|
-
const tool = loadTool();
|
|
71
|
-
let seenLabels: string[] = [];
|
|
72
|
-
const ctx = makeCtx({
|
|
73
|
-
select: async (_question, labels) => {
|
|
74
|
-
seenLabels = labels;
|
|
75
|
-
return labels.find((label) => label.startsWith("Auto-refine loop"));
|
|
76
|
-
},
|
|
77
|
-
});
|
|
78
|
-
const result = await tool.execute("t1", {
|
|
79
|
-
question: "Ameliorate?",
|
|
80
|
-
options: OPTIONS,
|
|
81
|
-
autoComplete: true, // deliberately erroneous: trailing must suppress it
|
|
82
|
-
trailing: "auto-refine-loop",
|
|
83
|
-
}, undefined, undefined, ctx);
|
|
84
|
-
|
|
85
|
-
assert.equal(seenLabels.at(-1)?.startsWith("Auto-refine loop"), true, "Auto-refine loop must be last");
|
|
86
|
-
assert.equal(seenLabels.some((label) => label.startsWith("Auto-complete")), false, "Auto-complete must be absent");
|
|
87
|
-
const text = result.content[0].text as string;
|
|
88
|
-
assert.match(text, /User selected Auto-refine loop/);
|
|
89
|
-
assert.match(text, /until no high-severity finding \(hard cap 5 rounds\)/);
|
|
90
|
-
assert.match(text, /goal wait: continue until no unpassed VCs remain/);
|
|
91
|
-
assert.match(text, /refine \(role: "reviewer", target: "implementation", reviewers: <configured reviewerCount>\)/);
|
|
92
|
-
// 0.5.4: the trailing instructions now include the reviewer-count
|
|
93
|
-
// question with a skill-aware default (D-1/D-2) and the deferred
|
|
94
|
-
// combined persistence (D-5).
|
|
95
|
-
assert.match(text, /impl-review-reviewer-count/);
|
|
96
|
-
assert.match(text, /How many concurrent reviewers should each implementation-review round use\?/);
|
|
97
|
-
assert.match(text, /1 \(recommended\)/, "default workdir run has no active run → default 1");
|
|
98
|
-
assert.match(text, /Do not persist yet/);
|
|
99
|
-
assert.equal(result.details.source, "user");
|
|
100
|
-
assert.equal(result.details.answer, "Auto-refine loop");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("keeps Auto-complete as the trailing option without the trailing param", async () => {
|
|
69
|
+
it("keeps Auto-complete as the trailing option (the auto-refine trailing param was removed in v0.6.1)", async () => {
|
|
104
70
|
const tool = loadTool();
|
|
105
71
|
let seenLabels: string[] = [];
|
|
106
72
|
const ctx = makeCtx({
|
|
@@ -117,22 +83,9 @@ describe("ask_choice trailing option", () => {
|
|
|
117
83
|
assert.equal(seenLabels.at(-1)?.startsWith("Auto-complete"), true);
|
|
118
84
|
assert.equal(seenLabels.some((label) => label.startsWith("Auto-refine loop")), false);
|
|
119
85
|
});
|
|
120
|
-
|
|
121
|
-
it("never auto-answers a trailing question in headless sessions", async () => {
|
|
122
|
-
const tool = loadTool();
|
|
123
|
-
const ctx = makeCtx({ hasUI: false, select: async () => undefined });
|
|
124
|
-
await assert.rejects(
|
|
125
|
-
tool.execute("t3", {
|
|
126
|
-
question: "Ameliorate?",
|
|
127
|
-
options: OPTIONS,
|
|
128
|
-
autoComplete: true,
|
|
129
|
-
trailing: "auto-refine-loop",
|
|
130
|
-
}, undefined, undefined, ctx),
|
|
131
|
-
/No UI available/,
|
|
132
|
-
);
|
|
133
|
-
});
|
|
134
86
|
});
|
|
135
87
|
|
|
88
|
+
|
|
136
89
|
describe("ask_choice panel fitting", () => {
|
|
137
90
|
it("sanitizes newlines in the question and labels", () => {
|
|
138
91
|
const fitted = fitAskChoicePanel("Q line1\nQ line2", [{ core: "1. a\nb", display: "1. a\nb — desc" }], 100, 30);
|
|
@@ -15,6 +15,7 @@ import { initState, recordDecision, startRun } from "../src/state.ts";
|
|
|
15
15
|
import { createCheckpoint, loadCheckpoint } from "../src/workflow-state.ts";
|
|
16
16
|
import { reconcileCheckpointWithLedger } from "../src/resume.ts";
|
|
17
17
|
import { readDecisionLedger } from "../src/resume.ts";
|
|
18
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
18
19
|
|
|
19
20
|
type ToolDef = {
|
|
20
21
|
execute: (id: string, params: any, signal: undefined, update: undefined, ctx: any) => Promise<any>;
|
|
@@ -58,7 +59,7 @@ function startActiveRun(workdir: string) {
|
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function makeCtx(workdir: string, overrides: Record<string, unknown> = {}) {
|
|
61
|
-
|
|
62
|
+
const ctx = {
|
|
62
63
|
cwd: workdir,
|
|
63
64
|
hasUI: true,
|
|
64
65
|
sessionManager: {},
|
|
@@ -70,6 +71,8 @@ function makeCtx(workdir: string, overrides: Record<string, unknown> = {}) {
|
|
|
70
71
|
...overrides,
|
|
71
72
|
},
|
|
72
73
|
};
|
|
74
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
75
|
+
return ctx;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function recordDecisionEntry(workdir: string, runId: string, entry: Record<string, unknown>): void {
|
|
@@ -139,10 +142,6 @@ describe("ask_choice batch validation (R-013b/D-025)", () => {
|
|
|
139
142
|
() => tool.execute("t", { question: "single?", options: [{ label: "A" }], questions: [Q1, Q2] }, undefined, undefined, ctx),
|
|
140
143
|
/not both/,
|
|
141
144
|
);
|
|
142
|
-
await assert.rejects(
|
|
143
|
-
() => tool.execute("t", { questions: [Q1, Q2], trailing: "auto-refine-loop" }, undefined, undefined, ctx),
|
|
144
|
-
/trailing/,
|
|
145
|
-
);
|
|
146
145
|
});
|
|
147
146
|
|
|
148
147
|
it("rejects the canonical scope-confirm id in batches (F-003)", async () => {
|
package/tests/ask-form.test.ts
CHANGED
|
@@ -37,7 +37,7 @@ describe("form state machine", () => {
|
|
|
37
37
|
it("preselects the recommended option as cursor without marking it answered", () => {
|
|
38
38
|
const state = createFormState(qs(2));
|
|
39
39
|
assert.deepEqual(state.selection, [0, 0]);
|
|
40
|
-
//
|
|
40
|
+
// a pre-positioned cursor is NOT an answer — chips
|
|
41
41
|
// stay □ until the user presses Enter (or commits a custom answer).
|
|
42
42
|
assert.equal(allAnswered(state), false);
|
|
43
43
|
assert.deepEqual(state.confirmed, [false, false]);
|
|
@@ -428,7 +428,7 @@ describe("recommended marker hygiene (0.4.1)", () => {
|
|
|
428
428
|
});
|
|
429
429
|
});
|
|
430
430
|
|
|
431
|
-
describe("themed question frame (
|
|
431
|
+
describe("themed question frame (0.4.1)", () => {
|
|
432
432
|
const T: FormTheme = {
|
|
433
433
|
fg: (c, t) => `⟪${c}⟩${t}⟪/⟫`,
|
|
434
434
|
bg: (c, t) => `⟦${c}⟧${t}⟦/⟧`,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/** Tests for the execution reviewer: tri-state verdict parsing, contract
|
|
2
|
+
* coverage, rollback boundaries, skipped-pass, and no-cover exclusion. */
|
|
3
|
+
|
|
4
|
+
import * as assert from "node:assert/strict";
|
|
5
|
+
import { describe, it } from "node:test";
|
|
6
|
+
import type { CheckItem } from "../src/plan.ts";
|
|
7
|
+
import { auditRollbackSet, buildTaskView } from "../src/tasks.ts";
|
|
8
|
+
import {
|
|
9
|
+
applyAuditOutcome,
|
|
10
|
+
auditablePendingChecks,
|
|
11
|
+
buildAuditTask,
|
|
12
|
+
parseAuditReport,
|
|
13
|
+
presolvedCheckIds,
|
|
14
|
+
} from "../src/auditor.ts";
|
|
15
|
+
import { parsePlanTasks } from "../src/plan.ts";
|
|
16
|
+
|
|
17
|
+
const PLAN = `## Tasks
|
|
18
|
+
|
|
19
|
+
- Task-1: parser — files: src/a.ts; wave: 1
|
|
20
|
+
- Task-2: tool — files: src/b.ts; wave: 1
|
|
21
|
+
- Task-3: core — deps: Task-1, Task-2; files: src/c.ts; wave: 2
|
|
22
|
+
- Task-3.1: injection — files: src/c.ts
|
|
23
|
+
|
|
24
|
+
## Verification Checks
|
|
25
|
+
|
|
26
|
+
- [ ] \`VC-001\` covers \`Task-1\`; pass condition: parser ok
|
|
27
|
+
- [ ] \`VC-002\` covers \`Task-2\` and \`Task-3\`; pass condition: tool+core ok
|
|
28
|
+
- [ ] \`VC-003\` covers \`Task-3.1\`; pass condition: injection ok
|
|
29
|
+
- [ ] \`VC-004\` covers nothing here; pass condition: orphan
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
function checks(done: string[] = []): CheckItem[] {
|
|
33
|
+
const raw = PLAN.split("## Verification Checks")[1] ?? "";
|
|
34
|
+
const items: CheckItem[] = [];
|
|
35
|
+
for (const line of raw.split("\n")) {
|
|
36
|
+
const m = line.match(/^- \[ \] `(VC-\d+)` (.+)$/);
|
|
37
|
+
if (m) items.push({ id: m[1], text: m[2], done: done.includes(m[1]) });
|
|
38
|
+
}
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function view(progress?: Record<string, { status: "complete" | "skipped" }>) {
|
|
43
|
+
return buildTaskView(parsePlanTasks(PLAN), progress);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ALL_IDS = ["VC-001", "VC-002", "VC-003", "VC-004"];
|
|
47
|
+
|
|
48
|
+
describe("auditor parsing", () => {
|
|
49
|
+
it("parses per-check verdicts and ignores unknown ids", () => {
|
|
50
|
+
const report = [
|
|
51
|
+
"- `VC-001` — verdict: pass; evidence: src/a.ts; note: ok",
|
|
52
|
+
"- `VC-002` — verdict: fail; evidence: src/c.ts; note: missing",
|
|
53
|
+
"- `VC-999` — verdict: pass; evidence: n/a",
|
|
54
|
+
].join("\n");
|
|
55
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
56
|
+
assert.deepEqual(passed, ["VC-001"]);
|
|
57
|
+
assert.deepEqual(failed, ["VC-002"]);
|
|
58
|
+
assert.deepEqual(undeterminable.sort(), ["VC-003", "VC-004"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("parses verdicts wrapped in markdown emphasis", () => {
|
|
62
|
+
// Regression: `verdict: **pass**` used to parse as nothing, and the
|
|
63
|
+
// caller's fail-closed rule then rolled the entire run back.
|
|
64
|
+
const report = [
|
|
65
|
+
"- `VC-001` — verdict: **pass**; evidence: src/a.ts",
|
|
66
|
+
"- `VC-002` — verdict: **fail**; evidence: src/c.ts",
|
|
67
|
+
"- `VC-003` — verdict: *pass*; evidence: src/d.ts",
|
|
68
|
+
"- `VC-004` — verdict: `pass`; evidence: src/e.ts",
|
|
69
|
+
].join("\n");
|
|
70
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
71
|
+
assert.deepEqual(passed, ["VC-001", "VC-003", "VC-004"]);
|
|
72
|
+
assert.deepEqual(failed, ["VC-002"]);
|
|
73
|
+
assert.deepEqual(undeterminable, []);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not read a verdict word prefix as a verdict", () => {
|
|
77
|
+
const report = ["- `VC-001` — verdict: passed; evidence: x", "- `VC-002` — verdict: failing; evidence: y"].join("\n");
|
|
78
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
79
|
+
assert.deepEqual(passed, []);
|
|
80
|
+
assert.deepEqual(failed, []);
|
|
81
|
+
assert.deepEqual(undeterminable.sort(), ["VC-001", "VC-002", "VC-003", "VC-004"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("conflicting verdicts for one check resolve to fail", () => {
|
|
85
|
+
const report = "- `VC-001` — verdict: pass; note: a\n- `VC-001` — verdict: fail; note: b";
|
|
86
|
+
const { passed, failed } = parseAuditReport(report, ALL_IDS);
|
|
87
|
+
assert.deepEqual(passed, []);
|
|
88
|
+
assert.deepEqual(failed, ["VC-001"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("reads an explicit undeterminable verdict", () => {
|
|
92
|
+
const report = "- `VC-001` — verdict: pass\n- `VC-002` — verdict: undeterminable; evidence: bash not granted";
|
|
93
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
94
|
+
assert.deepEqual(passed, ["VC-001"]);
|
|
95
|
+
assert.deepEqual(failed, []);
|
|
96
|
+
assert.deepEqual(undeterminable.sort(), ["VC-002", "VC-003", "VC-004"]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("treats a reviewer-shaped report as undeterminable, never as failure", () => {
|
|
100
|
+
// Regression: agents/reviewer.md was the auditor's system prompt and
|
|
101
|
+
// mandates `## Findings` / `## Questions` with F-### lines and never
|
|
102
|
+
// says "verdict". Under fail-closed that whole shape read as "every
|
|
103
|
+
// check failed" and rolled the run back.
|
|
104
|
+
const report = [
|
|
105
|
+
"## Findings",
|
|
106
|
+
"",
|
|
107
|
+
"- `F-001` — severity: high; affected: VC-001; evidence: src/a.ts; impact: x; recommended fix: y; suggested disposition: accept.",
|
|
108
|
+
"",
|
|
109
|
+
"## Questions",
|
|
110
|
+
"",
|
|
111
|
+
"None.",
|
|
112
|
+
].join("\n");
|
|
113
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
114
|
+
assert.deepEqual(passed, []);
|
|
115
|
+
assert.deepEqual(failed, [], "a reviewer-shaped report is not a failed audit");
|
|
116
|
+
assert.deepEqual(undeterminable.sort(), ["VC-001", "VC-002", "VC-003", "VC-004"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("classifies a partially covered report per check", () => {
|
|
120
|
+
const report = "- `VC-001` — verdict: pass\n- `VC-003` — verdict: **pass**";
|
|
121
|
+
const { passed, failed, undeterminable } = parseAuditReport(report, ALL_IDS);
|
|
122
|
+
assert.deepEqual(passed, ["VC-001", "VC-003"]);
|
|
123
|
+
assert.deepEqual(failed, []);
|
|
124
|
+
assert.deepEqual(undeterminable.sort(), ["VC-002", "VC-004"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("builds the brief with the auditable pending checks only", () => {
|
|
128
|
+
const task = buildAuditTask("/tmp/PLAN_v1.md", checks(), view(), 1);
|
|
129
|
+
assert.match(task, /read-only/i);
|
|
130
|
+
assert.match(task, /`VC-001`/);
|
|
131
|
+
assert.doesNotMatch(task, /`VC-999`/);
|
|
132
|
+
assert.doesNotMatch(task, /`VC-004`/, "a check covering no task is never audited");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("brief states the tri-state contract and forbids fail-for-want-of-evidence", () => {
|
|
136
|
+
const task = buildAuditTask("/tmp/PLAN_v1.md", checks(), view(), 1);
|
|
137
|
+
assert.match(task, /verdict: pass \| fail \| undeterminable/);
|
|
138
|
+
assert.match(task, /never report `fail` for want of evidence/i);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("brief omits checks already done in an earlier round", () => {
|
|
142
|
+
// Regression: the brief listed every auditable check, so a round-2
|
|
143
|
+
// auditor that legitimately skipped the satisfied ones looked like a
|
|
144
|
+
// contract violation -- and, with no rollback to produce progress, the
|
|
145
|
+
// budget climbed to the cap and the run livelocked.
|
|
146
|
+
const done = buildAuditTask("/tmp/PLAN_v1.md", checks(["VC-001"]), view(), 2);
|
|
147
|
+
assert.doesNotMatch(done, /`VC-001`/);
|
|
148
|
+
assert.match(done, /`VC-002`/);
|
|
149
|
+
assert.deepEqual(
|
|
150
|
+
auditablePendingChecks(checks(["VC-001"]), view()).map((item) => item.id),
|
|
151
|
+
["VC-002", "VC-003"],
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("applyAuditOutcome classifies without touching the checklist or task tree", () => {
|
|
156
|
+
const checklist = checks();
|
|
157
|
+
const tasks = view({ "Task-1": { status: "complete" }, "Task-2": { status: "complete" }, "Task-3": { status: "complete" }, "Task-3.1": { status: "complete" } });
|
|
158
|
+
const parsed = parseAuditReport("- `VC-001` — verdict: pass\n- `VC-002` — verdict: fail", ["VC-001", "VC-002"]);
|
|
159
|
+
const outcome = applyAuditOutcome(3, parsed, "report text");
|
|
160
|
+
assert.deepEqual(outcome.passed, ["VC-001"]);
|
|
161
|
+
assert.deepEqual(outcome.failed, ["VC-002"]);
|
|
162
|
+
assert.deepEqual(outcome.undeterminable, []);
|
|
163
|
+
assert.equal(outcome.round, 3);
|
|
164
|
+
assert.equal(outcome.report, "report text");
|
|
165
|
+
assert.equal(checklist.every((item) => item.done === false), true, "no done flag written here");
|
|
166
|
+
assert.equal(tasks.find((t) => t.id === "Task-2")?.status, "complete", "no rollback here");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
describe("audit rollback boundaries", () => {
|
|
171
|
+
it("failed multi-cover check rolls back every covered task", () => {
|
|
172
|
+
const tasks = view({ "Task-1": { status: "complete" }, "Task-2": { status: "complete" }, "Task-3": { status: "complete" }, "Task-3.1": { status: "complete" } });
|
|
173
|
+
const rolledBack = auditRollbackSet(tasks, checks(), "VC-002");
|
|
174
|
+
assert.deepEqual(rolledBack.sort(), ["Task-2", "Task-3", "Task-3.1"]);
|
|
175
|
+
// A check that passed keeps its task closed.
|
|
176
|
+
assert.equal(tasks.find((t) => t.id === "Task-1")?.status, "complete");
|
|
177
|
+
assert.equal(tasks.find((t) => t.id === "Task-2")?.status, "pending");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("covering the parent cascades to subtasks even when the parent alone is listed", () => {
|
|
181
|
+
const tasks = view({ "Task-3": { status: "complete" }, "Task-3.1": { status: "complete" } });
|
|
182
|
+
assert.ok(auditRollbackSet(tasks, checks(), "VC-002").includes("Task-3.1"), "subtask reopens with the parent");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("skipped tasks reopen too (skip state cleared)", () => {
|
|
186
|
+
const tasks = view({ "Task-2": { status: "skipped" } });
|
|
187
|
+
assert.ok(auditRollbackSet(tasks, checks(), "VC-002").includes("Task-2"));
|
|
188
|
+
assert.equal(tasks.find((t) => t.id === "Task-2")?.skipReason, undefined);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("checks with no task coverage are excluded from presolved audit", () => {
|
|
192
|
+
const tasks = view({ "Task-1": { status: "complete" }, "Task-2": { status: "complete" }, "Task-3": { status: "complete" }, "Task-3.1": { status: "complete" } });
|
|
193
|
+
const presolved = presolvedCheckIds(checks(), tasks);
|
|
194
|
+
assert.ok(!presolved.includes("VC-004"), "no-cover check is never audited");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("mixed coverage (skipped + complete siblings) resolves as skipped-pass (I-007)", () => {
|
|
198
|
+
const tasks = view({ "Task-2": { status: "skipped" }, "Task-3": { status: "complete" }, "Task-3.1": { status: "complete" } });
|
|
199
|
+
const presolved = presolvedCheckIds(checks(), tasks);
|
|
200
|
+
assert.ok(presolved.includes("VC-002"), "skipped + complete siblings presolve");
|
|
201
|
+
assert.ok(!presolved.includes("VC-003"), "pure complete coverage goes to the auditor");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("all-skipped coverage resolves as skipped-pass", () => {
|
|
205
|
+
const tasks = view({ "Task-3": { status: "skipped" }, "Task-3.1": { status: "skipped" } });
|
|
206
|
+
const presolved = presolvedCheckIds(checks(), tasks);
|
|
207
|
+
assert.ok(presolved.includes("VC-003"));
|
|
208
|
+
assert.ok(!presolved.includes("VC-002"), "mixed coverage needs the auditor");
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -10,7 +10,7 @@ import * as path from "node:path";
|
|
|
10
10
|
import { after, before, describe, it } from "node:test";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { registerAskChoiceTool } from "../tools/ask-choice.ts";
|
|
13
|
-
import { executeHandoff
|
|
13
|
+
import { executeHandoff } from "../tools/execute-plan.ts";
|
|
14
14
|
import {
|
|
15
15
|
AUTO_APPROVE_ENV,
|
|
16
16
|
assertAutoApprovable,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
isExternalStateQuestion,
|
|
19
19
|
} from "../src/auto-approve.ts";
|
|
20
20
|
import { getExecution, stopExecution } from "../src/exec.ts";
|
|
21
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
21
22
|
|
|
22
23
|
type ToolDef = {
|
|
23
24
|
execute: (id: string, params: any, signal: undefined, update: undefined, ctx: any) => Promise<any>;
|
|
@@ -55,7 +56,7 @@ function loadTool(): ToolDef {
|
|
|
55
56
|
function makeCtx(opts: { hasUI?: boolean } = {}): any {
|
|
56
57
|
selectCalls = 0;
|
|
57
58
|
confirmCalls = 0;
|
|
58
|
-
|
|
59
|
+
const ctx = {
|
|
59
60
|
cwd: root,
|
|
60
61
|
hasUI: opts.hasUI ?? false,
|
|
61
62
|
sessionManager: {},
|
|
@@ -74,6 +75,8 @@ function makeCtx(opts: { hasUI?: boolean } = {}): any {
|
|
|
74
75
|
theme: { fg: (_kind: string, text: string) => text, bold: (text: string) => text },
|
|
75
76
|
},
|
|
76
77
|
};
|
|
78
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
79
|
+
return ctx;
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
const LIFECYCLE_OPTIONS = [
|
|
@@ -241,15 +244,9 @@ describe("execute_handoff under auto-approve", () => {
|
|
|
241
244
|
});
|
|
242
245
|
|
|
243
246
|
function makeExecMocks() {
|
|
244
|
-
const pi = {
|
|
245
|
-
appendEntry: () => {},
|
|
246
|
-
sendMessage: () => {},
|
|
247
|
-
setModel: async () => true,
|
|
248
|
-
} as unknown as ExtensionAPI;
|
|
249
|
-
setCurrentApi(pi);
|
|
250
247
|
const ctx = makeCtx({ hasUI: false });
|
|
251
248
|
ctx.cwd = root;
|
|
252
|
-
return {
|
|
249
|
+
return { ctx };
|
|
253
250
|
}
|
|
254
251
|
|
|
255
252
|
it("approves headlessly with an [auto-approve] annotated result and no confirm call", async () => {
|
|
@@ -261,7 +258,7 @@ describe("execute_handoff under auto-approve", () => {
|
|
|
261
258
|
assert.match(outcome.message, /^\[auto-approve\] Execution approved/);
|
|
262
259
|
assert.equal(confirmCalls, 0, "no confirm prompt may open");
|
|
263
260
|
} finally {
|
|
264
|
-
if (getExecution()) await stopExecution(
|
|
261
|
+
if (getExecution()) await stopExecution(ctx, "cleanup");
|
|
265
262
|
setEnv(undefined);
|
|
266
263
|
}
|
|
267
264
|
});
|
|
@@ -14,9 +14,9 @@ import {
|
|
|
14
14
|
recordAskChoice,
|
|
15
15
|
registerAutoCompleteTurnHandlers,
|
|
16
16
|
restoreAutoCompleteFromSession,
|
|
17
|
-
setAutoCompleteApi,
|
|
18
17
|
} from "../src/autocomplete.ts";
|
|
19
18
|
import { initState, setRunStatus, startRun } from "../src/state.ts";
|
|
19
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
20
20
|
|
|
21
21
|
let root: string;
|
|
22
22
|
|
|
@@ -46,7 +46,6 @@ before(() => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
after(() => {
|
|
49
|
-
setAutoCompleteApi(null);
|
|
50
49
|
fs.rmSync(root, { recursive: true, force: true });
|
|
51
50
|
});
|
|
52
51
|
|
|
@@ -54,8 +53,8 @@ describe("Auto-complete state", () => {
|
|
|
54
53
|
it("enables only for the active planning run and restores only while planning", () => {
|
|
55
54
|
const { workdir, runId } = makeRun("restore");
|
|
56
55
|
const entries: any[] = [];
|
|
57
|
-
setAutoCompleteApi({ appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }) } as any);
|
|
58
56
|
const ctx = makeContext(workdir);
|
|
57
|
+
setMessagingApi({ appendEntry: (customType: string, data: unknown) => entries.push({ type: "custom", customType, data }), sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
59
58
|
|
|
60
59
|
assert.equal(enableAutoComplete(ctx), true);
|
|
61
60
|
assert.equal(autoCompleteStatus(ctx), "enabled");
|
|
@@ -79,8 +78,8 @@ describe("Auto-complete state", () => {
|
|
|
79
78
|
it("clears explicitly and marks plan writes as a continuation boundary", () => {
|
|
80
79
|
const { workdir } = makeRun("clear");
|
|
81
80
|
const entries: any[] = [];
|
|
82
|
-
setAutoCompleteApi({ appendEntry: (customType: string, data: unknown) => entries.push({ customType, data }) } as any);
|
|
83
81
|
const ctx = makeContext(workdir);
|
|
82
|
+
setMessagingApi({ appendEntry: (customType: string, data: unknown) => entries.push({ customType, data }), sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
84
83
|
enableAutoComplete(ctx);
|
|
85
84
|
markPlanWritten(ctx);
|
|
86
85
|
recordAskChoice(ctx, true);
|
|
@@ -98,12 +97,10 @@ describe("Auto-complete continuation", () => {
|
|
|
98
97
|
const handlers = new Map<string, Function[]>();
|
|
99
98
|
const pi: any = {
|
|
100
99
|
on: (name: string, handler: Function) => handlers.set(name, [...(handlers.get(name) ?? []), handler]),
|
|
101
|
-
appendEntry: () => {},
|
|
102
|
-
sendUserMessage: async (content: string, options: unknown) => sent.push({ content, options }),
|
|
103
100
|
};
|
|
104
|
-
setAutoCompleteApi(pi);
|
|
105
101
|
registerAutoCompleteTurnHandlers(pi);
|
|
106
102
|
const ctx = makeContext(workdir, session);
|
|
103
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async (content: string, options: unknown) => sent.push({ content, options }) });
|
|
107
104
|
enableAutoComplete(ctx);
|
|
108
105
|
await handlers.get("turn_start")?.[0]?.({}, ctx);
|
|
109
106
|
recordAskChoice(ctx, true);
|
|
@@ -132,10 +129,10 @@ describe("ask_choice Auto-complete wiring", () => {
|
|
|
132
129
|
assert.match(source, /isAutoCompleteEnabled/);
|
|
133
130
|
assert.match(source, /recordAskChoice\(ctx, true\)/);
|
|
134
131
|
assert.match(source, /autoComplete && selected\.startsWith\("Auto-complete"\)/);
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
assert.
|
|
138
|
-
assert.
|
|
132
|
+
// v0.6.1: the auto-refine trailing param was removed with the
|
|
133
|
+
// implementation-review loop.
|
|
134
|
+
assert.doesNotMatch(source, /auto-refine-loop/);
|
|
135
|
+
assert.doesNotMatch(source, /AUTO_REFINE_LOOP_LABEL/);
|
|
139
136
|
const indexSource = fs.readFileSync(path.join(process.cwd(), "index.ts"), "utf8");
|
|
140
137
|
assert.match(indexSource, /plans-autocomplete-stop/);
|
|
141
138
|
assert.match(indexSource, /autoCompleteStatus\(ctx\)/);
|
|
@@ -51,9 +51,9 @@ async function setupIndexedRepo(): Promise<{ root: string; store: Store; cleanup
|
|
|
51
51
|
tsx: makeBackend("tsx", ParserCtor, runtime.runtime.parser.tsx),
|
|
52
52
|
python: new PythonBackend(ParserCtor, runtime.runtime.parser.python),
|
|
53
53
|
};
|
|
54
|
-
fs.mkdirSync(path.join(root, ".git", "
|
|
54
|
+
fs.mkdirSync(path.join(root, ".git", "pi-plans"), { recursive: true });
|
|
55
55
|
const store = new Store(
|
|
56
|
-
{ dbPath: path.join(root, ".git", "
|
|
56
|
+
{ dbPath: path.join(root, ".git", "pi-plans", "code_graph.db"), worktreeRoot: root, gitCommonDir: path.join(root, ".git") },
|
|
57
57
|
runtime.runtime.sqlite,
|
|
58
58
|
);
|
|
59
59
|
runIndex({ store, worktreeRoot: root, parsers, reindex: false });
|
|
@@ -31,7 +31,7 @@ function initRepo(): { root: string; cleanup: () => void } {
|
|
|
31
31
|
fs.writeFileSync(path.join(root, "helper.ts"), "export function twice(n: number): number { return n * 2; }\n");
|
|
32
32
|
git(root, ["add", "-A"]);
|
|
33
33
|
git(root, ["commit", "-m", "init"]);
|
|
34
|
-
fs.mkdirSync(path.join(root, ".git", "
|
|
34
|
+
fs.mkdirSync(path.join(root, ".git", "pi-plans"), { recursive: true });
|
|
35
35
|
const canonicalRoot = fs.realpathSync(root);
|
|
36
36
|
return {
|
|
37
37
|
root: canonicalRoot,
|
|
@@ -63,7 +63,7 @@ async function loadParsers() {
|
|
|
63
63
|
|
|
64
64
|
function openStore(root: string, sqlite: typeof import("node:sqlite")): Store {
|
|
65
65
|
const canonicalRoot = fs.realpathSync(root);
|
|
66
|
-
const dbPath = path.join(canonicalRoot, ".git", "
|
|
66
|
+
const dbPath = path.join(canonicalRoot, ".git", "pi-plans", "code_graph.db");
|
|
67
67
|
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
68
68
|
return new Store({ dbPath, worktreeRoot: canonicalRoot, gitCommonDir: path.join(canonicalRoot, ".git") }, sqlite);
|
|
69
69
|
}
|
|
@@ -63,7 +63,7 @@ test("runIndex indexes files, writes rows, and screening excludes full_code", as
|
|
|
63
63
|
};
|
|
64
64
|
const sqlite = await import("node:sqlite");
|
|
65
65
|
const commonDir = git(worktreeRoot, ["rev-parse", "--git-common-dir"]);
|
|
66
|
-
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "
|
|
66
|
+
const dbDir = path.join(path.resolve(worktreeRoot, commonDir), "pi-plans");
|
|
67
67
|
fs.mkdirSync(dbDir, { recursive: true });
|
|
68
68
|
const dbPath = path.join(dbDir, "code_graph.db");
|
|
69
69
|
try { fs.unlinkSync(dbPath); } catch { /* ignore */ }
|
|
@@ -151,7 +151,7 @@ test("runIndex with opts.paths indexes only hit paths and purges missing ones",
|
|
|
151
151
|
tsx: makeBackend("tsx", ParserCtor, runtime.runtime.parser.tsx),
|
|
152
152
|
python: new PythonBackend(ParserCtor, runtime.runtime.parser.python),
|
|
153
153
|
};
|
|
154
|
-
const stateRoot = path.join(worktreeRoot, ".git", "
|
|
154
|
+
const stateRoot = path.join(worktreeRoot, ".git", "pi-plans");
|
|
155
155
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
156
156
|
const store = new Store({ dbPath: path.join(stateRoot, "code_graph.db"), worktreeRoot, gitCommonDir: path.join(worktreeRoot, ".git") }, runtime.runtime.sqlite);
|
|
157
157
|
t.after(() => store.close());
|
|
@@ -57,7 +57,7 @@ test("closed loop: init → mutation → apply → drift → final-commit → cl
|
|
|
57
57
|
const { root, cleanup } = initRepo();
|
|
58
58
|
t.after(cleanup);
|
|
59
59
|
|
|
60
|
-
const stateRoot = path.join(root, ".git", "
|
|
60
|
+
const stateRoot = path.join(root, ".git", "pi-plans");
|
|
61
61
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
62
62
|
const dbPath = path.join(stateRoot, "code_graph.db");
|
|
63
63
|
const store = new Store({ dbPath, worktreeRoot: root, gitCommonDir: path.join(root, ".git") }, runtime.runtime.sqlite);
|
|
@@ -51,7 +51,7 @@ test("updateFunction propagates offsets, rebuilds source exactly, and applies by
|
|
|
51
51
|
if (!runtime.status.parserAvailable || !runtime.status.sqliteAvailable) return;
|
|
52
52
|
const { root, cleanup } = initRepo();
|
|
53
53
|
t.after(cleanup);
|
|
54
|
-
const stateRoot = path.join(root, ".git", "
|
|
54
|
+
const stateRoot = path.join(root, ".git", "pi-plans");
|
|
55
55
|
fs.mkdirSync(stateRoot, { recursive: true });
|
|
56
56
|
const store = new Store({ dbPath: path.join(stateRoot, "g.db"), worktreeRoot: root, gitCommonDir: path.join(root, ".git") }, runtime.runtime.sqlite);
|
|
57
57
|
t.after(() => store.close());
|