pi-plans 0.5.7 → 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 +126 -0
- package/README.md +49 -39
- package/agents/ref-analyst.md +7 -4
- package/agents/reviewer.md +12 -3
- package/index.ts +74 -40
- package/package.json +2 -1
- package/references/pi-planning-workflow.md +45 -58
- package/references/plan-artifact-template.md +71 -60
- package/references/state-and-config.md +63 -47
- 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 +22 -10
- package/skills/debug-and-plan/SKILL.md +4 -4
- package/skills/plan-big/SKILL.md +5 -5
- package/skills/plan-normal/SKILL.md +5 -5
- package/skills/plan-small/SKILL.md +5 -5
- package/skills/plan-with-refs/SKILL.md +8 -8
- 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 +154 -76
- package/src/dashboard.ts +257 -0
- package/src/exec.ts +709 -705
- package/src/global-state.ts +304 -0
- package/src/guard.ts +16 -3
- package/src/messaging.ts +44 -0
- package/src/plan.ts +421 -112
- package/src/query-hook.ts +4 -4
- package/src/refine-prompts.ts +14 -72
- 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 +40 -130
- package/src/resume.ts +15 -17
- package/src/role-panels.ts +542 -0
- package/src/run-context.ts +5 -4
- package/src/run-picker.ts +98 -0
- package/src/state.ts +380 -77
- package/src/subagent.ts +32 -1
- 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 +78 -57
- package/tests/analyze-refs.test.ts +35 -18
- package/tests/ask-choice-pros-cons.test.ts +147 -0
- 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 +184 -0
- 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 +43 -88
- 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 +19 -49
- package/tools/code-graph.ts +2 -2
- package/tools/execute-plan.ts +63 -33
- package/tools/graph-aware-file-tools.ts +6 -4
- package/tools/plans.ts +40 -66
- package/tools/refine.ts +101 -164
- package/agents/criticizer.md +0 -18
- 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, "..");
|
|
@@ -113,7 +109,7 @@ after(() => {
|
|
|
113
109
|
});
|
|
114
110
|
|
|
115
111
|
describe("candidate discovery", () => {
|
|
116
|
-
it("lists resumable runs, excludes terminal ones,
|
|
112
|
+
it("lists resumable runs, excludes terminal ones, registry hint first", () => {
|
|
117
113
|
const workdir = setupRepo("discovery");
|
|
118
114
|
const planning = startRun(workdir, { topic: "alpha", skill: "plan-normal", requestText: "a" }).run;
|
|
119
115
|
const stopped = startRun(workdir, { topic: "beta", skill: "plan-normal", requestText: "b" }).run;
|
|
@@ -136,21 +132,24 @@ describe("candidate discovery", () => {
|
|
|
136
132
|
assert.equal(ids.includes(abandoned.run_id), false);
|
|
137
133
|
assert.equal(ids.includes(done.run_id), false, "plain done excluded");
|
|
138
134
|
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
// v0.6.0: the active-pointer auto-win is gone. The registry hint (newest
|
|
136
|
+
// non-terminal run) still sorts first; picking defaults to null when
|
|
137
|
+
// several candidates exist — the command opens the form (binding-first).
|
|
138
|
+
assert.equal(candidates[0]!.runId, stopped.run_id, "registry hint sorts first");
|
|
139
|
+
assert.equal(pickDefaultCandidate(workdir, candidates), null, "multi-candidate requires choosing");
|
|
142
140
|
});
|
|
143
141
|
|
|
144
|
-
it("unique candidate auto-picks;
|
|
142
|
+
it("unique candidate auto-picks; ambiguity requires choosing (v0.6.0)", () => {
|
|
145
143
|
const workdir = setupRepo("picking");
|
|
146
144
|
const only = startRun(workdir, { topic: "only", skill: "plan-normal", requestText: "a" }).run;
|
|
147
145
|
const candidates = listResumeCandidates(workdir);
|
|
148
146
|
assert.equal(pickDefaultCandidate(workdir, candidates)?.runId, only.run_id);
|
|
149
147
|
|
|
150
|
-
// D-
|
|
148
|
+
// v0.6.0 (D-1): no auto-win — two resumable runs are ambiguous here; the
|
|
149
|
+
// command's binding-first path handles the session-bound case.
|
|
151
150
|
const second = startRun(workdir, { topic: "second", skill: "plan-normal", requestText: "b" }).run;
|
|
152
151
|
const withActive = listResumeCandidates(workdir);
|
|
153
|
-
assert.equal(pickDefaultCandidate(workdir, withActive)
|
|
152
|
+
assert.equal(pickDefaultCandidate(workdir, withActive), null, "two candidates require choosing");
|
|
154
153
|
|
|
155
154
|
// Ambiguity: two resumable runs, active pointer names a non-resumable one.
|
|
156
155
|
const third = startRun(workdir, { topic: "third", skill: "plan-normal", requestText: "c" }).run;
|
|
@@ -165,7 +164,7 @@ describe("candidate discovery", () => {
|
|
|
165
164
|
const run = startRun(workdir, { topic: "corrupt", skill: "plan-normal", requestText: "a" }).run;
|
|
166
165
|
createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
|
|
167
166
|
fs.writeFileSync(
|
|
168
|
-
path.join(workdir, ".git", "
|
|
167
|
+
path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "checkpoint.json"),
|
|
169
168
|
"{ broken",
|
|
170
169
|
"utf8",
|
|
171
170
|
);
|
|
@@ -179,27 +178,27 @@ describe("/resume-plans command", () => {
|
|
|
179
178
|
it("refuses non-interactive and busy sessions, notifies when nothing is resumable", async () => {
|
|
180
179
|
const workdir = setupRepo("guards");
|
|
181
180
|
const noUI = makeCtx(workdir, { hasUI: false });
|
|
182
|
-
await resumePlansCommand(
|
|
181
|
+
await resumePlansCommand(ctxAdapter(noUI) as never, BASE_DIR);
|
|
183
182
|
assert.ok(noUI.notifies.some((n) => /interactive/.test(n.message)));
|
|
184
183
|
|
|
185
184
|
const busy = makeCtx(workdir, { isIdleResult: false });
|
|
186
|
-
await resumePlansCommand(
|
|
185
|
+
await resumePlansCommand(ctxAdapter(busy) as never, BASE_DIR);
|
|
187
186
|
assert.ok(busy.notifies.some((n) => /busy/.test(n.message)));
|
|
188
187
|
assert.equal(busy.userMessages.length, 0);
|
|
189
188
|
|
|
190
189
|
const empty = setupRepo("empty-repo");
|
|
191
190
|
const emptyCtx = makeCtx(empty);
|
|
192
|
-
await resumePlansCommand(
|
|
191
|
+
await resumePlansCommand(ctxAdapter(emptyCtx) as never, BASE_DIR);
|
|
193
192
|
assert.ok(emptyCtx.notifies.some((n) => /No resumable/.test(n.message)));
|
|
194
193
|
});
|
|
195
194
|
|
|
196
195
|
it("corrupt checkpoints report and change nothing", async () => {
|
|
197
196
|
const workdir = setupRepo("corrupt-cmd");
|
|
198
197
|
const run = startRun(workdir, { topic: "corruptcmd", skill: "plan-normal", requestText: "a" }).run;
|
|
199
|
-
const cpPath = path.join(workdir, ".git", "
|
|
198
|
+
const cpPath = path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "checkpoint.json");
|
|
200
199
|
fs.writeFileSync(cpPath, "{ broken", "utf8");
|
|
201
200
|
const mock = makeCtx(workdir);
|
|
202
|
-
await resumePlansCommand(
|
|
201
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
203
202
|
assert.ok(mock.notifies.some((n) => /corrupt/.test(n.message)));
|
|
204
203
|
assert.equal(mock.userMessages.length, 0);
|
|
205
204
|
assert.equal(fs.readFileSync(cpPath, "utf8"), "{ broken");
|
|
@@ -222,7 +221,7 @@ describe("/resume-plans command", () => {
|
|
|
222
221
|
void cp;
|
|
223
222
|
|
|
224
223
|
const mock = makeCtx(workdir);
|
|
225
|
-
await resumePlansCommand(
|
|
224
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
226
225
|
assert.equal(mock.userMessages.length, 1, "exactly one kickoff");
|
|
227
226
|
const brief = mock.userMessages[0]!;
|
|
228
227
|
assert.match(brief, /PI-PLANS RESUME/);
|
|
@@ -241,7 +240,7 @@ describe("/resume-plans command", () => {
|
|
|
241
240
|
const latest = startRun(workdir, { topic: "three", skill: "plan-normal", requestText: "c" }).run;
|
|
242
241
|
setRunStatus(workdir, latest.run_id, "abandoned");
|
|
243
242
|
const mock = makeCtx(workdir, { selectAnswer: null });
|
|
244
|
-
await resumePlansCommand(
|
|
243
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
245
244
|
assert.equal(mock.userMessages.length, 0);
|
|
246
245
|
assert.ok(mock.notifies.some((n) => /Cancelled/.test(n.message)));
|
|
247
246
|
assert.equal(mock.selects.length, 1);
|
|
@@ -254,7 +253,7 @@ describe("/resume-plans command", () => {
|
|
|
254
253
|
const child = spawn("sleep", ["30"], { stdio: "ignore" });
|
|
255
254
|
try {
|
|
256
255
|
fs.writeFileSync(
|
|
257
|
-
path.join(workdir, ".git", "
|
|
256
|
+
path.join(workdir, ".git", "pi-plans", "runs", run.run_id, "owner.json"),
|
|
258
257
|
JSON.stringify({
|
|
259
258
|
schema: 1,
|
|
260
259
|
host: os.hostname(),
|
|
@@ -268,7 +267,7 @@ describe("/resume-plans command", () => {
|
|
|
268
267
|
"utf8",
|
|
269
268
|
);
|
|
270
269
|
const mock = makeCtx(workdir);
|
|
271
|
-
await resumePlansCommand(
|
|
270
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
272
271
|
assert.ok(mock.notifies.some((n) => /actively owned/.test(n.message)));
|
|
273
272
|
assert.equal(mock.userMessages.length, 0);
|
|
274
273
|
} finally {
|
|
@@ -277,8 +276,11 @@ describe("/resume-plans command", () => {
|
|
|
277
276
|
});
|
|
278
277
|
|
|
279
278
|
it("cross-worktree: cancel changes nothing; confirm migrates artifacts and resets approval", async () => {
|
|
280
|
-
// 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.
|
|
281
282
|
const source = setupRepo("xwt-source", { commit: true });
|
|
283
|
+
setArtifactRoot(source, "./docs/pi-plans", "user");
|
|
282
284
|
const run = startRun(source, { topic: "xwt", skill: "plan-normal", requestText: "a" }).run;
|
|
283
285
|
const cp = createCheckpoint(source, { runId: run.run_id, originWorkdir: source, workdir: source });
|
|
284
286
|
void cp;
|
|
@@ -291,12 +293,12 @@ describe("/resume-plans command", () => {
|
|
|
291
293
|
spawnSync("git", ["worktree", "add", target], { cwd: source });
|
|
292
294
|
|
|
293
295
|
const cancelMock = makeCtx(target, { confirmAnswer: false });
|
|
294
|
-
await resumePlansCommand(
|
|
296
|
+
await resumePlansCommand(ctxAdapter(cancelMock) as never, BASE_DIR);
|
|
295
297
|
assert.equal(cancelMock.userMessages.length, 0);
|
|
296
298
|
assert.ok(cancelMock.confirmShown.length >= 1);
|
|
297
299
|
|
|
298
300
|
const goMock = makeCtx(target, { confirmAnswer: true });
|
|
299
|
-
await resumePlansCommand(
|
|
301
|
+
await resumePlansCommand(ctxAdapter(goMock) as never, BASE_DIR);
|
|
300
302
|
assert.equal(goMock.userMessages.length, 1, "one kickoff after migration");
|
|
301
303
|
// Artifacts copied into the target worktree's artifact root.
|
|
302
304
|
const copied = path.join(target, "docs", "pi-plans", path.basename(run.artifact_dir));
|
|
@@ -320,7 +322,7 @@ describe("/resume-plans command", () => {
|
|
|
320
322
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
321
323
|
fs.writeFileSync(path.join(run.artifact_dir, "PLAN_v1.md"), "# plan", "utf8");
|
|
322
324
|
const mock = makeCtx(workdir, { confirmAnswer: true });
|
|
323
|
-
await resumePlansCommand(
|
|
325
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
324
326
|
assert.equal(mock.userMessages.length, 1);
|
|
325
327
|
assert.match(mock.userMessages[0]!, /re-run the execution handoff/);
|
|
326
328
|
assert.ok(mock.confirmShown.some((c) => /Legacy execution run/.test(c.title)));
|
|
@@ -328,6 +330,7 @@ describe("/resume-plans command", () => {
|
|
|
328
330
|
|
|
329
331
|
it("migrateRunIntoCurrentWorktree aborts on differing conflicts (F-003)", () => {
|
|
330
332
|
const source = setupRepo("mig-overwrite", { commit: true });
|
|
333
|
+
setArtifactRoot(source, "./docs/pi-plans", "user");
|
|
331
334
|
const run = startRun(source, { topic: "mig", skill: "plan-normal", requestText: "a" }).run;
|
|
332
335
|
createCheckpoint(source, { runId: run.run_id, originWorkdir: source, workdir: source });
|
|
333
336
|
fs.mkdirSync(run.artifact_dir, { recursive: true });
|
|
@@ -373,7 +376,7 @@ describe("F-004 ledger reconcile (crash window)", () => {
|
|
|
373
376
|
questionId: "q-x",
|
|
374
377
|
});
|
|
375
378
|
const mock = makeCtx(workdir);
|
|
376
|
-
await resumePlansCommand(
|
|
379
|
+
await resumePlansCommand(ctxAdapter(mock) as never, BASE_DIR);
|
|
377
380
|
assert.equal(mock.userMessages.length, 1);
|
|
378
381
|
const brief = mock.userMessages[0]!;
|
|
379
382
|
assert.doesNotMatch(brief, /PENDING question/, "answered ledger entry wins");
|
|
@@ -383,58 +386,10 @@ describe("F-004 ledger reconcile (crash window)", () => {
|
|
|
383
386
|
});
|
|
384
387
|
});
|
|
385
388
|
|
|
386
|
-
describe("impl-review
|
|
387
|
-
it("
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
{ questionId: "termination-condition", answer: "1 round" },
|
|
391
|
-
{ questionId: "impl-review-reviewer-count", answer: "3" },
|
|
392
|
-
];
|
|
393
|
-
const resolved = resolveImplReviewConfig({ completedRounds: 0 }, ledger);
|
|
394
|
-
assert.equal(resolved.condition, "1 round");
|
|
395
|
-
assert.ok(resolved.conditionFromLedger);
|
|
396
|
-
assert.equal(resolved.reviewerCount, 3);
|
|
397
|
-
assert.ok(resolved.reviewerCountFromLedger);
|
|
398
|
-
assert.deepEqual(resolved.missing, []);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it("only termination answered: reviewer-count is the single missing question", async () => {
|
|
402
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
403
|
-
const resolved = resolveImplReviewConfig({ completedRounds: 0 }, [
|
|
404
|
-
{ questionId: "termination-condition", answer: "goal wait" },
|
|
405
|
-
]);
|
|
406
|
-
assert.equal(resolved.condition, "goal wait");
|
|
407
|
-
assert.equal(resolved.reviewerCount, undefined);
|
|
408
|
-
assert.deepEqual(resolved.missing, ["impl-review-reviewer-count"]);
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
it("persisted checkpoint wins over the ledger; latest ledger entry wins", async () => {
|
|
412
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
413
|
-
const resolved = resolveImplReviewConfig(
|
|
414
|
-
{ terminationCondition: "1 round", reviewerCount: 2, completedRounds: 0 },
|
|
415
|
-
[
|
|
416
|
-
{ questionId: "termination-condition", answer: "goal wait" },
|
|
417
|
-
{ questionId: "impl-review-reviewer-count", answer: "1" },
|
|
418
|
-
],
|
|
419
|
-
);
|
|
420
|
-
assert.equal(resolved.condition, "1 round");
|
|
421
|
-
assert.ok(!resolved.conditionFromLedger);
|
|
422
|
-
assert.equal(resolved.reviewerCount, 2);
|
|
423
|
-
assert.deepEqual(resolved.missing, []);
|
|
424
|
-
const staleThenFresh = resolveImplReviewConfig({ completedRounds: 0 }, [
|
|
425
|
-
{ questionId: "termination-condition", answer: "old" },
|
|
426
|
-
{ questionId: "termination-condition", answer: "new" },
|
|
427
|
-
]);
|
|
428
|
-
assert.equal(staleThenFresh.condition, "new", "latest ledger entry wins");
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("non-integer or out-of-range ledger counts are ignored, not adopted", async () => {
|
|
432
|
-
const { resolveImplReviewConfig } = await import("../src/resume-command.ts");
|
|
433
|
-
for (const bad of ["0", "4", "three"]) {
|
|
434
|
-
const resolved = resolveImplReviewConfig({ terminationCondition: "1 round", completedRounds: 0 }, [
|
|
435
|
-
{ questionId: "impl-review-reviewer-count", answer: bad },
|
|
436
|
-
]);
|
|
437
|
-
assert.equal(resolved.reviewerCount, undefined, `ledger answer ${bad} ignored`);
|
|
438
|
-
}
|
|
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);
|
|
439
393
|
});
|
|
440
394
|
});
|
|
395
|
+
|