From af24dcb3c327d5f680a2a26cdc8a94d8f622d007 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 9 Apr 2026 11:53:28 -0500 Subject: [PATCH] feat: expose slice completion over workflow MCP --- .../mcp-server/src/workflow-tools.test.ts | 162 +++++++++++++++++- packages/mcp-server/src/workflow-tools.ts | 136 +++++++++++++++ .../extensions/gsd/bootstrap/db-tools.ts | 82 +-------- .../extensions/gsd/tests/workflow-mcp.test.ts | 21 ++- .../gsd/tests/workflow-tool-executors.test.ts | 54 ++++++ .../gsd/tools/workflow-tool-executors.ts | 84 +++++++++ src/resources/extensions/gsd/workflow-mcp.ts | 2 + 7 files changed, 456 insertions(+), 85 deletions(-) diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts index 05b353b54..855596821 100644 --- a/packages/mcp-server/src/workflow-tools.test.ts +++ b/packages/mcp-server/src/workflow-tools.test.ts @@ -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); + } + }); }); diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts index 1aa0269fd..dfe652ec2 100644 --- a/packages/mcp-server/src/workflow-tools.ts +++ b/packages/mcp-server/src/workflow-tools.ts @@ -64,6 +64,34 @@ type WorkflowToolExecutors = { }, basePath?: string, ) => Promise; + 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; executeSummarySave: ( params: { milestone_id: string; @@ -181,6 +209,14 @@ async function handleTaskComplete( ); } +async function handleSliceComplete( + projectDir: string, + args: Record, +): Promise { + 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) => { + const { projectDir, ...sliceArgs } = args as { projectDir: string } & Record; + 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) => { + const { projectDir, ...sliceArgs } = args as { projectDir: string } & Record; + return handleSliceComplete(projectDir, sliceArgs); + }, + ); + server.tool( "gsd_summary_save", "Save a GSD summary/research/context/assessment artifact to the database and disk.", diff --git a/src/resources/extensions/gsd/bootstrap/db-tools.ts b/src/resources/extensions/gsd/bootstrap/db-tools.ts index 1f7c15d2d..dd605c708 100644 --- a/src/resources/extensions/gsd/bootstrap/db-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/db-tools.ts @@ -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 = { diff --git a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts index 392fff68f..4dd7c4f00 100644 --- a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts @@ -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/); }); diff --git a/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts b/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts index 46418c613..66f28980b 100644 --- a/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts @@ -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); + } +}); diff --git a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts index d5b2f3564..0e481819b 100644 --- a/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +++ b/src/resources/extensions/gsd/tools/workflow-tool-executors.ts @@ -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 { + 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; + 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(), diff --git a/src/resources/extensions/gsd/workflow-mcp.ts b/src/resources/extensions/gsd/workflow-mcp.ts index 1c5fac70d..ed9dc0b95 100644 --- a/src/resources/extensions/gsd/workflow-mcp.ts +++ b/src/resources/extensions/gsd/workflow-mcp.ts @@ -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", ]);