diff --git a/packages/mcp-server/src/workflow-tools.test.ts b/packages/mcp-server/src/workflow-tools.test.ts index 2efaccced..05b353b54 100644 --- a/packages/mcp-server/src/workflow-tools.test.ts +++ b/packages/mcp-server/src/workflow-tools.test.ts @@ -42,14 +42,14 @@ function makeMockServer() { } describe("workflow MCP tools", () => { - it("registers the five workflow tools", () => { + it("registers the six workflow tools", () => { const server = makeMockServer(); registerWorkflowTools(server as any); - assert.equal(server.tools.length, 5); + assert.equal(server.tools.length, 6); assert.deepEqual( server.tools.map((t) => t.name), - ["gsd_plan_milestone", "gsd_plan_slice", "gsd_summary_save", "gsd_task_complete", "gsd_milestone_status"], + ["gsd_plan_milestone", "gsd_plan_slice", "gsd_summary_save", "gsd_task_complete", "gsd_complete_task", "gsd_milestone_status"], ); }); @@ -125,6 +125,40 @@ describe("workflow MCP tools", () => { } }); + it("gsd_complete_task alias delegates to gsd_task_complete behavior", async () => { + const base = makeTmpBase(); + try { + mkdirSync(join(base, ".gsd", "milestones", "M002", "slices", "S02"), { recursive: true }); + writeFileSync( + join(base, ".gsd", "milestones", "M002", "slices", "S02", "S02-PLAN.md"), + "# S02\n\n- [ ] **T02: Demo** `est:5m`\n", + ); + + const server = makeMockServer(); + registerWorkflowTools(server as any); + const aliasTool = server.tools.find((t) => t.name === "gsd_complete_task"); + assert.ok(aliasTool, "task completion alias should be registered"); + + const result = await aliasTool!.handler({ + projectDir: base, + taskId: "T02", + sliceId: "S02", + milestoneId: "M002", + oneLiner: "Completed task via alias", + narrative: "Did the work through alias", + verification: "npm test", + }); + + assert.match((result as any).content[0].text as string, /Completed task T02/); + assert.ok( + existsSync(join(base, ".gsd", "milestones", "M002", "slices", "S02", "tasks", "T02-SUMMARY.md")), + "alias should write task summary to disk", + ); + } finally { + cleanup(base); + } + }); + it("gsd_plan_milestone and gsd_plan_slice work end-to-end", async () => { const base = makeTmpBase(); try { diff --git a/packages/mcp-server/src/workflow-tools.ts b/packages/mcp-server/src/workflow-tools.ts index 26e0e2dea..1aa0269fd 100644 --- a/packages/mcp-server/src/workflow-tools.ts +++ b/packages/mcp-server/src/workflow-tools.ts @@ -126,6 +126,61 @@ async function withProjectDir(projectDir: string, fn: () => Promise): Prom } } +async function handleTaskComplete( + projectDir: string, + args: Record, +): Promise { + const { + taskId, + sliceId, + milestoneId, + oneLiner, + narrative, + verification, + deviations, + knownIssues, + keyFiles, + keyDecisions, + blockerDiscovered, + verificationEvidence, + } = args as { + taskId: string; + sliceId: string; + milestoneId: string; + oneLiner: string; + narrative: string; + verification: string; + deviations?: string; + knownIssues?: string; + keyFiles?: string[]; + keyDecisions?: string[]; + blockerDiscovered?: boolean; + verificationEvidence?: Array< + { command: string; exitCode: number; verdict: string; durationMs: number } | string + >; + }; + const { executeTaskComplete } = await getWorkflowToolExecutors(); + return withProjectDir(projectDir, () => + executeTaskComplete( + { + taskId, + sliceId, + milestoneId, + oneLiner, + narrative, + verification, + deviations, + knownIssues, + keyFiles, + keyDecisions, + blockerDiscovered, + verificationEvidence, + }, + projectDir, + ), + ); +} + export function registerWorkflowTools(server: McpToolServer): void { server.tool( "gsd_plan_milestone", @@ -259,57 +314,40 @@ export function registerWorkflowTools(server: McpToolServer): void { ])).optional().describe("Verification evidence entries"), }, async (args: Record) => { - const { - projectDir, - taskId, - sliceId, - milestoneId, - oneLiner, - narrative, - verification, - deviations, - knownIssues, - keyFiles, - keyDecisions, - blockerDiscovered, - verificationEvidence, - } = args as { - projectDir: string; - taskId: string; - sliceId: string; - milestoneId: string; - oneLiner: string; - narrative: string; - verification: string; - deviations?: string; - knownIssues?: string; - keyFiles?: string[]; - keyDecisions?: string[]; - blockerDiscovered?: boolean; - verificationEvidence?: Array< - { command: string; exitCode: number; verdict: string; durationMs: number } | string - >; - }; - const { executeTaskComplete } = await getWorkflowToolExecutors(); - return withProjectDir(projectDir, () => - executeTaskComplete( - { - taskId, - sliceId, - milestoneId, - oneLiner, - narrative, - verification, - deviations, - knownIssues, - keyFiles, - keyDecisions, - blockerDiscovered, - verificationEvidence, - }, - projectDir, - ), - ); + const { projectDir, ...taskArgs } = args as { projectDir: string } & Record; + return handleTaskComplete(projectDir, taskArgs); + }, + ); + + server.tool( + "gsd_complete_task", + "Alias for gsd_task_complete. Record a completed task to the GSD database and render its SUMMARY.md.", + { + projectDir: z.string().describe("Absolute path to the project directory"), + taskId: z.string().describe("Task ID (e.g. T01)"), + sliceId: z.string().describe("Slice ID (e.g. S01)"), + milestoneId: z.string().describe("Milestone ID (e.g. M001)"), + oneLiner: z.string().describe("One-line summary of what was accomplished"), + narrative: z.string().describe("Detailed narrative of what happened during the task"), + verification: z.string().describe("What was verified and how"), + deviations: z.string().optional().describe("Deviations from the task plan"), + knownIssues: z.string().optional().describe("Known issues discovered but not fixed"), + keyFiles: z.array(z.string()).optional().describe("List of key files created or modified"), + keyDecisions: z.array(z.string()).optional().describe("List of key decisions made during this task"), + blockerDiscovered: z.boolean().optional().describe("Whether a plan-invalidating blocker was discovered"), + verificationEvidence: z.array(z.union([ + z.object({ + command: z.string(), + exitCode: z.number(), + verdict: z.string(), + durationMs: z.number(), + }), + z.string(), + ])).optional().describe("Verification evidence entries"), + }, + async (args: Record) => { + const { projectDir, ...taskArgs } = args as { projectDir: string } & Record; + return handleTaskComplete(projectDir, taskArgs); }, ); diff --git a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts index 76ae0fd7d..392fff68f 100644 --- a/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +++ b/src/resources/extensions/gsd/tests/workflow-mcp.test.ts @@ -104,7 +104,7 @@ test("transport compatibility fails cleanly when MCP server is unavailable", () assert.match(error ?? "", /workflow MCP server is not configured or discoverable/); }); -test("transport compatibility fails cleanly when unit requires unsupported tools", () => { +test("transport compatibility now allows auto execute-task over workflow MCP surface", () => { const error = getWorkflowTransportSupportError( "claude-code", ["gsd_complete_task"], @@ -118,8 +118,7 @@ test("transport compatibility fails cleanly when unit requires unsupported tools }, ); - assert.match(error ?? "", /requires gsd_complete_task/); - assert.match(error ?? "", /currently exposes only/); + assert.equal(error, null); }); test("transport compatibility ignores API-backed providers", () => { @@ -156,6 +155,24 @@ 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", () => { + const error = getWorkflowTransportSupportError( + "claude-code", + ["gsd_complete_slice"], + { + projectRoot: "/tmp/project", + env: { GSD_WORKFLOW_MCP_COMMAND: "node" }, + surface: "auto-mode", + unitType: "complete-slice", + authMode: "externalCli", + baseUrl: "local://claude-code", + }, + ); + + assert.match(error ?? "", /requires gsd_complete_slice/); + assert.match(error ?? "", /currently exposes only/); +}); + test("guided-flow source enforces workflow compatibility preflight", () => { const src = readSrc("guided-flow.ts"); assert.match(src, /getRequiredWorkflowToolsForGuidedUnit/); diff --git a/src/resources/extensions/gsd/workflow-mcp.ts b/src/resources/extensions/gsd/workflow-mcp.ts index 4aa99b5f1..1c5fac70d 100644 --- a/src/resources/extensions/gsd/workflow-mcp.ts +++ b/src/resources/extensions/gsd/workflow-mcp.ts @@ -20,6 +20,7 @@ export interface WorkflowCapabilityOptions { } const MCP_WORKFLOW_TOOL_SURFACE = new Set([ + "gsd_complete_task", "gsd_milestone_status", "gsd_plan_milestone", "gsd_plan_slice",