feat: expose task completion alias over workflow MCP
This commit is contained in:
parent
f7008107fb
commit
2f63012628
4 changed files with 147 additions and 57 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -126,6 +126,61 @@ async function withProjectDir<T>(projectDir: string, fn: () => Promise<T>): Prom
|
|||
}
|
||||
}
|
||||
|
||||
async function handleTaskComplete(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
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<string, unknown>) => {
|
||||
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<string, unknown>;
|
||||
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<string, unknown>) => {
|
||||
const { projectDir, ...taskArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleTaskComplete(projectDir, taskArgs);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue