feat: expose slice completion over workflow MCP
This commit is contained in:
parent
2f63012628
commit
af24dcb3c3
7 changed files with 456 additions and 85 deletions
|
|
@ -42,14 +42,23 @@ function makeMockServer() {
|
|||
}
|
||||
|
||||
describe("workflow MCP tools", () => {
|
||||
it("registers the six workflow tools", () => {
|
||||
it("registers the eight workflow tools", () => {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
|
||||
assert.equal(server.tools.length, 6);
|
||||
assert.equal(server.tools.length, 8);
|
||||
assert.deepEqual(
|
||||
server.tools.map((t) => t.name),
|
||||
["gsd_plan_milestone", "gsd_plan_slice", "gsd_summary_save", "gsd_task_complete", "gsd_complete_task", "gsd_milestone_status"],
|
||||
[
|
||||
"gsd_plan_milestone",
|
||||
"gsd_plan_slice",
|
||||
"gsd_slice_complete",
|
||||
"gsd_complete_slice",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
"gsd_complete_task",
|
||||
"gsd_milestone_status",
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -222,4 +231,151 @@ describe("workflow MCP tools", () => {
|
|||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
it("gsd_slice_complete and gsd_complete_slice work end-to-end", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
const milestoneTool = server.tools.find((t) => t.name === "gsd_plan_milestone");
|
||||
const sliceTool = server.tools.find((t) => t.name === "gsd_plan_slice");
|
||||
const taskTool = server.tools.find((t) => t.name === "gsd_task_complete");
|
||||
const canonicalTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
||||
const aliasTool = server.tools.find((t) => t.name === "gsd_complete_slice");
|
||||
assert.ok(milestoneTool, "milestone planning tool should be registered");
|
||||
assert.ok(sliceTool, "slice planning tool should be registered");
|
||||
assert.ok(taskTool, "task completion tool should be registered");
|
||||
assert.ok(canonicalTool, "slice completion tool should be registered");
|
||||
assert.ok(aliasTool, "slice completion alias should be registered");
|
||||
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M003",
|
||||
title: "Demo milestone",
|
||||
vision: "Prepare canonical slice completion state.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S03",
|
||||
title: "Demo Slice",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Canonical slice completes through MCP.",
|
||||
goal: "Seed workflow state.",
|
||||
successCriteria: "Slice summary and UAT files are written.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Planning and completion share the MCP bridge.",
|
||||
observabilityImpact: "Workflow tests cover canonical completion.",
|
||||
},
|
||||
],
|
||||
});
|
||||
await sliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M003",
|
||||
sliceId: "S03",
|
||||
goal: "Complete canonical slice over MCP.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T03",
|
||||
title: "Canonical task",
|
||||
description: "Seed a completed task for slice completion.",
|
||||
estimate: "5m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M003-ROADMAP.md"],
|
||||
expectedOutput: ["S03-SUMMARY.md", "S03-UAT.md"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await taskTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M003",
|
||||
sliceId: "S03",
|
||||
taskId: "T03",
|
||||
oneLiner: "Completed canonical task",
|
||||
narrative: "Prepared the canonical slice for completion.",
|
||||
verification: "node --test",
|
||||
});
|
||||
|
||||
const canonicalResult = await canonicalTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M003",
|
||||
sliceId: "S03",
|
||||
sliceTitle: "Demo Slice",
|
||||
oneLiner: "Completed canonical slice",
|
||||
narrative: "Did the slice work",
|
||||
verification: "npm test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
});
|
||||
assert.match((canonicalResult as any).content[0].text as string, /Completed slice S03/);
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M004",
|
||||
title: "Alias milestone",
|
||||
vision: "Prepare alias slice completion state.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S04",
|
||||
title: "Alias Slice",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Alias slice completes through MCP.",
|
||||
goal: "Seed alias workflow state.",
|
||||
successCriteria: "Alias summary and UAT files are written.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Alias reaches the shared slice executor.",
|
||||
observabilityImpact: "Workflow tests cover alias completion.",
|
||||
},
|
||||
],
|
||||
});
|
||||
await sliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
goal: "Complete alias slice over MCP.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T04",
|
||||
title: "Alias task",
|
||||
description: "Seed a completed task for alias slice completion.",
|
||||
estimate: "5m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M004-ROADMAP.md"],
|
||||
expectedOutput: ["S04-SUMMARY.md", "S04-UAT.md"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await taskTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
taskId: "T04",
|
||||
oneLiner: "Completed alias task",
|
||||
narrative: "Prepared the alias slice for completion.",
|
||||
verification: "node --test",
|
||||
});
|
||||
|
||||
const aliasResult = await aliasTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
sliceTitle: "Alias Slice",
|
||||
oneLiner: "Completed alias slice",
|
||||
narrative: "Did the slice work via alias",
|
||||
verification: "npm test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
});
|
||||
assert.match((aliasResult as any).content[0].text as string, /Completed slice S04/);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M004", "slices", "S04", "S04-SUMMARY.md")),
|
||||
"alias should write slice summary to disk",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M004", "slices", "S04", "S04-UAT.md")),
|
||||
"alias should write slice UAT to disk",
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -64,6 +64,34 @@ type WorkflowToolExecutors = {
|
|||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeSliceComplete: (
|
||||
params: {
|
||||
sliceId: string;
|
||||
milestoneId: string;
|
||||
sliceTitle: string;
|
||||
oneLiner: string;
|
||||
narrative: string;
|
||||
verification: string;
|
||||
uatContent: string;
|
||||
deviations?: string;
|
||||
knownLimitations?: string;
|
||||
followUps?: string;
|
||||
keyFiles?: string[] | string;
|
||||
keyDecisions?: string[] | string;
|
||||
patternsEstablished?: string[] | string;
|
||||
observabilitySurfaces?: string[] | string;
|
||||
provides?: string[] | string;
|
||||
requirementsSurfaced?: string[] | string;
|
||||
drillDownPaths?: string[] | string;
|
||||
affects?: string[] | string;
|
||||
requirementsAdvanced?: Array<{ id: string; how: string } | string>;
|
||||
requirementsValidated?: Array<{ id: string; proof: string } | string>;
|
||||
requirementsInvalidated?: Array<{ id: string; what: string } | string>;
|
||||
filesModified?: Array<{ path: string; description: string } | string>;
|
||||
requires?: Array<{ slice: string; provides: string } | string>;
|
||||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeSummarySave: (
|
||||
params: {
|
||||
milestone_id: string;
|
||||
|
|
@ -181,6 +209,14 @@ async function handleTaskComplete(
|
|||
);
|
||||
}
|
||||
|
||||
async function handleSliceComplete(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { executeSliceComplete } = await getWorkflowToolExecutors();
|
||||
return withProjectDir(projectDir, () => executeSliceComplete(args as any, projectDir));
|
||||
}
|
||||
|
||||
export function registerWorkflowTools(server: McpToolServer): void {
|
||||
server.tool(
|
||||
"gsd_plan_milestone",
|
||||
|
|
@ -260,6 +296,106 @@ export function registerWorkflowTools(server: McpToolServer): void {
|
|||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_slice_complete",
|
||||
"Record a completed slice to the GSD database, render SUMMARY.md + UAT.md, and update roadmap projection.",
|
||||
{
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
sliceId: z.string().describe("Slice ID (e.g. S01)"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
sliceTitle: z.string().describe("Title of the slice"),
|
||||
oneLiner: z.string().describe("One-line summary of what the slice accomplished"),
|
||||
narrative: z.string().describe("Detailed narrative of what happened across all tasks"),
|
||||
verification: z.string().describe("What was verified across all tasks"),
|
||||
uatContent: z.string().describe("UAT test content (markdown body)"),
|
||||
deviations: z.string().optional(),
|
||||
knownLimitations: z.string().optional(),
|
||||
followUps: z.string().optional(),
|
||||
keyFiles: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
keyDecisions: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
patternsEstablished: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
observabilitySurfaces: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
provides: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
requirementsSurfaced: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
drillDownPaths: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
affects: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
requirementsAdvanced: z.array(z.union([
|
||||
z.object({ id: z.string(), how: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requirementsValidated: z.array(z.union([
|
||||
z.object({ id: z.string(), proof: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requirementsInvalidated: z.array(z.union([
|
||||
z.object({ id: z.string(), what: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
filesModified: z.array(z.union([
|
||||
z.object({ path: z.string(), description: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requires: z.array(z.union([
|
||||
z.object({ slice: z.string(), provides: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...sliceArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleSliceComplete(projectDir, sliceArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_complete_slice",
|
||||
"Alias for gsd_slice_complete. Record a completed slice to the GSD database and render summary/UAT artifacts.",
|
||||
{
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
sliceId: z.string().describe("Slice ID (e.g. S01)"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
sliceTitle: z.string().describe("Title of the slice"),
|
||||
oneLiner: z.string().describe("One-line summary of what the slice accomplished"),
|
||||
narrative: z.string().describe("Detailed narrative of what happened across all tasks"),
|
||||
verification: z.string().describe("What was verified across all tasks"),
|
||||
uatContent: z.string().describe("UAT test content (markdown body)"),
|
||||
deviations: z.string().optional(),
|
||||
knownLimitations: z.string().optional(),
|
||||
followUps: z.string().optional(),
|
||||
keyFiles: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
keyDecisions: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
patternsEstablished: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
observabilitySurfaces: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
provides: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
requirementsSurfaced: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
drillDownPaths: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
affects: z.union([z.array(z.string()), z.string()]).optional(),
|
||||
requirementsAdvanced: z.array(z.union([
|
||||
z.object({ id: z.string(), how: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requirementsValidated: z.array(z.union([
|
||||
z.object({ id: z.string(), proof: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requirementsInvalidated: z.array(z.union([
|
||||
z.object({ id: z.string(), what: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
filesModified: z.array(z.union([
|
||||
z.object({ path: z.string(), description: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
requires: z.array(z.union([
|
||||
z.object({ slice: z.string(), provides: z.string() }),
|
||||
z.string(),
|
||||
])).optional(),
|
||||
},
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...sliceArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleSliceComplete(projectDir, sliceArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_summary_save",
|
||||
"Save a GSD summary/research/context/assessment artifact to the database and disk.",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { getErrorMessage } from "../error-utils.js";
|
|||
import {
|
||||
executePlanMilestone,
|
||||
executePlanSlice,
|
||||
executeSliceComplete,
|
||||
executeSummarySave,
|
||||
executeTaskComplete,
|
||||
} from "../tools/workflow-tool-executors.js";
|
||||
|
|
@ -647,86 +648,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
// ─── gsd_slice_complete (gsd_complete_slice alias) ─────────────────────
|
||||
|
||||
const sliceCompleteExecute = async (_toolCallId: string, params: any, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) => {
|
||||
const dbAvailable = await ensureDbOpen();
|
||||
if (!dbAvailable) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot complete slice." }],
|
||||
details: { operation: "complete_slice", error: "db_unavailable" } as any,
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Coerce string items to objects for fields where LLMs sometimes pass
|
||||
// plain strings instead of the expected { key, value } shape (#3541).
|
||||
// Parses "key — value" or "key - value" format when possible.
|
||||
const splitPair = (s: string): [string, string] => {
|
||||
const m = s.match(/^(.+?)\s*(?:—|-)\s+(.+)$/);
|
||||
return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
|
||||
};
|
||||
const coerced = { ...params };
|
||||
// Coerce simple string-array fields: LLMs sometimes pass a plain string
|
||||
// instead of a single-element array (#3585).
|
||||
const wrapArray = (v: any): any[] =>
|
||||
v == null ? [] : Array.isArray(v) ? v : [v];
|
||||
coerced.provides = wrapArray(params.provides);
|
||||
coerced.keyFiles = wrapArray(params.keyFiles);
|
||||
coerced.keyDecisions = wrapArray(params.keyDecisions);
|
||||
coerced.patternsEstablished = wrapArray(params.patternsEstablished);
|
||||
coerced.observabilitySurfaces = wrapArray(params.observabilitySurfaces);
|
||||
coerced.requirementsSurfaced = wrapArray(params.requirementsSurfaced);
|
||||
coerced.drillDownPaths = wrapArray(params.drillDownPaths);
|
||||
coerced.affects = wrapArray(params.affects);
|
||||
coerced.filesModified = wrapArray(params.filesModified).map((f: any) => {
|
||||
if (typeof f !== "string") return f;
|
||||
const [path, description] = splitPair(f);
|
||||
return { path, description };
|
||||
});
|
||||
coerced.requires = wrapArray(params.requires).map((r: any) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [slice, provides] = splitPair(r);
|
||||
return { slice, provides };
|
||||
});
|
||||
coerced.requirementsAdvanced = wrapArray(params.requirementsAdvanced).map((r: any) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, how] = splitPair(r);
|
||||
return { id, how };
|
||||
});
|
||||
coerced.requirementsValidated = wrapArray(params.requirementsValidated).map((r: any) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, proof] = splitPair(r);
|
||||
return { id, proof };
|
||||
});
|
||||
coerced.requirementsInvalidated = wrapArray(params.requirementsInvalidated).map((r: any) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, what] = splitPair(r);
|
||||
return { id, what };
|
||||
});
|
||||
|
||||
const { handleCompleteSlice } = await import("../tools/complete-slice.js");
|
||||
const result = await handleCompleteSlice(coerced, process.cwd());
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error completing slice: ${result.error}` }],
|
||||
details: { operation: "complete_slice", error: result.error } as any,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Completed slice ${result.sliceId} (${result.milestoneId})` }],
|
||||
details: {
|
||||
operation: "complete_slice",
|
||||
sliceId: result.sliceId,
|
||||
milestoneId: result.milestoneId,
|
||||
summaryPath: result.summaryPath,
|
||||
uatPath: result.uatPath,
|
||||
} as any,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `complete_slice tool failed: ${msg}`, { tool: "gsd_slice_complete", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error completing slice: ${msg}` }],
|
||||
details: { operation: "complete_slice", error: msg } as any,
|
||||
};
|
||||
}
|
||||
return executeSliceComplete(params, process.cwd());
|
||||
};
|
||||
|
||||
const sliceCompleteTool = {
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ test("transport compatibility now allows plan-slice over workflow MCP surface",
|
|||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
|
||||
test("transport compatibility now allows complete-slice over workflow MCP surface", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_complete_slice"],
|
||||
|
|
@ -169,7 +169,24 @@ test("transport compatibility still blocks units whose MCP tools are not exposed
|
|||
},
|
||||
);
|
||||
|
||||
assert.match(error ?? "", /requires gsd_complete_slice/);
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_complete_milestone"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
surface: "auto-mode",
|
||||
unitType: "complete-milestone",
|
||||
authMode: "externalCli",
|
||||
baseUrl: "local://claude-code",
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(error ?? "", /requires gsd_complete_milestone/);
|
||||
assert.match(error ?? "", /currently exposes only/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
executeMilestoneStatus,
|
||||
executePlanMilestone,
|
||||
executePlanSlice,
|
||||
executeSliceComplete,
|
||||
} from "../tools/workflow-tool-executors.ts";
|
||||
|
||||
function makeTmpBase(): string {
|
||||
|
|
@ -58,6 +59,20 @@ function seedSlice(milestoneId: string, sliceId: string, status: string): void {
|
|||
).run(milestoneId, sliceId, `Slice ${sliceId}`, status, new Date().toISOString());
|
||||
}
|
||||
|
||||
function writeRoadmap(base: string, milestoneId: string, sliceIds: string[]): void {
|
||||
const milestoneDir = join(base, ".gsd", "milestones", milestoneId);
|
||||
mkdirSync(milestoneDir, { recursive: true });
|
||||
const lines = [
|
||||
`# ${milestoneId}: Workflow MCP planning`,
|
||||
"",
|
||||
"## Slices",
|
||||
"",
|
||||
...sliceIds.map((sliceId) => `- [ ] **${sliceId}: Slice ${sliceId}** \`risk:medium\` \`depends:[]\`\n - After this: demo`),
|
||||
"",
|
||||
];
|
||||
writeFileSync(join(milestoneDir, `${milestoneId}-ROADMAP.md`), lines.join("\n"));
|
||||
}
|
||||
|
||||
test("executeSummarySave persists artifact and returns computed path", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
|
|
@ -234,3 +249,42 @@ test("executePlanSlice writes task planning state and rendered plan artifacts",
|
|||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("executeSliceComplete coerces string enrichment entries and writes summary/UAT artifacts", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
openTestDb(base);
|
||||
seedMilestone("M001", "Milestone One");
|
||||
seedSlice("M001", "S01", "pending");
|
||||
writeRoadmap(base, "M001", ["S01"]);
|
||||
const db = _getAdapter();
|
||||
db!.prepare(
|
||||
"INSERT OR REPLACE INTO tasks (milestone_id, slice_id, id, title, status) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("M001", "S01", "T01", "Task T01", "complete");
|
||||
|
||||
const result = await inProjectDir(base, () => executeSliceComplete({
|
||||
milestoneId: "M001",
|
||||
sliceId: "S01",
|
||||
sliceTitle: "Slice S01",
|
||||
oneLiner: "Completed slice",
|
||||
narrative: "Implemented the slice",
|
||||
verification: "node --test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
provides: "shared executor path",
|
||||
requirementsAdvanced: ["R001 - added slice completion support"],
|
||||
filesModified: ["src/file.ts - updated logic"],
|
||||
requires: ["S00 - upstream context"],
|
||||
}, base));
|
||||
|
||||
assert.equal(result.details.operation, "complete_slice");
|
||||
const summaryPath = String(result.details.summaryPath);
|
||||
const uatPath = String(result.details.uatPath);
|
||||
assert.ok(existsSync(summaryPath), "slice summary should be written to disk");
|
||||
assert.ok(existsSync(uatPath), "slice UAT should be written to disk");
|
||||
assert.match(readFileSync(summaryPath, "utf-8"), /shared executor path/);
|
||||
assert.match(readFileSync(summaryPath, "utf-8"), /R001/);
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import {
|
|||
} from "../gsd-db.js";
|
||||
import { saveArtifactToDb } from "../db-writer.js";
|
||||
import { handleCompleteTask } from "./complete-task.js";
|
||||
import type { CompleteSliceParams } from "../types.js";
|
||||
import { handleCompleteSlice } from "./complete-slice.js";
|
||||
import type { PlanMilestoneParams } from "./plan-milestone.js";
|
||||
import { handlePlanMilestone } from "./plan-milestone.js";
|
||||
import type { PlanSliceParams } from "./plan-slice.js";
|
||||
|
|
@ -122,6 +124,7 @@ export interface TaskCompleteParams {
|
|||
verificationEvidence?: VerificationEvidenceInput[];
|
||||
}
|
||||
|
||||
export type SliceCompleteExecutorParams = CompleteSliceParams;
|
||||
export type PlanMilestoneExecutorParams = PlanMilestoneParams;
|
||||
export type PlanSliceExecutorParams = PlanSliceParams;
|
||||
|
||||
|
|
@ -169,6 +172,87 @@ export async function executeTaskComplete(
|
|||
}
|
||||
}
|
||||
|
||||
export async function executeSliceComplete(
|
||||
params: SliceCompleteExecutorParams,
|
||||
basePath: string = process.cwd(),
|
||||
): Promise<ToolExecutionResult> {
|
||||
const dbAvailable = await ensureDbOpen();
|
||||
if (!dbAvailable) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: GSD database is not available. Cannot complete slice." }],
|
||||
details: { operation: "complete_slice", error: "db_unavailable" },
|
||||
};
|
||||
}
|
||||
try {
|
||||
const splitPair = (s: string): [string, string] => {
|
||||
const m = s.match(/^(.+?)\s*(?:—|-)\s+(.+)$/);
|
||||
return m ? [m[1].trim(), m[2].trim()] : [s.trim(), ""];
|
||||
};
|
||||
const wrapArray = (v: unknown): unknown[] =>
|
||||
v == null ? [] : Array.isArray(v) ? v : [v];
|
||||
|
||||
const coerced = { ...params } as CompleteSliceParams & Record<string, unknown>;
|
||||
coerced.provides = wrapArray(params.provides) as string[];
|
||||
coerced.keyFiles = wrapArray(params.keyFiles) as string[];
|
||||
coerced.keyDecisions = wrapArray(params.keyDecisions) as string[];
|
||||
coerced.patternsEstablished = wrapArray(params.patternsEstablished) as string[];
|
||||
coerced.observabilitySurfaces = wrapArray(params.observabilitySurfaces) as string[];
|
||||
coerced.requirementsSurfaced = wrapArray(params.requirementsSurfaced) as string[];
|
||||
coerced.drillDownPaths = wrapArray(params.drillDownPaths) as string[];
|
||||
coerced.affects = wrapArray(params.affects) as string[];
|
||||
coerced.filesModified = wrapArray(params.filesModified).map((f) => {
|
||||
if (typeof f !== "string") return f;
|
||||
const [path, description] = splitPair(f);
|
||||
return { path, description };
|
||||
}) as Array<{ path: string; description: string }>;
|
||||
coerced.requires = wrapArray(params.requires).map((r) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [slice, provides] = splitPair(r);
|
||||
return { slice, provides };
|
||||
}) as Array<{ slice: string; provides: string }>;
|
||||
coerced.requirementsAdvanced = wrapArray(params.requirementsAdvanced).map((r) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, how] = splitPair(r);
|
||||
return { id, how };
|
||||
}) as Array<{ id: string; how: string }>;
|
||||
coerced.requirementsValidated = wrapArray(params.requirementsValidated).map((r) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, proof] = splitPair(r);
|
||||
return { id, proof };
|
||||
}) as Array<{ id: string; proof: string }>;
|
||||
coerced.requirementsInvalidated = wrapArray(params.requirementsInvalidated).map((r) => {
|
||||
if (typeof r !== "string") return r;
|
||||
const [id, what] = splitPair(r);
|
||||
return { id, what };
|
||||
}) as Array<{ id: string; what: string }>;
|
||||
|
||||
const result = await handleCompleteSlice(coerced as CompleteSliceParams, basePath);
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error completing slice: ${result.error}` }],
|
||||
details: { operation: "complete_slice", error: result.error },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Completed slice ${result.sliceId} (${result.milestoneId})` }],
|
||||
details: {
|
||||
operation: "complete_slice",
|
||||
sliceId: result.sliceId,
|
||||
milestoneId: result.milestoneId,
|
||||
summaryPath: result.summaryPath,
|
||||
uatPath: result.uatPath,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `complete_slice tool failed: ${msg}`, { tool: "gsd_slice_complete", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Error completing slice: ${msg}` }],
|
||||
details: { operation: "complete_slice", error: msg },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executePlanMilestone(
|
||||
params: PlanMilestoneExecutorParams,
|
||||
basePath: string = process.cwd(),
|
||||
|
|
|
|||
|
|
@ -21,9 +21,11 @@ export interface WorkflowCapabilityOptions {
|
|||
|
||||
const MCP_WORKFLOW_TOOL_SURFACE = new Set([
|
||||
"gsd_complete_task",
|
||||
"gsd_complete_slice",
|
||||
"gsd_milestone_status",
|
||||
"gsd_plan_milestone",
|
||||
"gsd_plan_slice",
|
||||
"gsd_slice_complete",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
]);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue