gsd-pi 2.67.0-dev.1cd1e0f → 2.67.0-dev.2142d3e
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/dist/resources/extensions/claude-code-cli/stream-adapter.js +3 -0
- package/dist/resources/extensions/gsd/auto/phases.js +17 -0
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +12 -0
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +11 -435
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +1 -4
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +7 -64
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +88 -8
- package/dist/resources/extensions/gsd/commands/handlers/core.js +38 -24
- package/dist/resources/extensions/gsd/commands/index.js +8 -1
- package/dist/resources/extensions/gsd/guided-flow.js +16 -0
- package/dist/resources/extensions/gsd/init-wizard.js +34 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +508 -0
- package/dist/resources/extensions/gsd/workflow-logger.js +18 -3
- package/dist/resources/extensions/gsd/workflow-mcp.js +190 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +38 -0
- package/packages/mcp-server/src/server.ts +6 -2
- package/packages/mcp-server/src/workflow-tools.test.ts +976 -0
- package/packages/mcp-server/src/workflow-tools.ts +986 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +3 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +121 -0
- package/src/resources/extensions/gsd/auto/phases.ts +25 -0
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +22 -435
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +1 -5
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +7 -72
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +122 -6
- package/src/resources/extensions/gsd/commands/handlers/core.ts +52 -25
- package/src/resources/extensions/gsd/commands/index.ts +7 -1
- package/src/resources/extensions/gsd/guided-flow.ts +24 -0
- package/src/resources/extensions/gsd/init-wizard.ts +34 -0
- package/src/resources/extensions/gsd/tests/core-overlay-fallback.test.ts +101 -0
- package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/init-bootstrap-completeness.test.ts +121 -0
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +16 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +301 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +625 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +629 -0
- package/src/resources/extensions/gsd/workflow-logger.ts +19 -3
- package/src/resources/extensions/gsd/workflow-mcp.ts +233 -0
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → xR6qurkuYSvyjBjRyJLxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{PHqEommYRR8CRn3i84CGM → xR6qurkuYSvyjBjRyJLxG}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { mkdirSync, rmSync, writeFileSync, existsSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
|
|
8
|
+
import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts";
|
|
9
|
+
import { registerWorkflowTools } from "./workflow-tools.ts";
|
|
10
|
+
|
|
11
|
+
function makeTmpBase(): string {
|
|
12
|
+
const base = join(tmpdir(), `gsd-mcp-workflow-${randomUUID()}`);
|
|
13
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
14
|
+
return base;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function cleanup(base: string): void {
|
|
18
|
+
try {
|
|
19
|
+
closeDatabase();
|
|
20
|
+
} catch {
|
|
21
|
+
// swallow
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
rmSync(base, { recursive: true, force: true });
|
|
25
|
+
} catch {
|
|
26
|
+
// swallow
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function writeWriteGateSnapshot(
|
|
31
|
+
base: string,
|
|
32
|
+
snapshot: { verifiedDepthMilestones?: string[]; activeQueuePhase?: boolean; pendingGateId?: string | null },
|
|
33
|
+
): void {
|
|
34
|
+
mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(base, ".gsd", "runtime", "write-gate-state.json"),
|
|
37
|
+
JSON.stringify(
|
|
38
|
+
{
|
|
39
|
+
verifiedDepthMilestones: snapshot.verifiedDepthMilestones ?? [],
|
|
40
|
+
activeQueuePhase: snapshot.activeQueuePhase ?? false,
|
|
41
|
+
pendingGateId: snapshot.pendingGateId ?? null,
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
2,
|
|
45
|
+
),
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeMockServer() {
|
|
51
|
+
const tools: Array<{
|
|
52
|
+
name: string;
|
|
53
|
+
description: string;
|
|
54
|
+
params: Record<string, unknown>;
|
|
55
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>;
|
|
56
|
+
}> = [];
|
|
57
|
+
return {
|
|
58
|
+
tools,
|
|
59
|
+
tool(
|
|
60
|
+
name: string,
|
|
61
|
+
description: string,
|
|
62
|
+
params: Record<string, unknown>,
|
|
63
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>,
|
|
64
|
+
) {
|
|
65
|
+
tools.push({ name, description, params, handler });
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe("workflow MCP tools", () => {
|
|
71
|
+
it("registers the seventeen workflow tools", () => {
|
|
72
|
+
const server = makeMockServer();
|
|
73
|
+
registerWorkflowTools(server as any);
|
|
74
|
+
|
|
75
|
+
assert.equal(server.tools.length, 17);
|
|
76
|
+
assert.deepEqual(
|
|
77
|
+
server.tools.map((t) => t.name),
|
|
78
|
+
[
|
|
79
|
+
"gsd_plan_milestone",
|
|
80
|
+
"gsd_plan_slice",
|
|
81
|
+
"gsd_replan_slice",
|
|
82
|
+
"gsd_slice_replan",
|
|
83
|
+
"gsd_slice_complete",
|
|
84
|
+
"gsd_complete_slice",
|
|
85
|
+
"gsd_complete_milestone",
|
|
86
|
+
"gsd_milestone_complete",
|
|
87
|
+
"gsd_validate_milestone",
|
|
88
|
+
"gsd_milestone_validate",
|
|
89
|
+
"gsd_reassess_roadmap",
|
|
90
|
+
"gsd_roadmap_reassess",
|
|
91
|
+
"gsd_save_gate_result",
|
|
92
|
+
"gsd_summary_save",
|
|
93
|
+
"gsd_task_complete",
|
|
94
|
+
"gsd_complete_task",
|
|
95
|
+
"gsd_milestone_status",
|
|
96
|
+
],
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("gsd_summary_save writes artifact through the shared executor", async () => {
|
|
101
|
+
const base = makeTmpBase();
|
|
102
|
+
try {
|
|
103
|
+
const server = makeMockServer();
|
|
104
|
+
registerWorkflowTools(server as any);
|
|
105
|
+
const tool = server.tools.find((t) => t.name === "gsd_summary_save");
|
|
106
|
+
assert.ok(tool, "summary tool should be registered");
|
|
107
|
+
const originalCwd = process.cwd();
|
|
108
|
+
|
|
109
|
+
const result = await tool!.handler({
|
|
110
|
+
projectDir: base,
|
|
111
|
+
milestone_id: "M001",
|
|
112
|
+
slice_id: "S01",
|
|
113
|
+
artifact_type: "SUMMARY",
|
|
114
|
+
content: "# Summary\n\nHello",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const text = (result as any).content[0].text as string;
|
|
118
|
+
assert.match(text, /Saved SUMMARY artifact/);
|
|
119
|
+
assert.equal(process.cwd(), originalCwd, "workflow MCP tools should not mutate process.cwd");
|
|
120
|
+
assert.ok(
|
|
121
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md")),
|
|
122
|
+
"summary file should exist on disk",
|
|
123
|
+
);
|
|
124
|
+
} finally {
|
|
125
|
+
cleanup(base);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("rejects workflow tool calls outside the configured project root", async () => {
|
|
130
|
+
const base = makeTmpBase();
|
|
131
|
+
const otherBase = makeTmpBase();
|
|
132
|
+
const prevRoot = process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
133
|
+
try {
|
|
134
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = base;
|
|
135
|
+
const server = makeMockServer();
|
|
136
|
+
registerWorkflowTools(server as any);
|
|
137
|
+
const tool = server.tools.find((t) => t.name === "gsd_summary_save");
|
|
138
|
+
assert.ok(tool, "summary tool should be registered");
|
|
139
|
+
|
|
140
|
+
await assert.rejects(
|
|
141
|
+
() =>
|
|
142
|
+
tool!.handler({
|
|
143
|
+
projectDir: otherBase,
|
|
144
|
+
milestone_id: "M001",
|
|
145
|
+
artifact_type: "SUMMARY",
|
|
146
|
+
content: "# Summary",
|
|
147
|
+
}),
|
|
148
|
+
/configured workflow project root/,
|
|
149
|
+
);
|
|
150
|
+
} finally {
|
|
151
|
+
if (prevRoot === undefined) {
|
|
152
|
+
delete process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
153
|
+
} else {
|
|
154
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = prevRoot;
|
|
155
|
+
}
|
|
156
|
+
cleanup(base);
|
|
157
|
+
cleanup(otherBase);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("rejects non-file executor module URLs", async () => {
|
|
162
|
+
const base = makeTmpBase();
|
|
163
|
+
const prevModule = process.env.GSD_WORKFLOW_EXECUTORS_MODULE;
|
|
164
|
+
const prevRoot = process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
165
|
+
try {
|
|
166
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = base;
|
|
167
|
+
process.env.GSD_WORKFLOW_EXECUTORS_MODULE = "data:text/javascript,export default {}";
|
|
168
|
+
const { registerWorkflowTools: freshRegisterWorkflowTools } = await import(`./workflow-tools.ts?bad-module=${randomUUID()}`);
|
|
169
|
+
const server = makeMockServer();
|
|
170
|
+
freshRegisterWorkflowTools(server as any);
|
|
171
|
+
const tool = server.tools.find((t) => t.name === "gsd_summary_save");
|
|
172
|
+
assert.ok(tool, "summary tool should be registered");
|
|
173
|
+
|
|
174
|
+
await assert.rejects(
|
|
175
|
+
() =>
|
|
176
|
+
tool!.handler({
|
|
177
|
+
projectDir: base,
|
|
178
|
+
milestone_id: "M001",
|
|
179
|
+
artifact_type: "SUMMARY",
|
|
180
|
+
content: "# Summary",
|
|
181
|
+
}),
|
|
182
|
+
/only supports file: URLs or filesystem paths/,
|
|
183
|
+
);
|
|
184
|
+
} finally {
|
|
185
|
+
if (prevModule === undefined) {
|
|
186
|
+
delete process.env.GSD_WORKFLOW_EXECUTORS_MODULE;
|
|
187
|
+
} else {
|
|
188
|
+
process.env.GSD_WORKFLOW_EXECUTORS_MODULE = prevModule;
|
|
189
|
+
}
|
|
190
|
+
if (prevRoot === undefined) {
|
|
191
|
+
delete process.env.GSD_WORKFLOW_PROJECT_ROOT;
|
|
192
|
+
} else {
|
|
193
|
+
process.env.GSD_WORKFLOW_PROJECT_ROOT = prevRoot;
|
|
194
|
+
}
|
|
195
|
+
cleanup(base);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("blocks workflow mutation tools while a discussion gate is pending", async () => {
|
|
200
|
+
const base = makeTmpBase();
|
|
201
|
+
try {
|
|
202
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
203
|
+
writeFileSync(
|
|
204
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
|
|
205
|
+
"# S01\n\n- [ ] **T01: Demo** `est:5m`\n",
|
|
206
|
+
);
|
|
207
|
+
writeWriteGateSnapshot(base, { pendingGateId: "depth_verification_M001_confirm" });
|
|
208
|
+
|
|
209
|
+
const server = makeMockServer();
|
|
210
|
+
registerWorkflowTools(server as any);
|
|
211
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
212
|
+
assert.ok(taskTool, "task tool should be registered");
|
|
213
|
+
|
|
214
|
+
await assert.rejects(
|
|
215
|
+
() =>
|
|
216
|
+
taskTool!.handler({
|
|
217
|
+
projectDir: base,
|
|
218
|
+
taskId: "T01",
|
|
219
|
+
sliceId: "S01",
|
|
220
|
+
milestoneId: "M001",
|
|
221
|
+
oneLiner: "Completed task",
|
|
222
|
+
narrative: "Did the work",
|
|
223
|
+
verification: "npm test",
|
|
224
|
+
}),
|
|
225
|
+
/Discussion gate .* has not been confirmed/,
|
|
226
|
+
);
|
|
227
|
+
} finally {
|
|
228
|
+
cleanup(base);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("blocks workflow mutation tools during queue mode", async () => {
|
|
233
|
+
const base = makeTmpBase();
|
|
234
|
+
try {
|
|
235
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
236
|
+
writeFileSync(
|
|
237
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
|
|
238
|
+
"# S01\n\n- [ ] **T01: Demo** `est:5m`\n",
|
|
239
|
+
);
|
|
240
|
+
writeWriteGateSnapshot(base, { activeQueuePhase: true });
|
|
241
|
+
|
|
242
|
+
const server = makeMockServer();
|
|
243
|
+
registerWorkflowTools(server as any);
|
|
244
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
245
|
+
assert.ok(taskTool, "task tool should be registered");
|
|
246
|
+
|
|
247
|
+
await assert.rejects(
|
|
248
|
+
() =>
|
|
249
|
+
taskTool!.handler({
|
|
250
|
+
projectDir: base,
|
|
251
|
+
taskId: "T01",
|
|
252
|
+
sliceId: "S01",
|
|
253
|
+
milestoneId: "M001",
|
|
254
|
+
oneLiner: "Completed task",
|
|
255
|
+
narrative: "Did the work",
|
|
256
|
+
verification: "npm test",
|
|
257
|
+
}),
|
|
258
|
+
/planning tool .* not executes work|Cannot gsd_task_complete|Unknown tools are not permitted during queue mode/,
|
|
259
|
+
);
|
|
260
|
+
} finally {
|
|
261
|
+
cleanup(base);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("gsd_task_complete and gsd_milestone_status work end-to-end", async () => {
|
|
266
|
+
const base = makeTmpBase();
|
|
267
|
+
try {
|
|
268
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
269
|
+
writeFileSync(
|
|
270
|
+
join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
|
|
271
|
+
"# S01\n\n- [ ] **T01: Demo** `est:5m`\n",
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
const server = makeMockServer();
|
|
275
|
+
registerWorkflowTools(server as any);
|
|
276
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
277
|
+
const statusTool = server.tools.find((t) => t.name === "gsd_milestone_status");
|
|
278
|
+
assert.ok(taskTool, "task tool should be registered");
|
|
279
|
+
assert.ok(statusTool, "status tool should be registered");
|
|
280
|
+
|
|
281
|
+
const taskResult = await taskTool!.handler({
|
|
282
|
+
projectDir: base,
|
|
283
|
+
taskId: "T01",
|
|
284
|
+
sliceId: "S01",
|
|
285
|
+
milestoneId: "M001",
|
|
286
|
+
oneLiner: "Completed task",
|
|
287
|
+
narrative: "Did the work",
|
|
288
|
+
verification: "npm test",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
assert.match((taskResult as any).content[0].text as string, /Completed task T01/);
|
|
292
|
+
assert.ok(
|
|
293
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md")),
|
|
294
|
+
"task summary should be written to disk",
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const statusResult = await statusTool!.handler({
|
|
298
|
+
projectDir: base,
|
|
299
|
+
milestoneId: "M001",
|
|
300
|
+
});
|
|
301
|
+
const parsed = JSON.parse((statusResult as any).content[0].text as string);
|
|
302
|
+
assert.equal(parsed.milestoneId, "M001");
|
|
303
|
+
assert.equal(parsed.sliceCount, 1);
|
|
304
|
+
assert.equal(parsed.slices[0].id, "S01");
|
|
305
|
+
} finally {
|
|
306
|
+
cleanup(base);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("gsd_complete_task alias delegates to gsd_task_complete behavior", async () => {
|
|
311
|
+
const base = makeTmpBase();
|
|
312
|
+
try {
|
|
313
|
+
mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S02"), { recursive: true });
|
|
314
|
+
writeFileSync(
|
|
315
|
+
join(base, ".gsd", "milestones", "M002", "slices", "S02", "S02-PLAN.md"),
|
|
316
|
+
"# S02\n\n- [ ] **T02: Demo** `est:5m`\n",
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const server = makeMockServer();
|
|
320
|
+
registerWorkflowTools(server as any);
|
|
321
|
+
const aliasTool = server.tools.find((t) => t.name === "gsd_complete_task");
|
|
322
|
+
assert.ok(aliasTool, "task completion alias should be registered");
|
|
323
|
+
|
|
324
|
+
const result = await aliasTool!.handler({
|
|
325
|
+
projectDir: base,
|
|
326
|
+
taskId: "T02",
|
|
327
|
+
sliceId: "S02",
|
|
328
|
+
milestoneId: "M002",
|
|
329
|
+
oneLiner: "Completed task via alias",
|
|
330
|
+
narrative: "Did the work through alias",
|
|
331
|
+
verification: "npm test",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
assert.match((result as any).content[0].text as string, /Completed task T02/);
|
|
335
|
+
assert.ok(
|
|
336
|
+
existsSync(join(base, ".gsd", "milestones", "M002", "slices", "S02", "tasks", "T02-SUMMARY.md")),
|
|
337
|
+
"alias should write task summary to disk",
|
|
338
|
+
);
|
|
339
|
+
} finally {
|
|
340
|
+
cleanup(base);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("gsd_plan_milestone and gsd_plan_slice work end-to-end", async () => {
|
|
345
|
+
const base = makeTmpBase();
|
|
346
|
+
try {
|
|
347
|
+
const server = makeMockServer();
|
|
348
|
+
registerWorkflowTools(server as any);
|
|
349
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
350
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
351
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
352
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
353
|
+
|
|
354
|
+
const milestoneResult = await milestoneTool!.handler({
|
|
355
|
+
projectDir: base,
|
|
356
|
+
milestoneId: "M001",
|
|
357
|
+
title: "Workflow MCP planning",
|
|
358
|
+
vision: "Plan milestone over MCP.",
|
|
359
|
+
slices: [
|
|
360
|
+
{
|
|
361
|
+
sliceId: "S01",
|
|
362
|
+
title: "Bridge planning",
|
|
363
|
+
risk: "medium",
|
|
364
|
+
depends: [],
|
|
365
|
+
demo: "Milestone plan persists through MCP.",
|
|
366
|
+
goal: "Persist roadmap state.",
|
|
367
|
+
successCriteria: "ROADMAP.md renders from DB.",
|
|
368
|
+
proofLevel: "integration",
|
|
369
|
+
integrationClosure: "Prompts and MCP call the same handler.",
|
|
370
|
+
observabilityImpact: "Executor tests cover output paths.",
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
assert.match((milestoneResult as any).content[0].text as string, /Planned milestone M001/);
|
|
375
|
+
|
|
376
|
+
const sliceResult = await sliceTool!.handler({
|
|
377
|
+
projectDir: base,
|
|
378
|
+
milestoneId: "M001",
|
|
379
|
+
sliceId: "S01",
|
|
380
|
+
goal: "Persist slice plan over MCP.",
|
|
381
|
+
tasks: [
|
|
382
|
+
{
|
|
383
|
+
taskId: "T01",
|
|
384
|
+
title: "Add planning bridge",
|
|
385
|
+
description: "Implement the shared executor path.",
|
|
386
|
+
estimate: "15m",
|
|
387
|
+
files: ["src/resources/extensions/gsd/tools/workflow-tool-executors.ts"],
|
|
388
|
+
verify: "node --test",
|
|
389
|
+
inputs: ["ROADMAP.md"],
|
|
390
|
+
expectedOutput: ["S01-PLAN.md", "T01-PLAN.md"],
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
});
|
|
394
|
+
assert.match((sliceResult as any).content[0].text as string, /Planned slice S01/);
|
|
395
|
+
assert.ok(
|
|
396
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md")),
|
|
397
|
+
"slice plan should exist on disk",
|
|
398
|
+
);
|
|
399
|
+
assert.ok(
|
|
400
|
+
existsSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-PLAN.md")),
|
|
401
|
+
"task plan should exist on disk",
|
|
402
|
+
);
|
|
403
|
+
} finally {
|
|
404
|
+
cleanup(base);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("gsd_replan_slice and gsd_slice_replan work end-to-end", async () => {
|
|
409
|
+
const base = makeTmpBase();
|
|
410
|
+
try {
|
|
411
|
+
const server = makeMockServer();
|
|
412
|
+
registerWorkflowTools(server as any);
|
|
413
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
414
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
415
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
416
|
+
const canonicalTool = server.tools.find((t) => t.name === "gsd_replan_slice");
|
|
417
|
+
const aliasTool = server.tools.find((t) => t.name === "gsd_slice_replan");
|
|
418
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
419
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
420
|
+
assert.ok(taskTool, "task completion tool should be registered");
|
|
421
|
+
assert.ok(canonicalTool, "slice replanning tool should be registered");
|
|
422
|
+
assert.ok(aliasTool, "slice replanning alias should be registered");
|
|
423
|
+
|
|
424
|
+
await milestoneTool!.handler({
|
|
425
|
+
projectDir: base,
|
|
426
|
+
milestoneId: "M099",
|
|
427
|
+
title: "Slice replanning",
|
|
428
|
+
vision: "Drive replan parity over MCP.",
|
|
429
|
+
slices: [
|
|
430
|
+
{
|
|
431
|
+
sliceId: "S09",
|
|
432
|
+
title: "Replan slice",
|
|
433
|
+
risk: "medium",
|
|
434
|
+
depends: [],
|
|
435
|
+
demo: "Slice replans after a blocker task completes.",
|
|
436
|
+
goal: "Prepare replan state.",
|
|
437
|
+
successCriteria: "Plan and replan artifacts update over MCP.",
|
|
438
|
+
proofLevel: "integration",
|
|
439
|
+
integrationClosure: "Replan uses the shared executor path.",
|
|
440
|
+
observabilityImpact: "Tests cover replan artifacts.",
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
});
|
|
444
|
+
await sliceTool!.handler({
|
|
445
|
+
projectDir: base,
|
|
446
|
+
milestoneId: "M099",
|
|
447
|
+
sliceId: "S09",
|
|
448
|
+
goal: "Plan a slice that will be replanned.",
|
|
449
|
+
tasks: [
|
|
450
|
+
{
|
|
451
|
+
taskId: "T09",
|
|
452
|
+
title: "Blocker task",
|
|
453
|
+
description: "Finish the blocker-discovery task.",
|
|
454
|
+
estimate: "5m",
|
|
455
|
+
files: ["src/blocker.ts"],
|
|
456
|
+
verify: "node --test",
|
|
457
|
+
inputs: ["M099-ROADMAP.md"],
|
|
458
|
+
expectedOutput: ["T09-SUMMARY.md"],
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
taskId: "T10",
|
|
462
|
+
title: "Pending task",
|
|
463
|
+
description: "Original follow-up task.",
|
|
464
|
+
estimate: "10m",
|
|
465
|
+
files: ["src/pending.ts"],
|
|
466
|
+
verify: "node --test",
|
|
467
|
+
inputs: ["S09-PLAN.md"],
|
|
468
|
+
expectedOutput: ["Updated plan"],
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
});
|
|
472
|
+
await taskTool!.handler({
|
|
473
|
+
projectDir: base,
|
|
474
|
+
milestoneId: "M099",
|
|
475
|
+
sliceId: "S09",
|
|
476
|
+
taskId: "T09",
|
|
477
|
+
oneLiner: "Completed blocker task",
|
|
478
|
+
narrative: "Prepared the slice for replanning.",
|
|
479
|
+
verification: "node --test",
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const canonicalResult = await canonicalTool!.handler({
|
|
483
|
+
projectDir: base,
|
|
484
|
+
milestoneId: "M099",
|
|
485
|
+
sliceId: "S09",
|
|
486
|
+
blockerTaskId: "T09",
|
|
487
|
+
blockerDescription: "Original approach is no longer viable.",
|
|
488
|
+
whatChanged: "Updated the remaining task and added remediation work.",
|
|
489
|
+
updatedTasks: [
|
|
490
|
+
{
|
|
491
|
+
taskId: "T10",
|
|
492
|
+
title: "Pending task (updated)",
|
|
493
|
+
description: "Updated follow-up task after replanning.",
|
|
494
|
+
estimate: "15m",
|
|
495
|
+
files: ["src/pending.ts", "src/replanned.ts"],
|
|
496
|
+
verify: "node --test",
|
|
497
|
+
inputs: ["S09-PLAN.md"],
|
|
498
|
+
expectedOutput: ["Updated plan"],
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
taskId: "T11",
|
|
502
|
+
title: "Remediation task",
|
|
503
|
+
description: "New task introduced by the replan.",
|
|
504
|
+
estimate: "20m",
|
|
505
|
+
files: ["src/remediation.ts"],
|
|
506
|
+
verify: "node --test",
|
|
507
|
+
inputs: ["S09-REPLAN.md"],
|
|
508
|
+
expectedOutput: ["Remediation patch"],
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
removedTaskIds: [],
|
|
512
|
+
});
|
|
513
|
+
assert.match((canonicalResult as any).content[0].text as string, /Replanned slice S09/);
|
|
514
|
+
|
|
515
|
+
const aliasResult = await aliasTool!.handler({
|
|
516
|
+
projectDir: base,
|
|
517
|
+
milestoneId: "M099",
|
|
518
|
+
sliceId: "S09",
|
|
519
|
+
blockerTaskId: "T09",
|
|
520
|
+
blockerDescription: "Alias path confirms the same replan flow.",
|
|
521
|
+
whatChanged: "Removed the remediation task after the alias check.",
|
|
522
|
+
updatedTasks: [
|
|
523
|
+
{
|
|
524
|
+
taskId: "T10",
|
|
525
|
+
title: "Pending task (updated again)",
|
|
526
|
+
description: "Alias adjusted the remaining pending task.",
|
|
527
|
+
estimate: "12m",
|
|
528
|
+
files: ["src/pending.ts"],
|
|
529
|
+
verify: "node --test",
|
|
530
|
+
inputs: ["S09-PLAN.md"],
|
|
531
|
+
expectedOutput: ["Updated plan"],
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
removedTaskIds: ["T11"],
|
|
535
|
+
});
|
|
536
|
+
assert.match((aliasResult as any).content[0].text as string, /Replanned slice S09/);
|
|
537
|
+
assert.ok(
|
|
538
|
+
existsSync(join(base, ".gsd", "milestones", "M099", "slices", "S09", "S09-REPLAN.md")),
|
|
539
|
+
"replan artifact should exist on disk",
|
|
540
|
+
);
|
|
541
|
+
assert.ok(
|
|
542
|
+
existsSync(join(base, ".gsd", "milestones", "M099", "slices", "S09", "S09-PLAN.md")),
|
|
543
|
+
"updated plan should exist on disk",
|
|
544
|
+
);
|
|
545
|
+
const removedTask = _getAdapter()!.prepare(
|
|
546
|
+
"SELECT id FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?",
|
|
547
|
+
).get("M099", "S09", "T11");
|
|
548
|
+
assert.equal(removedTask, undefined, "alias should remove the replanned task");
|
|
549
|
+
} finally {
|
|
550
|
+
cleanup(base);
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("gsd_slice_complete and gsd_complete_slice work end-to-end", async () => {
|
|
555
|
+
const base = makeTmpBase();
|
|
556
|
+
try {
|
|
557
|
+
const server = makeMockServer();
|
|
558
|
+
registerWorkflowTools(server as any);
|
|
559
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
560
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
561
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
562
|
+
const canonicalTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
|
563
|
+
const aliasTool = server.tools.find((t) => t.name === "gsd_complete_slice");
|
|
564
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
565
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
566
|
+
assert.ok(taskTool, "task completion tool should be registered");
|
|
567
|
+
assert.ok(canonicalTool, "slice completion tool should be registered");
|
|
568
|
+
assert.ok(aliasTool, "slice completion alias should be registered");
|
|
569
|
+
|
|
570
|
+
await milestoneTool!.handler({
|
|
571
|
+
projectDir: base,
|
|
572
|
+
milestoneId: "M003",
|
|
573
|
+
title: "Demo milestone",
|
|
574
|
+
vision: "Prepare canonical slice completion state.",
|
|
575
|
+
slices: [
|
|
576
|
+
{
|
|
577
|
+
sliceId: "S03",
|
|
578
|
+
title: "Demo Slice",
|
|
579
|
+
risk: "medium",
|
|
580
|
+
depends: [],
|
|
581
|
+
demo: "Canonical slice completes through MCP.",
|
|
582
|
+
goal: "Seed workflow state.",
|
|
583
|
+
successCriteria: "Slice summary and UAT files are written.",
|
|
584
|
+
proofLevel: "integration",
|
|
585
|
+
integrationClosure: "Planning and completion share the MCP bridge.",
|
|
586
|
+
observabilityImpact: "Workflow tests cover canonical completion.",
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
});
|
|
590
|
+
await sliceTool!.handler({
|
|
591
|
+
projectDir: base,
|
|
592
|
+
milestoneId: "M003",
|
|
593
|
+
sliceId: "S03",
|
|
594
|
+
goal: "Complete canonical slice over MCP.",
|
|
595
|
+
tasks: [
|
|
596
|
+
{
|
|
597
|
+
taskId: "T03",
|
|
598
|
+
title: "Canonical task",
|
|
599
|
+
description: "Seed a completed task for slice completion.",
|
|
600
|
+
estimate: "5m",
|
|
601
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
602
|
+
verify: "node --test",
|
|
603
|
+
inputs: ["M003-ROADMAP.md"],
|
|
604
|
+
expectedOutput: ["S03-SUMMARY.md", "S03-UAT.md"],
|
|
605
|
+
},
|
|
606
|
+
],
|
|
607
|
+
});
|
|
608
|
+
await taskTool!.handler({
|
|
609
|
+
projectDir: base,
|
|
610
|
+
milestoneId: "M003",
|
|
611
|
+
sliceId: "S03",
|
|
612
|
+
taskId: "T03",
|
|
613
|
+
oneLiner: "Completed canonical task",
|
|
614
|
+
narrative: "Prepared the canonical slice for completion.",
|
|
615
|
+
verification: "node --test",
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const canonicalResult = await canonicalTool!.handler({
|
|
619
|
+
projectDir: base,
|
|
620
|
+
milestoneId: "M003",
|
|
621
|
+
sliceId: "S03",
|
|
622
|
+
sliceTitle: "Demo Slice",
|
|
623
|
+
oneLiner: "Completed canonical slice",
|
|
624
|
+
narrative: "Did the slice work",
|
|
625
|
+
verification: "npm test",
|
|
626
|
+
uatContent: "## UAT\n\nPASS",
|
|
627
|
+
});
|
|
628
|
+
assert.match((canonicalResult as any).content[0].text as string, /Completed slice S03/);
|
|
629
|
+
|
|
630
|
+
await milestoneTool!.handler({
|
|
631
|
+
projectDir: base,
|
|
632
|
+
milestoneId: "M004",
|
|
633
|
+
title: "Alias milestone",
|
|
634
|
+
vision: "Prepare alias slice completion state.",
|
|
635
|
+
slices: [
|
|
636
|
+
{
|
|
637
|
+
sliceId: "S04",
|
|
638
|
+
title: "Alias Slice",
|
|
639
|
+
risk: "medium",
|
|
640
|
+
depends: [],
|
|
641
|
+
demo: "Alias slice completes through MCP.",
|
|
642
|
+
goal: "Seed alias workflow state.",
|
|
643
|
+
successCriteria: "Alias summary and UAT files are written.",
|
|
644
|
+
proofLevel: "integration",
|
|
645
|
+
integrationClosure: "Alias reaches the shared slice executor.",
|
|
646
|
+
observabilityImpact: "Workflow tests cover alias completion.",
|
|
647
|
+
},
|
|
648
|
+
],
|
|
649
|
+
});
|
|
650
|
+
await sliceTool!.handler({
|
|
651
|
+
projectDir: base,
|
|
652
|
+
milestoneId: "M004",
|
|
653
|
+
sliceId: "S04",
|
|
654
|
+
goal: "Complete alias slice over MCP.",
|
|
655
|
+
tasks: [
|
|
656
|
+
{
|
|
657
|
+
taskId: "T04",
|
|
658
|
+
title: "Alias task",
|
|
659
|
+
description: "Seed a completed task for alias slice completion.",
|
|
660
|
+
estimate: "5m",
|
|
661
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
662
|
+
verify: "node --test",
|
|
663
|
+
inputs: ["M004-ROADMAP.md"],
|
|
664
|
+
expectedOutput: ["S04-SUMMARY.md", "S04-UAT.md"],
|
|
665
|
+
},
|
|
666
|
+
],
|
|
667
|
+
});
|
|
668
|
+
await taskTool!.handler({
|
|
669
|
+
projectDir: base,
|
|
670
|
+
milestoneId: "M004",
|
|
671
|
+
sliceId: "S04",
|
|
672
|
+
taskId: "T04",
|
|
673
|
+
oneLiner: "Completed alias task",
|
|
674
|
+
narrative: "Prepared the alias slice for completion.",
|
|
675
|
+
verification: "node --test",
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const aliasResult = await aliasTool!.handler({
|
|
679
|
+
projectDir: base,
|
|
680
|
+
milestoneId: "M004",
|
|
681
|
+
sliceId: "S04",
|
|
682
|
+
sliceTitle: "Alias Slice",
|
|
683
|
+
oneLiner: "Completed alias slice",
|
|
684
|
+
narrative: "Did the slice work via alias",
|
|
685
|
+
verification: "npm test",
|
|
686
|
+
uatContent: "## UAT\n\nPASS",
|
|
687
|
+
});
|
|
688
|
+
assert.match((aliasResult as any).content[0].text as string, /Completed slice S04/);
|
|
689
|
+
assert.ok(
|
|
690
|
+
existsSync(join(base, ".gsd", "milestones", "M004", "slices", "S04", "S04-SUMMARY.md")),
|
|
691
|
+
"alias should write slice summary to disk",
|
|
692
|
+
);
|
|
693
|
+
assert.ok(
|
|
694
|
+
existsSync(join(base, ".gsd", "milestones", "M004", "slices", "S04", "S04-UAT.md")),
|
|
695
|
+
"alias should write slice UAT to disk",
|
|
696
|
+
);
|
|
697
|
+
} finally {
|
|
698
|
+
cleanup(base);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
it("gsd_validate_milestone and gsd_milestone_complete work end-to-end", async () => {
|
|
703
|
+
const base = makeTmpBase();
|
|
704
|
+
try {
|
|
705
|
+
const server = makeMockServer();
|
|
706
|
+
registerWorkflowTools(server as any);
|
|
707
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
708
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
709
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
710
|
+
const completeSliceTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
|
711
|
+
const validateTool = server.tools.find((t) => t.name === "gsd_validate_milestone");
|
|
712
|
+
const completeMilestoneAlias = server.tools.find((t) => t.name === "gsd_milestone_complete");
|
|
713
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
714
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
715
|
+
assert.ok(taskTool, "task completion tool should be registered");
|
|
716
|
+
assert.ok(completeSliceTool, "slice completion tool should be registered");
|
|
717
|
+
assert.ok(validateTool, "milestone validation tool should be registered");
|
|
718
|
+
assert.ok(completeMilestoneAlias, "milestone completion alias should be registered");
|
|
719
|
+
|
|
720
|
+
await milestoneTool!.handler({
|
|
721
|
+
projectDir: base,
|
|
722
|
+
milestoneId: "M005",
|
|
723
|
+
title: "Milestone lifecycle",
|
|
724
|
+
vision: "Drive validation and completion over MCP.",
|
|
725
|
+
slices: [
|
|
726
|
+
{
|
|
727
|
+
sliceId: "S05",
|
|
728
|
+
title: "Lifecycle slice",
|
|
729
|
+
risk: "medium",
|
|
730
|
+
depends: [],
|
|
731
|
+
demo: "Milestone can validate and complete.",
|
|
732
|
+
goal: "Seed milestone completion state.",
|
|
733
|
+
successCriteria: "Summary and validation artifacts are written.",
|
|
734
|
+
proofLevel: "integration",
|
|
735
|
+
integrationClosure: "Lifecycle tools share the MCP bridge.",
|
|
736
|
+
observabilityImpact: "Tests cover milestone end-to-end behavior.",
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
});
|
|
740
|
+
await sliceTool!.handler({
|
|
741
|
+
projectDir: base,
|
|
742
|
+
milestoneId: "M005",
|
|
743
|
+
sliceId: "S05",
|
|
744
|
+
goal: "Prepare a complete milestone.",
|
|
745
|
+
tasks: [
|
|
746
|
+
{
|
|
747
|
+
taskId: "T05",
|
|
748
|
+
title: "Lifecycle task",
|
|
749
|
+
description: "Seed a fully completed slice.",
|
|
750
|
+
estimate: "10m",
|
|
751
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
752
|
+
verify: "node --test",
|
|
753
|
+
inputs: ["M005-ROADMAP.md"],
|
|
754
|
+
expectedOutput: ["M005-VALIDATION.md", "M005-SUMMARY.md"],
|
|
755
|
+
},
|
|
756
|
+
],
|
|
757
|
+
});
|
|
758
|
+
await taskTool!.handler({
|
|
759
|
+
projectDir: base,
|
|
760
|
+
milestoneId: "M005",
|
|
761
|
+
sliceId: "S05",
|
|
762
|
+
taskId: "T05",
|
|
763
|
+
oneLiner: "Completed lifecycle task",
|
|
764
|
+
narrative: "Prepared the milestone for closure.",
|
|
765
|
+
verification: "node --test",
|
|
766
|
+
});
|
|
767
|
+
await completeSliceTool!.handler({
|
|
768
|
+
projectDir: base,
|
|
769
|
+
milestoneId: "M005",
|
|
770
|
+
sliceId: "S05",
|
|
771
|
+
sliceTitle: "Lifecycle Slice",
|
|
772
|
+
oneLiner: "Completed lifecycle slice",
|
|
773
|
+
narrative: "Closed the milestone slice.",
|
|
774
|
+
verification: "node --test",
|
|
775
|
+
uatContent: "## UAT\n\nPASS",
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
const validationResult = await validateTool!.handler({
|
|
779
|
+
projectDir: base,
|
|
780
|
+
milestoneId: "M005",
|
|
781
|
+
verdict: "pass",
|
|
782
|
+
remediationRound: 0,
|
|
783
|
+
successCriteriaChecklist: "- [x] Lifecycle verified",
|
|
784
|
+
sliceDeliveryAudit: "| Slice | Verdict |\n| --- | --- |\n| S05 | pass |",
|
|
785
|
+
crossSliceIntegration: "No cross-slice mismatches found.",
|
|
786
|
+
requirementCoverage: "No requirement gaps remain.",
|
|
787
|
+
verdictRationale: "The milestone delivered its scope.",
|
|
788
|
+
});
|
|
789
|
+
assert.match((validationResult as any).content[0].text as string, /Validated milestone M005/);
|
|
790
|
+
|
|
791
|
+
const completionResult = await completeMilestoneAlias!.handler({
|
|
792
|
+
projectDir: base,
|
|
793
|
+
milestoneId: "M005",
|
|
794
|
+
title: "Milestone lifecycle",
|
|
795
|
+
oneLiner: "Milestone closed successfully",
|
|
796
|
+
narrative: "Validation passed and all slices were complete.",
|
|
797
|
+
verificationPassed: true,
|
|
798
|
+
});
|
|
799
|
+
assert.match((completionResult as any).content[0].text as string, /Completed milestone M005/);
|
|
800
|
+
assert.ok(
|
|
801
|
+
existsSync(join(base, ".gsd", "milestones", "M005", "M005-VALIDATION.md")),
|
|
802
|
+
"validation artifact should exist on disk",
|
|
803
|
+
);
|
|
804
|
+
assert.ok(
|
|
805
|
+
existsSync(join(base, ".gsd", "milestones", "M005", "M005-SUMMARY.md")),
|
|
806
|
+
"milestone summary should exist on disk",
|
|
807
|
+
);
|
|
808
|
+
} finally {
|
|
809
|
+
cleanup(base);
|
|
810
|
+
}
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
it("gsd_reassess_roadmap, gsd_roadmap_reassess, and gsd_save_gate_result work end-to-end", async () => {
|
|
814
|
+
const base = makeTmpBase();
|
|
815
|
+
try {
|
|
816
|
+
const server = makeMockServer();
|
|
817
|
+
registerWorkflowTools(server as any);
|
|
818
|
+
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
|
819
|
+
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
|
820
|
+
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
|
821
|
+
const completeSliceTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
|
822
|
+
const reassessTool = server.tools.find((t) => t.name === "gsd_reassess_roadmap");
|
|
823
|
+
const reassessAlias = server.tools.find((t) => t.name === "gsd_roadmap_reassess");
|
|
824
|
+
const gateTool = server.tools.find((t) => t.name === "gsd_save_gate_result");
|
|
825
|
+
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
|
826
|
+
assert.ok(sliceTool, "slice planning tool should be registered");
|
|
827
|
+
assert.ok(taskTool, "task completion tool should be registered");
|
|
828
|
+
assert.ok(completeSliceTool, "slice completion tool should be registered");
|
|
829
|
+
assert.ok(reassessTool, "roadmap reassessment tool should be registered");
|
|
830
|
+
assert.ok(reassessAlias, "roadmap reassessment alias should be registered");
|
|
831
|
+
assert.ok(gateTool, "gate result tool should be registered");
|
|
832
|
+
|
|
833
|
+
await milestoneTool!.handler({
|
|
834
|
+
projectDir: base,
|
|
835
|
+
milestoneId: "M006",
|
|
836
|
+
title: "Roadmap reassessment",
|
|
837
|
+
vision: "Drive gate results and reassessment over MCP.",
|
|
838
|
+
slices: [
|
|
839
|
+
{
|
|
840
|
+
sliceId: "S06",
|
|
841
|
+
title: "Completed slice",
|
|
842
|
+
risk: "medium",
|
|
843
|
+
depends: [],
|
|
844
|
+
demo: "Completed slice triggers reassessment.",
|
|
845
|
+
goal: "Seed reassessment state.",
|
|
846
|
+
successCriteria: "Assessment and roadmap artifacts are written.",
|
|
847
|
+
proofLevel: "integration",
|
|
848
|
+
integrationClosure: "Roadmap updates share the MCP bridge.",
|
|
849
|
+
observabilityImpact: "Tests cover reassessment behavior.",
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
sliceId: "S07",
|
|
853
|
+
title: "Follow-up slice",
|
|
854
|
+
risk: "low",
|
|
855
|
+
depends: ["S06"],
|
|
856
|
+
demo: "Follow-up slice remains pending.",
|
|
857
|
+
goal: "Leave room for roadmap edits.",
|
|
858
|
+
successCriteria: "Roadmap mutation succeeds.",
|
|
859
|
+
proofLevel: "integration",
|
|
860
|
+
integrationClosure: "Pending slice can be modified after reassessment.",
|
|
861
|
+
observabilityImpact: "Tests observe roadmap mutation output.",
|
|
862
|
+
},
|
|
863
|
+
],
|
|
864
|
+
});
|
|
865
|
+
await sliceTool!.handler({
|
|
866
|
+
projectDir: base,
|
|
867
|
+
milestoneId: "M006",
|
|
868
|
+
sliceId: "S06",
|
|
869
|
+
goal: "Complete the first slice.",
|
|
870
|
+
tasks: [
|
|
871
|
+
{
|
|
872
|
+
taskId: "T06",
|
|
873
|
+
title: "Seed completed slice",
|
|
874
|
+
description: "Prepare gate and reassessment state.",
|
|
875
|
+
estimate: "10m",
|
|
876
|
+
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
|
877
|
+
verify: "node --test",
|
|
878
|
+
inputs: ["M006-ROADMAP.md"],
|
|
879
|
+
expectedOutput: ["S06-ASSESSMENT.md", "M006-ROADMAP.md"],
|
|
880
|
+
},
|
|
881
|
+
],
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
const gateResult = await gateTool!.handler({
|
|
885
|
+
projectDir: base,
|
|
886
|
+
milestoneId: "M006",
|
|
887
|
+
sliceId: "S06",
|
|
888
|
+
gateId: "Q3",
|
|
889
|
+
verdict: "pass",
|
|
890
|
+
rationale: "Threat surface is covered.",
|
|
891
|
+
findings: "No new attack surface was introduced.",
|
|
892
|
+
});
|
|
893
|
+
assert.match((gateResult as any).content[0].text as string, /Gate Q3 result saved/);
|
|
894
|
+
const gateRows = _getAdapter()!.prepare(
|
|
895
|
+
"SELECT status, verdict, rationale FROM quality_gates WHERE milestone_id = ? AND slice_id = ? AND gate_id = ?",
|
|
896
|
+
).all("M006", "S06", "Q3") as Array<Record<string, unknown>>;
|
|
897
|
+
assert.equal(gateRows.length, 1);
|
|
898
|
+
assert.equal(gateRows[0]["status"], "complete");
|
|
899
|
+
assert.equal(gateRows[0]["verdict"], "pass");
|
|
900
|
+
|
|
901
|
+
await taskTool!.handler({
|
|
902
|
+
projectDir: base,
|
|
903
|
+
milestoneId: "M006",
|
|
904
|
+
sliceId: "S06",
|
|
905
|
+
taskId: "T06",
|
|
906
|
+
oneLiner: "Completed reassessment task",
|
|
907
|
+
narrative: "Prepared the slice for reassessment.",
|
|
908
|
+
verification: "node --test",
|
|
909
|
+
});
|
|
910
|
+
await completeSliceTool!.handler({
|
|
911
|
+
projectDir: base,
|
|
912
|
+
milestoneId: "M006",
|
|
913
|
+
sliceId: "S06",
|
|
914
|
+
sliceTitle: "Completed slice",
|
|
915
|
+
oneLiner: "Completed reassessment slice",
|
|
916
|
+
narrative: "Closed the completed slice before reassessment.",
|
|
917
|
+
verification: "node --test",
|
|
918
|
+
uatContent: "## UAT\n\nPASS",
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
const reassessResult = await reassessTool!.handler({
|
|
922
|
+
projectDir: base,
|
|
923
|
+
milestoneId: "M006",
|
|
924
|
+
completedSliceId: "S06",
|
|
925
|
+
verdict: "roadmap-adjusted",
|
|
926
|
+
assessment: "Insert remediation work after the completed slice.",
|
|
927
|
+
sliceChanges: {
|
|
928
|
+
modified: [
|
|
929
|
+
{
|
|
930
|
+
sliceId: "S07",
|
|
931
|
+
title: "Follow-up slice (adjusted)",
|
|
932
|
+
risk: "medium",
|
|
933
|
+
depends: ["S06"],
|
|
934
|
+
demo: "Adjusted demo",
|
|
935
|
+
},
|
|
936
|
+
],
|
|
937
|
+
added: [
|
|
938
|
+
{
|
|
939
|
+
sliceId: "S08",
|
|
940
|
+
title: "Remediation slice",
|
|
941
|
+
risk: "high",
|
|
942
|
+
depends: ["S07"],
|
|
943
|
+
demo: "Remediation demo",
|
|
944
|
+
},
|
|
945
|
+
],
|
|
946
|
+
removed: [],
|
|
947
|
+
},
|
|
948
|
+
});
|
|
949
|
+
assert.match((reassessResult as any).content[0].text as string, /Reassessed roadmap for milestone M006 after S06/);
|
|
950
|
+
|
|
951
|
+
const reassessAliasResult = await reassessAlias!.handler({
|
|
952
|
+
projectDir: base,
|
|
953
|
+
milestoneId: "M006",
|
|
954
|
+
completedSliceId: "S06",
|
|
955
|
+
verdict: "roadmap-confirmed",
|
|
956
|
+
assessment: "No further changes needed after the first reassessment.",
|
|
957
|
+
sliceChanges: {
|
|
958
|
+
modified: [],
|
|
959
|
+
added: [],
|
|
960
|
+
removed: [],
|
|
961
|
+
},
|
|
962
|
+
});
|
|
963
|
+
assert.match((reassessAliasResult as any).content[0].text as string, /Reassessed roadmap for milestone M006 after S06/);
|
|
964
|
+
assert.ok(
|
|
965
|
+
existsSync(join(base, ".gsd", "milestones", "M006", "slices", "S06", "S06-ASSESSMENT.md")),
|
|
966
|
+
"assessment artifact should exist on disk",
|
|
967
|
+
);
|
|
968
|
+
assert.ok(
|
|
969
|
+
existsSync(join(base, ".gsd", "milestones", "M006", "M006-ROADMAP.md")),
|
|
970
|
+
"roadmap artifact should exist on disk",
|
|
971
|
+
);
|
|
972
|
+
} finally {
|
|
973
|
+
cleanup(base);
|
|
974
|
+
}
|
|
975
|
+
});
|
|
976
|
+
});
|