pi-plans 0.6.0 → 0.7.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/CONTRIBUTING.md +3 -3
- package/README.md +39 -37
- package/agents/reviewer.md +12 -3
- package/index.ts +42 -35
- package/package.json +1 -1
- package/references/pi-planning-workflow.md +44 -60
- package/references/plan-artifact-template.md +71 -60
- package/references/state-and-config.md +59 -43
- 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 +20 -9
- 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 +126 -0
- package/src/auto-approve.ts +1 -1
- package/src/autocomplete.ts +19 -17
- package/src/code-graph/commands.ts +2 -2
- 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 +257 -0
- package/src/exec.ts +692 -919
- 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 +1 -1
- package/src/resume-command.ts +34 -128
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +3 -10
- package/src/state.ts +272 -72
- package/src/subagent.ts +19 -29
- package/src/task-tool.ts +100 -0
- package/src/tasks.ts +189 -0
- package/src/thinking-levels.ts +67 -0
- package/src/ui-language.ts +3 -54
- package/src/workflow-state.ts +63 -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 +111 -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 +268 -0
- package/tests/exec-lifecycle.test.ts +181 -115
- package/tests/exec-panel-lifecycle.test.ts +106 -251
- package/tests/exec.test.ts +617 -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 +37 -22
- package/tests/resume.test.ts +33 -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/state.test.ts +151 -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/thinking-levels.test.ts +77 -0
- package/tests/ui-language.test.ts +2 -17
- package/tests/workflow-state.test.ts +17 -99
- 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 +48 -99
- package/tools/graph-aware-file-tools.ts +4 -10
- package/tools/plans.ts +40 -66
- 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
|
@@ -7,7 +7,7 @@ import * as os from "node:os";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import { after, before, describe, it } from "node:test";
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
applyLaneResult,
|
|
12
12
|
applyReviewConsolidated,
|
|
13
13
|
createCheckpoint,
|
|
@@ -19,8 +19,11 @@ import {
|
|
|
19
19
|
startReviewRound,
|
|
20
20
|
} from "../src/workflow-state.ts";
|
|
21
21
|
import { initState, startRun } from "../src/state.ts";
|
|
22
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
22
23
|
|
|
23
24
|
let tmpRoot: string;
|
|
25
|
+
let globalDir: string;
|
|
26
|
+
let previousGlobalDir: string | undefined;
|
|
24
27
|
|
|
25
28
|
function setupRun(name: string): { workdir: string; runId: string; artifactDir: string } {
|
|
26
29
|
const workdir = path.join(tmpRoot, name);
|
|
@@ -35,9 +38,17 @@ function setupRun(name: string): { workdir: string; runId: string; artifactDir:
|
|
|
35
38
|
|
|
36
39
|
before(() => {
|
|
37
40
|
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-rr-"));
|
|
41
|
+
// Isolate the global reviewer config (F-002): setRole writes the global
|
|
42
|
+
// file; tests must never touch the developer's ~/.pi/pi-plans.
|
|
43
|
+
previousGlobalDir = process.env.PI_PLANS_GLOBAL_DIR;
|
|
44
|
+
globalDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-global-rr-"));
|
|
45
|
+
process.env.PI_PLANS_GLOBAL_DIR = globalDir;
|
|
38
46
|
});
|
|
39
47
|
|
|
40
48
|
after(() => {
|
|
49
|
+
if (previousGlobalDir === undefined) delete process.env.PI_PLANS_GLOBAL_DIR;
|
|
50
|
+
else process.env.PI_PLANS_GLOBAL_DIR = previousGlobalDir;
|
|
51
|
+
fs.rmSync(globalDir, { recursive: true, force: true });
|
|
41
52
|
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
42
53
|
});
|
|
43
54
|
|
|
@@ -194,6 +205,7 @@ describe("implementation-review round 1 fixes", () => {
|
|
|
194
205
|
hasUI: false,
|
|
195
206
|
ui: { notify: () => {}, setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } },
|
|
196
207
|
};
|
|
208
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
197
209
|
const result = await tool!.execute("t1", { role: "reviewer", planPath, reviewers: 2 }, undefined, undefined, ctx);
|
|
198
210
|
assert.match(result.content[0]?.text ?? "", /current-session/);
|
|
199
211
|
// The durable round exists WITH lanes — the pre-fix crash site.
|
|
@@ -236,9 +248,9 @@ describe("implementation-review round 1 fixes", () => {
|
|
|
236
248
|
// Edit the plan in place — same path, different bytes.
|
|
237
249
|
fs.writeFileSync(planPath, "# plan (edited)\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n", "utf8");
|
|
238
250
|
const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
|
|
239
|
-
const pi = { appendEntry: () => {}, sendMessage: () => {} } as never;
|
|
240
251
|
const ctx = { cwd: workdir, sessionManager: {}, ui: { setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } } };
|
|
241
|
-
|
|
252
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
253
|
+
const load = loadExecutionFromCheckpoint(ctx as never, run.run_id);
|
|
242
254
|
assert.equal(load.status, "plan-mismatch");
|
|
243
255
|
assert.match((load as { error?: string }).error ?? "", /changed since the approval/);
|
|
244
256
|
});
|
|
@@ -266,9 +278,9 @@ describe("implementation-review round 1 fixes", () => {
|
|
|
266
278
|
});
|
|
267
279
|
mutateCheckpoint(workdir, run.run_id, (cp) => applyExecutionProgress(cp, { doneVcIds: ["VC-001"] }));
|
|
268
280
|
const { loadExecutionFromCheckpoint } = await import("../src/exec.ts");
|
|
269
|
-
const pi = { appendEntry: () => {}, sendMessage: () => {} } as never;
|
|
270
281
|
const ctx = { cwd: workdir, sessionManager: {}, ui: { setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } } };
|
|
271
|
-
|
|
282
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
283
|
+
const load = loadExecutionFromCheckpoint(ctx as never, run.run_id);
|
|
272
284
|
assert.equal(load.status, "loaded");
|
|
273
285
|
assert.equal((load as { reverifyAll?: boolean }).reverifyAll, true, "unverifiable HEAD re-verifies");
|
|
274
286
|
});
|
|
@@ -285,7 +297,7 @@ describe("implementation-review round 1 fixes", () => {
|
|
|
285
297
|
const owner = acquireOwnership(workdir, run.run_id);
|
|
286
298
|
// Another process takes over (fabricated record with a reused/dead pid).
|
|
287
299
|
fs.writeFileSync(
|
|
288
|
-
path.join(workdir, ".git", "
|
|
300
|
+
path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "owner.json"),
|
|
289
301
|
JSON.stringify({ ...owner, pid: process.pid, pidStart: "bogus-start", processToken: "someone-else", generation: owner.generation + 1 }),
|
|
290
302
|
"utf8",
|
|
291
303
|
);
|
|
@@ -323,7 +335,7 @@ describe("implementation-review round 1 fixes", () => {
|
|
|
323
335
|
});
|
|
324
336
|
});
|
|
325
337
|
|
|
326
|
-
describe("
|
|
338
|
+
describe("plan-review reviewer-count defaults (v0.6.1 single-reviewer)", () => {
|
|
327
339
|
async function setupLoopRun(name: string, skill: string, reviewerCount?: number) {
|
|
328
340
|
const workdir = path.join(tmpRoot, name);
|
|
329
341
|
fs.mkdirSync(workdir, { recursive: true });
|
|
@@ -336,16 +348,6 @@ describe("implementation-review reviewer-count fallback (0.5.4, D-4)", () => {
|
|
|
336
348
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
337
349
|
const planPath = path.join(run.artifact_dir, "PLAN_v1.md");
|
|
338
350
|
fs.writeFileSync(planPath, "# plan\n\n## Verifier Checklist\n\n- [ ] `VC-001` covers `I-001`; pass condition: x.\n", "utf8");
|
|
339
|
-
// refine's round-start guard requires a recorded termination condition
|
|
340
|
-
// before any implementation round, so the loop is always configured;
|
|
341
|
-
// reviewerCount may legitimately be absent (skill default applies).
|
|
342
|
-
mutateCheckpoint(workdir, run.run_id, (cp) =>
|
|
343
|
-
applyImplementationReviewConfigured(
|
|
344
|
-
{ ...cp, phase: "implementation-review", nextAction: "ask-question" },
|
|
345
|
-
"1 round",
|
|
346
|
-
reviewerCount,
|
|
347
|
-
),
|
|
348
|
-
);
|
|
349
351
|
return { workdir, runId: run.run_id, planPath };
|
|
350
352
|
}
|
|
351
353
|
|
|
@@ -373,13 +375,14 @@ describe("implementation-review reviewer-count fallback (0.5.4, D-4)", () => {
|
|
|
373
375
|
hasUI: false,
|
|
374
376
|
ui: { notify: () => {}, setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } },
|
|
375
377
|
};
|
|
376
|
-
|
|
378
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
379
|
+
const result = await tool.execute("t1", { planPath, reviewers: 3 }, undefined, undefined, ctx);
|
|
377
380
|
assert.match(result.content[0]?.text ?? "", /current-session/);
|
|
378
381
|
const loaded = loadCheckpoint(workdir, runId);
|
|
379
382
|
assert.ok(loaded.status === "ok");
|
|
380
383
|
const round = loaded.checkpoint.reviewRounds.at(-1);
|
|
381
384
|
assert.ok(round, "round recorded");
|
|
382
|
-
assert.equal(round!.reviewers, 3, "
|
|
385
|
+
assert.equal(round!.reviewers, 3, "explicit reviewers=3 honored");
|
|
383
386
|
assert.equal(round!.lanes.length, 3, "three lanes spawned");
|
|
384
387
|
});
|
|
385
388
|
|
|
@@ -394,7 +397,8 @@ describe("implementation-review reviewer-count fallback (0.5.4, D-4)", () => {
|
|
|
394
397
|
hasUI: false,
|
|
395
398
|
ui: { notify: () => {}, setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } },
|
|
396
399
|
};
|
|
397
|
-
|
|
400
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
401
|
+
await tool.execute("t1", { planPath, reviewers: 1 }, undefined, undefined, ctx);
|
|
398
402
|
const loaded = loadCheckpoint(workdir, runId);
|
|
399
403
|
assert.ok(loaded.status === "ok");
|
|
400
404
|
const round = loaded.checkpoint.reviewRounds.at(-1);
|
|
@@ -413,11 +417,12 @@ describe("implementation-review reviewer-count fallback (0.5.4, D-4)", () => {
|
|
|
413
417
|
hasUI: false,
|
|
414
418
|
ui: { notify: () => {}, setStatus: () => {}, theme: { fg: (_c: string, t: string) => t } },
|
|
415
419
|
};
|
|
416
|
-
|
|
420
|
+
setMessagingApi({ appendEntry: () => {}, sendMessage: () => {}, sendUserMessage: async () => {} });
|
|
421
|
+
await tool.execute("t1", { planPath }, undefined, undefined, ctx);
|
|
417
422
|
const loaded = loadCheckpoint(workdir, runId);
|
|
418
423
|
assert.ok(loaded.status === "ok");
|
|
419
424
|
const round = loaded.checkpoint.reviewRounds.at(-1);
|
|
420
|
-
assert.equal(round!.lanes.length, 1, "no
|
|
425
|
+
assert.equal(round!.lanes.length, 1, "no param → 1 lane default");
|
|
421
426
|
});
|
|
422
427
|
|
|
423
428
|
it("count=2 consolidation text carries the source-reviewer contract (D-7)", async () => {
|
package/tests/refine-ui.test.ts
CHANGED
|
@@ -151,15 +151,6 @@ describe("refine overlay viewport", () => {
|
|
|
151
151
|
);
|
|
152
152
|
assert.ok(reviewer.render(120).some((line) => line.includes("Reviewer (m1)")));
|
|
153
153
|
|
|
154
|
-
const criticizer = new RefineOverlayComponent(
|
|
155
|
-
fakeTheme,
|
|
156
|
-
"criticizer",
|
|
157
|
-
[readyLane("lane-1", "criticizer", "ok")],
|
|
158
|
-
() => {},
|
|
159
|
-
undefined,
|
|
160
|
-
"m2",
|
|
161
|
-
);
|
|
162
|
-
assert.ok(criticizer.render(120).some((line) => line.includes("Criticizer (m2)")));
|
|
163
154
|
});
|
|
164
155
|
|
|
165
156
|
it("renders model-aware titles and footer hints", () => {
|
|
@@ -230,14 +221,14 @@ describe("refine overlay viewport", () => {
|
|
|
230
221
|
const linesText = ["line 1", "line 2", "line 3", "line 4", "line 5"].join("\n");
|
|
231
222
|
const component = new RefineOverlayComponent(
|
|
232
223
|
fakeTheme,
|
|
233
|
-
"
|
|
234
|
-
[streamingToolLane("lane-1", "
|
|
224
|
+
"reviewer",
|
|
225
|
+
[streamingToolLane("lane-1", "reviewer", linesText)],
|
|
235
226
|
() => {},
|
|
236
227
|
undefined,
|
|
237
228
|
"singularity-gpt/gpt-5.5:xhigh",
|
|
238
229
|
);
|
|
239
230
|
const lines = component.render(90);
|
|
240
|
-
assert.ok(lines.some((line) => line.includes("
|
|
231
|
+
assert.ok(lines.some((line) => line.includes("Reviewer (singularity-gpt/gpt-5.5:xhigh)")));
|
|
241
232
|
assert.ok(lines.some((line) => line.includes("…")));
|
|
242
233
|
assert.ok(lines.some((line) => line.includes("line 3")));
|
|
243
234
|
assert.ok(lines.some((line) => line.includes("line 4")));
|
|
@@ -269,8 +260,8 @@ describe("refine overlay viewport", () => {
|
|
|
269
260
|
it("renders a single pane with pi-btw-style transcript badges", () => {
|
|
270
261
|
const component = new RefineOverlayComponent(
|
|
271
262
|
fakeTheme,
|
|
272
|
-
"
|
|
273
|
-
[readyLane("lane-1", "
|
|
263
|
+
"refs",
|
|
264
|
+
[readyLane("lane-1", "refs", "assistant output")],
|
|
274
265
|
() => {},
|
|
275
266
|
);
|
|
276
267
|
const lines = component.render(90);
|
|
@@ -340,7 +331,7 @@ describe("refine overlay wiring", () => {
|
|
|
340
331
|
it("threads the chrome language through both overlay construction sites (issue #3, VC-007)", () => {
|
|
341
332
|
const refineSource = fs.readFileSync(path.join(process.cwd(), "tools", "refine.ts"), "utf8");
|
|
342
333
|
assert.match(refineSource, /const overlayLang = uiLanguageFromTag\(config\.language\.tag\)/);
|
|
343
|
-
assert.match(refineSource, /new RefineOverlayController\(
|
|
334
|
+
assert.match(refineSource, /new RefineOverlayController\("reviewer", lanes, relayAbort, lang\)/);
|
|
344
335
|
assert.match(refineSource, /modelLabel,\s*\n\s*overlayLang,/);
|
|
345
336
|
const refsSource = fs.readFileSync(path.join(process.cwd(), "tools", "analyze-refs.ts"), "utf8");
|
|
346
337
|
assert.match(
|
|
@@ -25,7 +25,7 @@ import { startExecution } from "../src/exec.ts";
|
|
|
25
25
|
import { resetRunBindingForTests } from "../src/run-context.ts";
|
|
26
26
|
import { processStartOf } from "../src/run-ownership.ts";
|
|
27
27
|
import { createCheckpoint, loadCheckpoint, mutateCheckpoint, applyQuestionAsked } from "../src/workflow-state.ts";
|
|
28
|
-
import { getRun, initState, startRun } from "../src/state.ts";
|
|
28
|
+
import { DEFAULT_CONFIG, getRun, initState, resolveArtifactRoot, startRun } from "../src/state.ts";
|
|
29
29
|
|
|
30
30
|
initTheme("dark", false);
|
|
31
31
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-resume-life-"));
|
|
@@ -34,10 +34,15 @@ let serial = 0;
|
|
|
34
34
|
|
|
35
35
|
const PLAN_TEXT = `# Plan
|
|
36
36
|
|
|
37
|
-
##
|
|
37
|
+
## Tasks
|
|
38
38
|
|
|
39
|
-
-
|
|
40
|
-
-
|
|
39
|
+
- Task-1: first — files: src/a.ts; wave: 1
|
|
40
|
+
- Task-2: second — files: src/b.ts; wave: 1
|
|
41
|
+
|
|
42
|
+
## Verification Checks
|
|
43
|
+
|
|
44
|
+
- [ ] \`VC-001\` covers \`Task-1\`; pass condition: first.
|
|
45
|
+
- [ ] \`VC-002\` covers \`Task-2\`; pass condition: second.
|
|
41
46
|
`;
|
|
42
47
|
|
|
43
48
|
interface Fixture {
|
|
@@ -173,6 +178,9 @@ function setupRun(name: string): { cwd: string; runId: string; artifactDir: stri
|
|
|
173
178
|
createCheckpoint(cwd, { runId: run.run_id, originWorkdir: cwd, workdir: cwd });
|
|
174
179
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
175
180
|
fs.writeFileSync(path.join(run.artifact_dir, "PLAN_v1.md"), PLAN_TEXT, "utf8");
|
|
181
|
+
// The artifact dir lives under .git/ and is never tracked; seed a root file so
|
|
182
|
+
// the "seed" commit exists and resolveHeadAt() yields the approval evidence HEAD.
|
|
183
|
+
fs.writeFileSync(path.join(cwd, "WORKTREE.md"), "seed worktree marker\n", "utf8");
|
|
176
184
|
spawnSync("git", ["add", "-A"], { cwd });
|
|
177
185
|
spawnSync("git", ["commit", "-m", "seed"], { cwd });
|
|
178
186
|
return { cwd, runId: run.run_id, artifactDir: run.artifact_dir };
|
|
@@ -185,12 +193,10 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
185
193
|
async () => {
|
|
186
194
|
resetRunBindingForTests();
|
|
187
195
|
const { cwd, runId } = setupRun("cross-session");
|
|
188
|
-
// Session 1: execution starts,
|
|
196
|
+
// Session 1: execution starts, Task-1 closes via the task tool,
|
|
197
|
+
// then the session ends.
|
|
189
198
|
const fixture1: Fixture = {
|
|
190
|
-
script: [
|
|
191
|
-
{ kind: "tool" },
|
|
192
|
-
{ kind: "text", text: "[DONE:VC-001] Partial progress; stopping here." },
|
|
193
|
-
],
|
|
199
|
+
script: [{ kind: "text", text: "Partial progress; stopping here." }],
|
|
194
200
|
calls: [],
|
|
195
201
|
};
|
|
196
202
|
const session1 = await makeSession({
|
|
@@ -200,10 +206,18 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
200
206
|
extraExtension: ((pi: never) => {
|
|
201
207
|
const realPi = pi as unknown as import("@earendil-works/pi-coding-agent").ExtensionAPI;
|
|
202
208
|
realPi.on("session_start", async (_event: unknown, ctx: never) => {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
209
|
+
const planPath = path.join(artifactDirRelative(cwd), "PLAN_v1.md");
|
|
210
|
+
const planText = fs.readFileSync(planPath, "utf8");
|
|
211
|
+
const { parsePlanTasks, parseChecklist } = await import("../src/plan.ts");
|
|
212
|
+
await startExecution(ctx as never, {
|
|
213
|
+
planPath,
|
|
214
|
+
planTasks: parsePlanTasks(planText),
|
|
215
|
+
items: parseChecklist(planText),
|
|
216
|
+
});
|
|
217
|
+
const { applyTaskUpdate } = await import("../src/task-tool.ts");
|
|
218
|
+
const { getExecution, persistTaskProgress } = await import("../src/exec.ts");
|
|
219
|
+
applyTaskUpdate(getExecution()!.tasks, "Task-1", "complete", "first tests green");
|
|
220
|
+
persistTaskProgress(ctx as never);
|
|
207
221
|
});
|
|
208
222
|
}) as never,
|
|
209
223
|
});
|
|
@@ -214,7 +228,7 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
214
228
|
const mid = loadCheckpoint(cwd, runId);
|
|
215
229
|
assert.equal(mid.status, "ok");
|
|
216
230
|
if (mid.status === "ok") {
|
|
217
|
-
assert.
|
|
231
|
+
assert.equal(mid.checkpoint.execution?.tasks?.["Task-1"]?.status, "complete", "task progress persisted by session 1");
|
|
218
232
|
assert.ok(mid.checkpoint.execution?.approval?.headAtApproval, "approval evidence persisted");
|
|
219
233
|
}
|
|
220
234
|
} finally {
|
|
@@ -225,7 +239,7 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
225
239
|
|
|
226
240
|
// Session 2: fresh session, resume via the command.
|
|
227
241
|
const fixture2: Fixture = {
|
|
228
|
-
script: [{ kind: "text", text: "
|
|
242
|
+
script: [{ kind: "text", text: "Resumed; continuing the remaining task." }],
|
|
229
243
|
calls: [],
|
|
230
244
|
};
|
|
231
245
|
const notifies: string[] = [];
|
|
@@ -238,16 +252,15 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
238
252
|
assert.ok(fixture2.calls.length >= 1);
|
|
239
253
|
const firstPayload = JSON.stringify(fixture2.calls[0]);
|
|
240
254
|
assert.match(firstPayload, /PI-PLANS RESUME/);
|
|
241
|
-
assert.match(firstPayload, /
|
|
255
|
+
assert.match(firstPayload, /plans_update_task/);
|
|
242
256
|
assert.ok(notifies.some((message) => /Resumed/.test(message)), "resume notification shown");
|
|
243
|
-
//
|
|
257
|
+
// Task-1 stays complete across the session boundary.
|
|
244
258
|
const final = loadCheckpoint(cwd, runId);
|
|
245
259
|
assert.equal(final.status, "ok");
|
|
246
260
|
if (final.status === "ok") {
|
|
247
|
-
assert.
|
|
248
|
-
assert.equal(final.checkpoint.
|
|
261
|
+
assert.equal(final.checkpoint.execution?.tasks?.["Task-1"]?.status, "complete");
|
|
262
|
+
assert.equal(final.checkpoint.execution?.tasks?.["Task-2"]?.status, undefined);
|
|
249
263
|
}
|
|
250
|
-
assert.equal(getRun(cwd, runId)?.status, "done");
|
|
251
264
|
} finally {
|
|
252
265
|
session2.session.dispose();
|
|
253
266
|
}
|
|
@@ -313,7 +326,7 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
313
326
|
const child = spawn("sleep", ["30"], { stdio: "ignore" });
|
|
314
327
|
try {
|
|
315
328
|
fs.writeFileSync(
|
|
316
|
-
path.join(cwd, ".git", "
|
|
329
|
+
path.join(cwd, ".git", "pi-plans", "runs", runId, "owner.json"),
|
|
317
330
|
JSON.stringify({
|
|
318
331
|
schema: 1,
|
|
319
332
|
host: os.hostname(),
|
|
@@ -369,7 +382,9 @@ describe("/resume-plans on the real Pi host", () => {
|
|
|
369
382
|
});
|
|
370
383
|
|
|
371
384
|
function artifactDirRelative(cwd: string): string {
|
|
372
|
-
|
|
385
|
+
// Resolve through the real default so this fixture follows the configured
|
|
386
|
+
// artifact root (inside the state dir by default) instead of hardcoding it.
|
|
387
|
+
const root = resolveArtifactRoot(cwd, DEFAULT_CONFIG.artifact_root);
|
|
373
388
|
return path.join(root, fs.readdirSync(root)[0]!);
|
|
374
389
|
}
|
|
375
390
|
|
package/tests/resume.test.ts
CHANGED
|
@@ -11,7 +11,8 @@ import { migrateRunIntoCurrentWorktree, resumePlansCommand } from "../src/resume
|
|
|
11
11
|
import { createCheckpoint, loadCheckpoint, mutateCheckpoint, applyQuestionAsked, applyQuestionAnswered } from "../src/workflow-state.ts";
|
|
12
12
|
import { acquireOwnership, processStartOf } from "../src/run-ownership.ts";
|
|
13
13
|
import { resetRunBindingForTests } from "../src/run-context.ts";
|
|
14
|
-
import { initState, setRunStatus, startRun, StateError } from "../src/state.ts";
|
|
14
|
+
import { initState, setArtifactRoot, setRunStatus, startRun, StateError } from "../src/state.ts";
|
|
15
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
15
16
|
|
|
16
17
|
let tmpRoot: string;
|
|
17
18
|
|
|
@@ -63,7 +64,7 @@ function makeCtx(cwd: string, overrides: Partial<CtxMock> = {}): CtxMock {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
function ctxAdapter(mock: CtxMock): unknown {
|
|
66
|
-
|
|
67
|
+
const ctx = {
|
|
67
68
|
cwd: mock.cwd,
|
|
68
69
|
hasUI: mock.hasUI,
|
|
69
70
|
isIdle: () => mock.isIdleResult,
|
|
@@ -84,21 +85,16 @@ function ctxAdapter(mock: CtxMock): unknown {
|
|
|
84
85
|
setStatus: () => {},
|
|
85
86
|
},
|
|
86
87
|
};
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
function makePi(mock: CtxMock): unknown {
|
|
90
|
-
return {
|
|
91
|
-
sendUserMessage: async (content: string) => {
|
|
92
|
-
mock.userMessages.push(content);
|
|
93
|
-
},
|
|
88
|
+
setMessagingApi({
|
|
94
89
|
appendEntry: (customType: string, data?: unknown) => {
|
|
95
90
|
mock.entries.push({ customType, data });
|
|
96
91
|
},
|
|
97
92
|
sendMessage: () => {},
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
};
|
|
93
|
+
sendUserMessage: async (content: string) => {
|
|
94
|
+
mock.userMessages.push(content);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
return ctx;
|
|
102
98
|
}
|
|
103
99
|
|
|
104
100
|
const BASE_DIR = path.resolve(import.meta.dirname, "..");
|
|
@@ -168,7 +164,7 @@ describe("candidate discovery", () => {
|
|
|
168
164
|
const run = startRun(workdir, { topic: "corrupt", skill: "plan-normal", requestText: "a" }).run;
|
|
169
165
|
createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
|
|
170
166
|
fs.writeFileSync(
|
|
171
|
-
path.join(workdir, ".git", "
|
|
167
|
+
path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "checkpoint.json"),
|
|
172
168
|
"{ broken",
|
|
173
169
|
"utf8",
|
|
174
170
|
);
|
|
@@ -182,27 +178,27 @@ describe("/resume-plans command", () => {
|
|
|
182
178
|
it("refuses non-interactive and busy sessions, notifies when nothing is resumable", async () => {
|
|
183
179
|
const workdir = setupRepo("guards");
|
|
184
180
|
const noUI = makeCtx(workdir, { hasUI: false });
|
|
185
|
-
await resumePlansCommand(
|
|
181
|
+
await resumePlansCommand(ctxAdapter(noUI) as never, BASE_DIR);
|
|
186
182
|
assert.ok(noUI.notifies.some((n) => /interactive/.test(n.message)));
|
|
187
183
|
|
|
188
184
|
const busy = makeCtx(workdir, { isIdleResult: false });
|
|
189
|
-
await resumePlansCommand(
|
|
185
|
+
await resumePlansCommand(ctxAdapter(busy) as never, BASE_DIR);
|
|
190
186
|
assert.ok(busy.notifies.some((n) => /busy/.test(n.message)));
|
|
191
187
|
assert.equal(busy.userMessages.length, 0);
|
|
192
188
|
|
|
193
189
|
const empty = setupRepo("empty-repo");
|
|
194
190
|
const emptyCtx = makeCtx(empty);
|
|
195
|
-
await resumePlansCommand(
|
|
191
|
+
await resumePlansCommand(ctxAdapter(emptyCtx) as never, BASE_DIR);
|
|
196
192
|
assert.ok(emptyCtx.notifies.some((n) => /No resumable/.test(n.message)));
|
|
197
193
|
});
|
|
198
194
|
|
|
199
195
|
it("corrupt checkpoints report and change nothing", async () => {
|
|
200
196
|
const workdir = setupRepo("corrupt-cmd");
|
|
201
197
|
const run = startRun(workdir, { topic: "corruptcmd", skill: "plan-normal", requestText: "a" }).run;
|
|
202
|
-
const cpPath = path.join(workdir, ".git", "
|
|
198
|
+
const cpPath = path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "checkpoint.json");
|
|
203
199
|
fs.writeFileSync(cpPath, "{ broken", "utf8");
|
|
204
200
|
const mock = makeCtx(workdir);
|
|
205
|
-
await resumePlansCommand(
|
|
201
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
206
202
|
assert.ok(mock.notifies.some((n) => /corrupt/.test(n.message)));
|
|
207
203
|
assert.equal(mock.userMessages.length, 0);
|
|
208
204
|
assert.equal(fs.readFileSync(cpPath, "utf8"), "{ broken");
|
|
@@ -225,7 +221,7 @@ describe("/resume-plans command", () => {
|
|
|
225
221
|
void cp;
|
|
226
222
|
|
|
227
223
|
const mock = makeCtx(workdir);
|
|
228
|
-
await resumePlansCommand(
|
|
224
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
229
225
|
assert.equal(mock.userMessages.length, 1, "exactly one kickoff");
|
|
230
226
|
const brief = mock.userMessages[0]!;
|
|
231
227
|
assert.match(brief, /PI-PLANS RESUME/);
|
|
@@ -244,7 +240,7 @@ describe("/resume-plans command", () => {
|
|
|
244
240
|
const latest = startRun(workdir, { topic: "three", skill: "plan-normal", requestText: "c" }).run;
|
|
245
241
|
setRunStatus(workdir, latest.run_id, "abandoned");
|
|
246
242
|
const mock = makeCtx(workdir, { selectAnswer: null });
|
|
247
|
-
await resumePlansCommand(
|
|
243
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
248
244
|
assert.equal(mock.userMessages.length, 0);
|
|
249
245
|
assert.ok(mock.notifies.some((n) => /Cancelled/.test(n.message)));
|
|
250
246
|
assert.equal(mock.selects.length, 1);
|
|
@@ -257,7 +253,7 @@ describe("/resume-plans command", () => {
|
|
|
257
253
|
const child = spawn("sleep", ["30"], { stdio: "ignore" });
|
|
258
254
|
try {
|
|
259
255
|
fs.writeFileSync(
|
|
260
|
-
path.join(workdir, ".git", "
|
|
256
|
+
path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "owner.json"),
|
|
261
257
|
JSON.stringify({
|
|
262
258
|
schema: 1,
|
|
263
259
|
host: os.hostname(),
|
|
@@ -271,7 +267,7 @@ describe("/resume-plans command", () => {
|
|
|
271
267
|
"utf8",
|
|
272
268
|
);
|
|
273
269
|
const mock = makeCtx(workdir);
|
|
274
|
-
await resumePlansCommand(
|
|
270
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
275
271
|
assert.ok(mock.notifies.some((n) => /actively owned/.test(n.message)));
|
|
276
272
|
assert.equal(mock.userMessages.length, 0);
|
|
277
273
|
} finally {
|
|
@@ -280,8 +276,11 @@ describe("/resume-plans command", () => {
|
|
|
280
276
|
});
|
|
281
277
|
|
|
282
278
|
it("cross-worktree: cancel changes nothing; confirm migrates artifacts and resets approval", async () => {
|
|
283
|
-
// Source worktree holds the artifacts inside its own tree.
|
|
279
|
+
// Source worktree holds the artifacts inside its own tree. D-007 only
|
|
280
|
+
// applies to a per-worktree root, so pin one; the default root lives in
|
|
281
|
+
// the shared git common dir and is never migrated.
|
|
284
282
|
const source = setupRepo("xwt-source", { commit: true });
|
|
283
|
+
setArtifactRoot(source, "./docs/pi-plans", "user");
|
|
285
284
|
const run = startRun(source, { topic: "xwt", skill: "plan-normal", requestText: "a" }).run;
|
|
286
285
|
const cp = createCheckpoint(source, { runId: run.run_id, originWorkdir: source, workdir: source });
|
|
287
286
|
void cp;
|
|
@@ -294,12 +293,12 @@ describe("/resume-plans command", () => {
|
|
|
294
293
|
spawnSync("git", ["worktree", "add", target], { cwd: source });
|
|
295
294
|
|
|
296
295
|
const cancelMock = makeCtx(target, { confirmAnswer: false });
|
|
297
|
-
await resumePlansCommand(
|
|
296
|
+
await resumePlansCommand(ctxAdapter(cancelMock) as never, BASE_DIR);
|
|
298
297
|
assert.equal(cancelMock.userMessages.length, 0);
|
|
299
298
|
assert.ok(cancelMock.confirmShown.length >= 1);
|
|
300
299
|
|
|
301
300
|
const goMock = makeCtx(target, { confirmAnswer: true });
|
|
302
|
-
await resumePlansCommand(
|
|
301
|
+
await resumePlansCommand(ctxAdapter(goMock) as never, BASE_DIR);
|
|
303
302
|
assert.equal(goMock.userMessages.length, 1, "one kickoff after migration");
|
|
304
303
|
// Artifacts copied into the target worktree's artifact root.
|
|
305
304
|
const copied = path.join(target, "docs", "pi-plans", path.basename(run.artifact_dir));
|
|
@@ -323,7 +322,7 @@ describe("/resume-plans command", () => {
|
|
|
323
322
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
324
323
|
fs.writeFileSync(path.join(run.artifact_dir, "PLAN_v1.md"), "# plan", "utf8");
|
|
325
324
|
const mock = makeCtx(workdir, { confirmAnswer: true });
|
|
326
|
-
await resumePlansCommand(
|
|
325
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
327
326
|
assert.equal(mock.userMessages.length, 1);
|
|
328
327
|
assert.match(mock.userMessages[0]!, /re-run the execution handoff/);
|
|
329
328
|
assert.ok(mock.confirmShown.some((c) => /Legacy execution run/.test(c.title)));
|
|
@@ -331,6 +330,7 @@ describe("/resume-plans command", () => {
|
|
|
331
330
|
|
|
332
331
|
it("migrateRunIntoCurrentWorktree aborts on differing conflicts (F-003)", () => {
|
|
333
332
|
const source = setupRepo("mig-overwrite", { commit: true });
|
|
333
|
+
setArtifactRoot(source, "./docs/pi-plans", "user");
|
|
334
334
|
const run = startRun(source, { topic: "mig", skill: "plan-normal", requestText: "a" }).run;
|
|
335
335
|
createCheckpoint(source, { runId: run.run_id, originWorkdir: source, workdir: source });
|
|
336
336
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
@@ -376,7 +376,7 @@ describe("F-004 ledger reconcile (crash window)", () => {
|
|
|
376
376
|
questionId: "q-x",
|
|
377
377
|
});
|
|
378
378
|
const mock = makeCtx(workdir);
|
|
379
|
-
await resumePlansCommand(
|
|
379
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
380
380
|
assert.equal(mock.userMessages.length, 1);
|
|
381
381
|
const brief = mock.userMessages[0]!;
|
|
382
382
|
assert.doesNotMatch(brief, /PENDING question/, "answered ledger entry wins");
|
|
@@ -386,58 +386,10 @@ describe("F-004 ledger reconcile (crash window)", () => {
|
|
|
386
386
|
});
|
|
387
387
|
});
|
|
388
388
|
|
|
389
|
-
describe("impl-review
|
|
390
|
-
it("
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
{ questionId: "termination-condition", answer: "1 round" },
|
|
394
|
-
{ questionId: "impl-review-reviewer-count", answer: "3" },
|
|
395
|
-
];
|
|
396
|
-
const resolved = resolveImplReviewConfig({ completedRounds: 0 }, ledger);
|
|
397
|
-
assert.equal(resolved.condition, "1 round");
|
|
398
|
-
assert.ok(resolved.conditionFromLedger);
|
|
399
|
-
assert.equal(resolved.reviewerCount, 3);
|
|
400
|
-
assert.ok(resolved.reviewerCountFromLedger);
|
|
401
|
-
assert.deepEqual(resolved.missing, []);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("only termination answered: reviewer-count is the single missing question", async () => {
|
|
405
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
406
|
-
const resolved = resolveImplReviewConfig({ completedRounds: 0 }, [
|
|
407
|
-
{ questionId: "termination-condition", answer: "goal wait" },
|
|
408
|
-
]);
|
|
409
|
-
assert.equal(resolved.condition, "goal wait");
|
|
410
|
-
assert.equal(resolved.reviewerCount, undefined);
|
|
411
|
-
assert.deepEqual(resolved.missing, ["impl-review-reviewer-count"]);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("persisted checkpoint wins over the ledger; latest ledger entry wins", async () => {
|
|
415
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
416
|
-
const resolved = resolveImplReviewConfig(
|
|
417
|
-
{ terminationCondition: "1 round", reviewerCount: 2, completedRounds: 0 },
|
|
418
|
-
[
|
|
419
|
-
{ questionId: "termination-condition", answer: "goal wait" },
|
|
420
|
-
{ questionId: "impl-review-reviewer-count", answer: "1" },
|
|
421
|
-
],
|
|
422
|
-
);
|
|
423
|
-
assert.equal(resolved.condition, "1 round");
|
|
424
|
-
assert.ok(!resolved.conditionFromLedger);
|
|
425
|
-
assert.equal(resolved.reviewerCount, 2);
|
|
426
|
-
assert.deepEqual(resolved.missing, []);
|
|
427
|
-
const staleThenFresh = resolveImplReviewConfig({ completedRounds: 0 }, [
|
|
428
|
-
{ questionId: "termination-condition", answer: "old" },
|
|
429
|
-
{ questionId: "termination-condition", answer: "new" },
|
|
430
|
-
]);
|
|
431
|
-
assert.equal(staleThenFresh.condition, "new", "latest ledger entry wins");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("non-integer or out-of-range ledger counts are ignored, not adopted", async () => {
|
|
435
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
436
|
-
for (const bad of ["0", "4", "three"]) {
|
|
437
|
-
const resolved = resolveImplReviewConfig({ terminationCondition: "1 round", completedRounds: 0 }, [
|
|
438
|
-
{ questionId: "impl-review-reviewer-count", answer: bad },
|
|
439
|
-
]);
|
|
440
|
-
assert.equal(resolved.reviewerCount, undefined, `ledger answer ${bad} ignored`);
|
|
441
|
-
}
|
|
389
|
+
describe("impl-review crash-window recovery (removed in v0.6.1)", () => {
|
|
390
|
+
it("the resolver is gone with the loop: the helper no longer exists", async () => {
|
|
391
|
+
const mod = await import("../src/resume-command.ts");
|
|
392
|
+
assert.equal("resolveImplReviewConfig" in mod, false);
|
|
442
393
|
});
|
|
443
394
|
});
|
|
395
|
+
|