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
|
@@ -1,38 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* rebuilds after restoreFromSession.
|
|
2
|
+
* Task dashboard lifecycle (v0.6.1): the aboveEditor widget registers when
|
|
3
|
+
* execution starts, reflects task-tool updates, renders the expanded tree on
|
|
4
|
+
* toggle, unregisters on stop/complete with the status line cleared, and
|
|
5
|
+
* rebuilds after restoreFromSession.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import * as assert from "node:assert/strict";
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
13
12
|
import { after, before, describe, it } from "node:test";
|
|
14
|
-
import type {
|
|
13
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
14
|
import {
|
|
16
15
|
executionContextMessage,
|
|
17
|
-
|
|
16
|
+
getExecution,
|
|
17
|
+
isDashboardExpanded,
|
|
18
|
+
persistTaskProgress,
|
|
18
19
|
restoreFromSession,
|
|
19
20
|
startExecution,
|
|
20
21
|
stopExecution,
|
|
22
|
+
toggleDashboardExpanded,
|
|
21
23
|
updateStatusWidget,
|
|
22
24
|
} from "../src/exec.ts";
|
|
25
|
+
import { applyTaskUpdate } from "../src/task-tool.ts";
|
|
26
|
+
import { parseChecklist, parsePlanTasks } from "../src/plan.ts";
|
|
23
27
|
import { initState, startRun } from "../src/state.ts";
|
|
28
|
+
import { setMessagingApi } from "../src/messaging.ts";
|
|
24
29
|
|
|
25
30
|
let root: string;
|
|
26
31
|
let counter = 0;
|
|
27
32
|
|
|
28
33
|
before(() => {
|
|
29
|
-
root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-
|
|
34
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-plans-dash-life-"));
|
|
30
35
|
});
|
|
31
36
|
|
|
32
37
|
after(() => {
|
|
33
38
|
fs.rmSync(root, { recursive: true, force: true });
|
|
34
39
|
});
|
|
35
40
|
|
|
41
|
+
const PLAN = `## Tasks
|
|
42
|
+
|
|
43
|
+
- Task-1: parser — files: src/a.ts; wave: 1
|
|
44
|
+
- Task-2: tool — files: src/b.ts; wave: 2
|
|
45
|
+
|
|
46
|
+
### Execution Waves
|
|
47
|
+
|
|
48
|
+
- wave 1: Task-1 — first
|
|
49
|
+
- wave 2: Task-2 — second
|
|
50
|
+
|
|
51
|
+
## Verification Checks
|
|
52
|
+
|
|
53
|
+
- [ ] \`VC-001\` covers \`Task-1\`; pass condition: parser
|
|
54
|
+
- [ ] \`VC-002\` covers \`Task-2\`; pass condition: tool
|
|
55
|
+
`;
|
|
56
|
+
|
|
36
57
|
function freshWorkdir(): string {
|
|
37
58
|
counter += 1;
|
|
38
59
|
const workdir = path.join(root, `repo-${counter}`);
|
|
@@ -49,55 +70,38 @@ interface PanelCall {
|
|
|
49
70
|
|
|
50
71
|
function makeHarness(workdir: string) {
|
|
51
72
|
const recorded: {
|
|
52
|
-
status: string | undefined;
|
|
53
|
-
statusCalls: number;
|
|
54
73
|
widgets: PanelCall[];
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} = {
|
|
59
|
-
const handlers = new Map<string, Array<(event: unknown, ctx: unknown) => unknown>>();
|
|
60
|
-
const pi = {
|
|
61
|
-
on: (name: string, handler: (event: unknown, ctx: unknown) => unknown) => {
|
|
62
|
-
const list = handlers.get(name) ?? [];
|
|
63
|
-
list.push(handler);
|
|
64
|
-
handlers.set(name, list);
|
|
65
|
-
},
|
|
66
|
-
registerTool: () => {},
|
|
67
|
-
registerCommand: () => {},
|
|
68
|
-
appendEntry: (customType: string, data: unknown) => {
|
|
69
|
-
recorded.entries.push({ customType, data });
|
|
70
|
-
},
|
|
71
|
-
sendMessage: (message: { customType: string }) => {
|
|
72
|
-
recorded.messages.push({ customType: message.customType });
|
|
73
|
-
},
|
|
74
|
-
} as unknown as ExtensionAPI;
|
|
74
|
+
status: string | undefined;
|
|
75
|
+
entries: Array<{ customType: string; data?: unknown; content?: string }>;
|
|
76
|
+
messages: string[];
|
|
77
|
+
} = { widgets: [], status: undefined, entries: [], messages: [] };
|
|
75
78
|
const ctx = {
|
|
76
79
|
cwd: workdir,
|
|
77
|
-
hasUI: true,
|
|
78
80
|
sessionManager: {},
|
|
81
|
+
hasUI: true,
|
|
82
|
+
mode: "print" as const,
|
|
79
83
|
ui: {
|
|
80
|
-
setStatus: (_key: string,
|
|
81
|
-
recorded.status =
|
|
82
|
-
recorded.statusCalls += 1;
|
|
84
|
+
setStatus: (_key: string, text: string | undefined) => {
|
|
85
|
+
recorded.status = text;
|
|
83
86
|
},
|
|
84
87
|
setWidget: (key: string, content: unknown, options?: { placement?: string }) => {
|
|
88
|
+
if (content === undefined) {
|
|
89
|
+
recorded.widgets.push({ key, content: undefined });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
85
92
|
recorded.widgets.push({ key, content, placement: options?.placement });
|
|
86
93
|
},
|
|
87
|
-
|
|
88
|
-
recorded.notifies.push({ message });
|
|
89
|
-
},
|
|
90
|
-
theme: { fg: (_c: string, s: string) => s },
|
|
94
|
+
theme: { fg: (_c: string, t: string) => t, bold: (t: string) => t },
|
|
91
95
|
},
|
|
96
|
+
isIdle: () => true,
|
|
97
|
+
hasPendingMessages: () => false,
|
|
92
98
|
} as unknown as ExtensionContext;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function items(...ids: string[]) {
|
|
100
|
-
return ids.map((id) => ({ id, text: `\`${id}\` demo item`, done: false }));
|
|
99
|
+
setMessagingApi({
|
|
100
|
+
appendEntry: (customType: string, data: unknown) => recorded.entries.push({ customType, data }),
|
|
101
|
+
sendMessage: (message: { customType: string; content: string }) => recorded.messages.push(message.content),
|
|
102
|
+
sendUserMessage: async () => {},
|
|
103
|
+
});
|
|
104
|
+
return { ctx, recorded };
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
function lastWidget(h: ReturnType<typeof makeHarness>): PanelCall | undefined {
|
|
@@ -110,238 +114,89 @@ function renderWidget(call: PanelCall | undefined, width: number): string[] {
|
|
|
110
114
|
return factory({ theme: { fg: (_c: string, s: string) => s } }, { fg: (_c: string, s: string) => s }).render(width);
|
|
111
115
|
}
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
async function start(h: ReturnType<typeof makeHarness>, workdir: string, name = "PLAN_v1.md") {
|
|
118
|
+
const planPath = path.join(workdir, name);
|
|
119
|
+
fs.writeFileSync(planPath, PLAN, "utf8");
|
|
120
|
+
await startExecution(h.ctx, { planPath, planTasks: parsePlanTasks(PLAN), items: parseChecklist(PLAN) });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe("dashboard lifecycle", () => {
|
|
124
|
+
it("registers the dashboard above the editor when execution starts", async () => {
|
|
115
125
|
const workdir = freshWorkdir();
|
|
116
126
|
const h = makeHarness(workdir);
|
|
117
|
-
await
|
|
118
|
-
{ id: "I-001", text: "First item." },
|
|
119
|
-
{ id: "I-002", text: "Second item." },
|
|
120
|
-
]);
|
|
127
|
+
await start(h, workdir);
|
|
121
128
|
const call = lastWidget(h);
|
|
122
129
|
assert.ok(call, "widget registered");
|
|
123
|
-
assert.equal(call!.key, "pi-plans");
|
|
130
|
+
assert.equal(call!.key, "pi-plans-dashboard");
|
|
124
131
|
assert.equal(call!.placement, "aboveEditor");
|
|
125
132
|
const lines = renderWidget(call, 100);
|
|
126
|
-
assert.
|
|
127
|
-
assert.
|
|
128
|
-
|
|
129
|
-
assert.match(h.recorded.status ?? "", /plans: .* ▸ exec/);
|
|
130
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
133
|
+
assert.ok(lines.join("\n").includes("Task-1"));
|
|
134
|
+
assert.match(h.recorded.status ?? "", /tasks 0\/2/);
|
|
135
|
+
await stopExecution(h.ctx, "done");
|
|
131
136
|
});
|
|
132
137
|
|
|
133
|
-
it("
|
|
138
|
+
it("task updates refresh the widget content and status line", async () => {
|
|
134
139
|
const workdir = freshWorkdir();
|
|
135
|
-
startRun(workdir, { topic: "live-activity", skill: "plan-small", requestText: "x" });
|
|
136
140
|
const h = makeHarness(workdir);
|
|
137
|
-
await
|
|
138
|
-
|
|
139
|
-
|
|
141
|
+
await start(h, workdir);
|
|
142
|
+
applyTaskUpdate(getExecution()!.tasks, "Task-1", "complete", "a-tests green");
|
|
143
|
+
persistTaskProgress(h.ctx);
|
|
144
|
+
updateStatusWidget(h.ctx);
|
|
145
|
+
assert.match(h.recorded.status ?? "", /tasks 1\/2/);
|
|
140
146
|
const lines = renderWidget(lastWidget(h), 100);
|
|
141
|
-
assert.
|
|
142
|
-
|
|
143
|
-
assert.ok((lines[6] ?? "").startsWith("╰"));
|
|
144
|
-
assert.ok(!lines.join("\n").includes("markers:"), "no duplicated legend rows");
|
|
145
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
147
|
+
assert.ok(lines.join("\n").includes("Task-2"), "current task advances");
|
|
148
|
+
await stopExecution(h.ctx, "done");
|
|
146
149
|
});
|
|
147
150
|
|
|
148
|
-
it("
|
|
151
|
+
it("toggle switches the widget to the expanded tree view", async () => {
|
|
149
152
|
const workdir = freshWorkdir();
|
|
150
|
-
startRun(workdir, { topic: "missing-activity", skill: "plan-small", requestText: "x" });
|
|
151
153
|
const h = makeHarness(workdir);
|
|
152
|
-
await
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
assert.ok(!(lines[5] ?? "").includes("since"), "no fabricated since-time without a run record");
|
|
163
|
-
assert.ok(!(lines[5] ?? "").includes("undefined"));
|
|
164
|
-
} finally {
|
|
165
|
-
fs.renameSync(stash, runsDir);
|
|
166
|
-
}
|
|
167
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
154
|
+
await start(h, workdir);
|
|
155
|
+
toggleDashboardExpanded(h.ctx);
|
|
156
|
+
assert.equal(isDashboardExpanded(), true);
|
|
157
|
+
const lines = renderWidget(lastWidget(h), 120);
|
|
158
|
+
assert.ok(lines.join("\n").includes("Verification checks:"), "tree view lists checks");
|
|
159
|
+
toggleDashboardExpanded(h.ctx);
|
|
160
|
+
assert.equal(isDashboardExpanded(), false);
|
|
161
|
+
const compact = renderWidget(lastWidget(h), 100);
|
|
162
|
+
assert.ok(!compact.join("\n").includes("Verification checks:"), "compact view returns");
|
|
163
|
+
await stopExecution(h.ctx, "done");
|
|
168
164
|
});
|
|
169
165
|
|
|
170
|
-
it("
|
|
166
|
+
it("clears the widget and status line on stop", async () => {
|
|
171
167
|
const workdir = freshWorkdir();
|
|
172
|
-
startRun(workdir, { topic: "first-run", skill: "plan-small", requestText: "x" });
|
|
173
168
|
const h = makeHarness(workdir);
|
|
174
|
-
await
|
|
175
|
-
|
|
176
|
-
]);
|
|
177
|
-
const before = renderWidget(lastWidget(h), 100);
|
|
178
|
-
assert.match(before[5] ?? "", /executing · since \d{2}-\d{2} \d{2}:\d{2}/, "activity resolves while the pointer agrees");
|
|
179
|
-
// A second run claims the shared disk pointer mid-execution, and the
|
|
180
|
-
// session is replaced (binding identity mismatch → resolve falls to the
|
|
181
|
-
// disk pointer): the activity row must degrade instead of mixing the
|
|
182
|
-
// new run's topic with the old run's status (impl-review r1 F-001).
|
|
183
|
-
startRun(workdir, { topic: "second-run", skill: "plan-small", requestText: "y" });
|
|
184
|
-
const prevSessionManager = (h.ctx as { sessionManager: unknown }).sessionManager;
|
|
185
|
-
(h.ctx as { sessionManager: unknown }).sessionManager = {};
|
|
186
|
-
try {
|
|
187
|
-
const after = renderWidget(lastWidget(h), 100);
|
|
188
|
-
assert.match(after[5] ?? "", /executing/);
|
|
189
|
-
assert.ok(!(after[5] ?? "").includes("since"), "no mixed-run status when the pointer moved");
|
|
190
|
-
} finally {
|
|
191
|
-
(h.ctx as { sessionManager: unknown }).sessionManager = prevSessionManager;
|
|
192
|
-
}
|
|
193
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("refreshes panel content from marker updates without re-registering", async () => {
|
|
197
|
-
const workdir = freshWorkdir();
|
|
198
|
-
const h = makeHarness(workdir);
|
|
199
|
-
registerExecutionTurnHandlers(h.pi);
|
|
200
|
-
await startExecution(h.pi, h.ctx, path.join(workdir, "PLAN_v2.md"), items("VC-001", "VC-002"), [
|
|
201
|
-
{ id: "I-001", text: "First item." },
|
|
202
|
-
{ id: "I-002", text: "Second item." },
|
|
203
|
-
]);
|
|
204
|
-
await h.emit("turn_end", {
|
|
205
|
-
message: { role: "assistant", content: [{ type: "text", text: "starting [I-002:current]" }] },
|
|
206
|
-
});
|
|
207
|
-
// The factory closure reads live state: render picks up the new focus.
|
|
208
|
-
const lines = renderWidget(lastWidget(h), 100);
|
|
209
|
-
assert.ok(lines.join("\n").includes("I-002"), "panel content follows the current-I marker");
|
|
210
|
-
assert.equal(h.recorded.widgets.length, 1, "no duplicate registrations across updates");
|
|
211
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("unregisters the panel and clears the execution summary on stop", async () => {
|
|
215
|
-
const workdir = freshWorkdir();
|
|
216
|
-
const h = makeHarness(workdir);
|
|
217
|
-
await startExecution(h.pi, h.ctx, path.join(workdir, "PLAN_v3.md"), items("VC-001"));
|
|
218
|
-
await stopExecution(h.pi, h.ctx, "test-stop");
|
|
169
|
+
await start(h, workdir);
|
|
170
|
+
await stopExecution(h.ctx, "user stop");
|
|
219
171
|
const call = lastWidget(h);
|
|
220
|
-
assert.ok(call
|
|
221
|
-
assert.equal(call!.key, "pi-plans");
|
|
222
|
-
assert.equal(call!.content, undefined, "widget cleared");
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it("rebuilds the panel from a session snapshot on restore", async () => {
|
|
226
|
-
const workdir = freshWorkdir();
|
|
227
|
-
startRun(workdir, { topic: "restore-topic", skill: "plan-small", requestText: "x" });
|
|
228
|
-
fs.writeFileSync(path.join(workdir, "PLAN_v9.md"), "# plan");
|
|
229
|
-
const h = makeHarness(workdir);
|
|
230
|
-
// A prior session persisted an exec snapshot; restore must rebind the UI.
|
|
231
|
-
await restoreFromSession(
|
|
232
|
-
h.pi,
|
|
233
|
-
h.ctx,
|
|
234
|
-
[
|
|
235
|
-
{ type: "custom", customType: "pi-plans-exec", data: {
|
|
236
|
-
planPath: path.join(workdir, "PLAN_v9.md"),
|
|
237
|
-
items: items("VC-001", "VC-002"),
|
|
238
|
-
implItems: [{ id: "I-001", text: "First." }],
|
|
239
|
-
startedAt: new Date().toISOString(),
|
|
240
|
-
usage: { inToks: 0, outToks: 0 },
|
|
241
|
-
} },
|
|
242
|
-
] as never,
|
|
243
|
-
);
|
|
244
|
-
const lines = renderWidget(lastWidget(h), 100);
|
|
245
|
-
assert.equal(lines.length, 7, "panel rebuilt after restore");
|
|
246
|
-
assert.match(h.recorded.status ?? "", /plans: restore-topic ▸ exec/);
|
|
247
|
-
assert.match(lines[5] ?? "", /· since \d{2}-\d{2} \d{2}:\d{2}/, "restored panel rebinds the activity row to run.json");
|
|
248
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
172
|
+
assert.ok(call && call.content === undefined, "widget unregistered");
|
|
249
173
|
});
|
|
250
174
|
|
|
251
|
-
it("
|
|
175
|
+
it("rebuilds the dashboard after restoreFromSession", async () => {
|
|
252
176
|
const workdir = freshWorkdir();
|
|
177
|
+
startRun(workdir, { topic: "restore", skill: "plan-small", requestText: "x" });
|
|
253
178
|
const h = makeHarness(workdir);
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
assert.ok(
|
|
265
|
-
|
|
266
|
-
assert.ok(panelLines.includes("Next: Implement I-002"), "panel shows the same action");
|
|
267
|
-
await stopExecution(h.pi, h.ctx, "done");
|
|
179
|
+
await start(h, workdir);
|
|
180
|
+
applyTaskUpdate(getExecution()!.tasks, "Task-1", "complete", "e1");
|
|
181
|
+
persistTaskProgress(h.ctx);
|
|
182
|
+
const snapshot = getExecution();
|
|
183
|
+
await stopExecution(h.ctx, "restart");
|
|
184
|
+
assert.equal(getExecution(), null);
|
|
185
|
+
const h2 = makeHarness(workdir);
|
|
186
|
+
await restoreFromSession(h2.ctx, [{ type: "custom", customType: "pi-plans-exec", data: snapshot }]);
|
|
187
|
+
assert.ok(getExecution(), "execution restored");
|
|
188
|
+
const lines = renderWidget(lastWidget(h2), 100);
|
|
189
|
+
assert.ok(lines.join("\n").includes("Task-2"), "restored view shows the open task");
|
|
190
|
+
await stopExecution(h2.ctx, "done");
|
|
268
191
|
});
|
|
269
192
|
|
|
270
|
-
it("
|
|
193
|
+
it("injection names the current wave and the task tool", async () => {
|
|
271
194
|
const workdir = freshWorkdir();
|
|
272
195
|
const h = makeHarness(workdir);
|
|
273
|
-
(h
|
|
274
|
-
|
|
275
|
-
assert.
|
|
276
|
-
assert.match(
|
|
277
|
-
|
|
278
|
-
});
|
|
279
|
-
describe("implementation-review loop widget (0.5.4, D-3/D-8)", () => {
|
|
280
|
-
it("stays alive after completion while the loop is live, then unregisters on completed", async () => {
|
|
281
|
-
const workdir = freshWorkdir();
|
|
282
|
-
const h = makeHarness(workdir);
|
|
283
|
-
// A previous test leaves a stray execution alive (module state); clear
|
|
284
|
-
// it so the loop widget's null-execution branch is reachable.
|
|
285
|
-
await stopExecution(h.pi, h.ctx, "cleanup-before-loop-test");
|
|
286
|
-
spawnSync("git", ["init"], { cwd: workdir });
|
|
287
|
-
const { startRun, setRunStatus } = await import("../src/state.ts");
|
|
288
|
-
const { run } = startRun(workdir, { topic: "loop-widget", skill: "plan-big", requestText: "t" });
|
|
289
|
-
const { createCheckpoint, mutateCheckpoint, applyImplementationReviewConfigured, applyCompleted } = await import(
|
|
290
|
-
"../src/workflow-state.ts"
|
|
291
|
-
);
|
|
292
|
-
createCheckpoint(workdir, { runId: run.run_id, originWorkdir: workdir, workdir });
|
|
293
|
-
mutateCheckpoint(workdir, run.run_id, (cp) =>
|
|
294
|
-
applyImplementationReviewConfigured(
|
|
295
|
-
{ ...cp, phase: "implementation-review", nextAction: "ask-question" },
|
|
296
|
-
"1 round",
|
|
297
|
-
3,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
setRunStatus(workdir, run.run_id, "done");
|
|
301
|
-
|
|
302
|
-
updateStatusWidget(h.ctx);
|
|
303
|
-
const call = lastWidget(h);
|
|
304
|
-
assert.ok(call && call.content !== undefined, "loop widget registered while the loop is live");
|
|
305
|
-
assert.equal(call!.key, "pi-plans");
|
|
306
|
-
const lines = renderWidget(call, 80);
|
|
307
|
-
assert.equal(lines.length, 4, "compact 4-line loop box");
|
|
308
|
-
assert.match(lines[1], /impl-review . round 1 . 3 reviewers/);
|
|
309
|
-
assert.match(lines[2], /until: 1 round/);
|
|
310
|
-
assert.match(h.recorded.status ?? "", /plans: impl-review round 1 . 3 reviewers/);
|
|
311
|
-
|
|
312
|
-
// Round progress: completedRounds bump renders on the next refresh
|
|
313
|
-
// (D-8 live reads).
|
|
314
|
-
const { applyReviewRoundStarted, applyLaneResult, writeReviewOutput, applyReviewConsolidated, applyImplementationRoundFinished } =
|
|
315
|
-
await import("../src/workflow-state.ts");
|
|
316
|
-
mutateCheckpoint(workdir, run.run_id, (cp) =>
|
|
317
|
-
applyReviewRoundStarted(cp, {
|
|
318
|
-
roundId: "impl-reviewer-r1",
|
|
319
|
-
role: "reviewer",
|
|
320
|
-
target: "implementation",
|
|
321
|
-
reviewers: 3,
|
|
322
|
-
lanes: [{ laneId: "l1" }, { laneId: "l2" }, { laneId: "l3" }],
|
|
323
|
-
}),
|
|
324
|
-
);
|
|
325
|
-
const loadNow = await import("../src/workflow-state.ts");
|
|
326
|
-
const loadedNow = loadNow.loadCheckpoint(workdir, run.run_id);
|
|
327
|
-
assert.ok(loadedNow.status === "ok");
|
|
328
|
-
let cpNow = loadedNow.checkpoint;
|
|
329
|
-
for (const lane of ["l1", "l2", "l3"]) {
|
|
330
|
-
const file = writeReviewOutput(workdir, run.run_id, "impl-reviewer-r1", lane, "out");
|
|
331
|
-
cpNow = applyLaneResult(cpNow, "impl-reviewer-r1", lane, { ok: true, resultFile: file });
|
|
332
|
-
}
|
|
333
|
-
mutateCheckpoint(workdir, run.run_id, () =>
|
|
334
|
-
applyImplementationRoundFinished(applyReviewConsolidated(cpNow, "impl-reviewer-r1")),
|
|
335
|
-
);
|
|
336
|
-
updateStatusWidget(h.ctx);
|
|
337
|
-
const refreshed = renderWidget(lastWidget(h), 80);
|
|
338
|
-
assert.match(refreshed[1], /round 2/, "live checkpoint reads advance the round");
|
|
339
|
-
|
|
340
|
-
// Completion unregisters the widget and restores the (done) line.
|
|
341
|
-
mutateCheckpoint(workdir, run.run_id, (cp) => applyCompleted({ ...cp, nextAction: "finish-review" }, "1 round disposed; termination condition met"));
|
|
342
|
-
updateStatusWidget(h.ctx);
|
|
343
|
-
const lastCall = lastWidget(h);
|
|
344
|
-
assert.ok(lastCall && lastCall.content === undefined, "widget unregistered after completion");
|
|
345
|
-
assert.match(h.recorded.status ?? "", /\(done\)/);
|
|
196
|
+
await start(h, workdir);
|
|
197
|
+
const injection = executionContextMessage(h.ctx)!;
|
|
198
|
+
assert.match(injection, /Current wave 1 open tasks:/);
|
|
199
|
+
assert.match(injection, /plans_update_task/);
|
|
200
|
+
await stopExecution(h.ctx, "done");
|
|
346
201
|
});
|
|
347
202
|
});
|