feat: expose milestone workflow tools over MCP
This commit is contained in:
parent
af24dcb3c3
commit
70458467ff
7 changed files with 1018 additions and 154 deletions
|
|
@ -5,6 +5,7 @@ import { join } from "node:path";
|
|||
import { tmpdir } from "node:os";
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import { _getAdapter, closeDatabase } from "../../../src/resources/extensions/gsd/gsd-db.ts";
|
||||
import { registerWorkflowTools } from "./workflow-tools.ts";
|
||||
|
||||
function makeTmpBase(): string {
|
||||
|
|
@ -14,6 +15,11 @@ function makeTmpBase(): string {
|
|||
}
|
||||
|
||||
function cleanup(base: string): void {
|
||||
try {
|
||||
closeDatabase();
|
||||
} catch {
|
||||
// swallow
|
||||
}
|
||||
try {
|
||||
rmSync(base, { recursive: true, force: true });
|
||||
} catch {
|
||||
|
|
@ -42,11 +48,11 @@ function makeMockServer() {
|
|||
}
|
||||
|
||||
describe("workflow MCP tools", () => {
|
||||
it("registers the eight workflow tools", () => {
|
||||
it("registers the fifteen workflow tools", () => {
|
||||
const server = makeMockServer();
|
||||
registerWorkflowTools(server as any);
|
||||
|
||||
assert.equal(server.tools.length, 8);
|
||||
assert.equal(server.tools.length, 15);
|
||||
assert.deepEqual(
|
||||
server.tools.map((t) => t.name),
|
||||
[
|
||||
|
|
@ -54,6 +60,13 @@ describe("workflow MCP tools", () => {
|
|||
"gsd_plan_slice",
|
||||
"gsd_slice_complete",
|
||||
"gsd_complete_slice",
|
||||
"gsd_complete_milestone",
|
||||
"gsd_milestone_complete",
|
||||
"gsd_validate_milestone",
|
||||
"gsd_milestone_validate",
|
||||
"gsd_reassess_roadmap",
|
||||
"gsd_roadmap_reassess",
|
||||
"gsd_save_gate_result",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
"gsd_complete_task",
|
||||
|
|
@ -307,6 +320,7 @@ describe("workflow MCP tools", () => {
|
|||
uatContent: "## UAT\n\nPASS",
|
||||
});
|
||||
assert.match((canonicalResult as any).content[0].text as string, /Completed slice S03/);
|
||||
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M004",
|
||||
|
|
@ -378,4 +392,279 @@ describe("workflow MCP tools", () => {
|
|||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
it("gsd_validate_milestone and gsd_milestone_complete 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 completeSliceTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
||||
const validateTool = server.tools.find((t) => t.name === "gsd_validate_milestone");
|
||||
const completeMilestoneAlias = server.tools.find((t) => t.name === "gsd_milestone_complete");
|
||||
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(completeSliceTool, "slice completion tool should be registered");
|
||||
assert.ok(validateTool, "milestone validation tool should be registered");
|
||||
assert.ok(completeMilestoneAlias, "milestone completion alias should be registered");
|
||||
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
title: "Milestone lifecycle",
|
||||
vision: "Drive validation and completion over MCP.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S05",
|
||||
title: "Lifecycle slice",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Milestone can validate and complete.",
|
||||
goal: "Seed milestone completion state.",
|
||||
successCriteria: "Summary and validation artifacts are written.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Lifecycle tools share the MCP bridge.",
|
||||
observabilityImpact: "Tests cover milestone end-to-end behavior.",
|
||||
},
|
||||
],
|
||||
});
|
||||
await sliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
sliceId: "S05",
|
||||
goal: "Prepare a complete milestone.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T05",
|
||||
title: "Lifecycle task",
|
||||
description: "Seed a fully completed slice.",
|
||||
estimate: "10m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M005-ROADMAP.md"],
|
||||
expectedOutput: ["M005-VALIDATION.md", "M005-SUMMARY.md"],
|
||||
},
|
||||
],
|
||||
});
|
||||
await taskTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
sliceId: "S05",
|
||||
taskId: "T05",
|
||||
oneLiner: "Completed lifecycle task",
|
||||
narrative: "Prepared the milestone for closure.",
|
||||
verification: "node --test",
|
||||
});
|
||||
await completeSliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
sliceId: "S05",
|
||||
sliceTitle: "Lifecycle Slice",
|
||||
oneLiner: "Completed lifecycle slice",
|
||||
narrative: "Closed the milestone slice.",
|
||||
verification: "node --test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
});
|
||||
|
||||
const validationResult = await validateTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
verdict: "pass",
|
||||
remediationRound: 0,
|
||||
successCriteriaChecklist: "- [x] Lifecycle verified",
|
||||
sliceDeliveryAudit: "| Slice | Verdict |\n| --- | --- |\n| S05 | pass |",
|
||||
crossSliceIntegration: "No cross-slice mismatches found.",
|
||||
requirementCoverage: "No requirement gaps remain.",
|
||||
verdictRationale: "The milestone delivered its scope.",
|
||||
});
|
||||
assert.match((validationResult as any).content[0].text as string, /Validated milestone M005/);
|
||||
|
||||
const completionResult = await completeMilestoneAlias!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M005",
|
||||
title: "Milestone lifecycle",
|
||||
oneLiner: "Milestone closed successfully",
|
||||
narrative: "Validation passed and all slices were complete.",
|
||||
verificationPassed: true,
|
||||
});
|
||||
assert.match((completionResult as any).content[0].text as string, /Completed milestone M005/);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M005", "M005-VALIDATION.md")),
|
||||
"validation artifact should exist on disk",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M005", "M005-SUMMARY.md")),
|
||||
"milestone summary should exist on disk",
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
it("gsd_reassess_roadmap, gsd_roadmap_reassess, and gsd_save_gate_result 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 completeSliceTool = server.tools.find((t) => t.name === "gsd_slice_complete");
|
||||
const reassessTool = server.tools.find((t) => t.name === "gsd_reassess_roadmap");
|
||||
const reassessAlias = server.tools.find((t) => t.name === "gsd_roadmap_reassess");
|
||||
const gateTool = server.tools.find((t) => t.name === "gsd_save_gate_result");
|
||||
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(completeSliceTool, "slice completion tool should be registered");
|
||||
assert.ok(reassessTool, "roadmap reassessment tool should be registered");
|
||||
assert.ok(reassessAlias, "roadmap reassessment alias should be registered");
|
||||
assert.ok(gateTool, "gate result tool should be registered");
|
||||
|
||||
await milestoneTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
title: "Roadmap reassessment",
|
||||
vision: "Drive gate results and reassessment over MCP.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S06",
|
||||
title: "Completed slice",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Completed slice triggers reassessment.",
|
||||
goal: "Seed reassessment state.",
|
||||
successCriteria: "Assessment and roadmap artifacts are written.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Roadmap updates share the MCP bridge.",
|
||||
observabilityImpact: "Tests cover reassessment behavior.",
|
||||
},
|
||||
{
|
||||
sliceId: "S07",
|
||||
title: "Follow-up slice",
|
||||
risk: "low",
|
||||
depends: ["S06"],
|
||||
demo: "Follow-up slice remains pending.",
|
||||
goal: "Leave room for roadmap edits.",
|
||||
successCriteria: "Roadmap mutation succeeds.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Pending slice can be modified after reassessment.",
|
||||
observabilityImpact: "Tests observe roadmap mutation output.",
|
||||
},
|
||||
],
|
||||
});
|
||||
await sliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
sliceId: "S06",
|
||||
goal: "Complete the first slice.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T06",
|
||||
title: "Seed completed slice",
|
||||
description: "Prepare gate and reassessment state.",
|
||||
estimate: "10m",
|
||||
files: ["packages/mcp-server/src/workflow-tools.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M006-ROADMAP.md"],
|
||||
expectedOutput: ["S06-ASSESSMENT.md", "M006-ROADMAP.md"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const gateResult = await gateTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
sliceId: "S06",
|
||||
gateId: "Q3",
|
||||
verdict: "pass",
|
||||
rationale: "Threat surface is covered.",
|
||||
findings: "No new attack surface was introduced.",
|
||||
});
|
||||
assert.match((gateResult as any).content[0].text as string, /Gate Q3 result saved/);
|
||||
const gateRows = _getAdapter()!.prepare(
|
||||
"SELECT status, verdict, rationale FROM quality_gates WHERE milestone_id = ? AND slice_id = ? AND gate_id = ?",
|
||||
).all("M006", "S06", "Q3") as Array<Record<string, unknown>>;
|
||||
assert.equal(gateRows.length, 1);
|
||||
assert.equal(gateRows[0]["status"], "complete");
|
||||
assert.equal(gateRows[0]["verdict"], "pass");
|
||||
|
||||
await taskTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
sliceId: "S06",
|
||||
taskId: "T06",
|
||||
oneLiner: "Completed reassessment task",
|
||||
narrative: "Prepared the slice for reassessment.",
|
||||
verification: "node --test",
|
||||
});
|
||||
await completeSliceTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
sliceId: "S06",
|
||||
sliceTitle: "Completed slice",
|
||||
oneLiner: "Completed reassessment slice",
|
||||
narrative: "Closed the completed slice before reassessment.",
|
||||
verification: "node --test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
});
|
||||
|
||||
const reassessResult = await reassessTool!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
completedSliceId: "S06",
|
||||
verdict: "roadmap-adjusted",
|
||||
assessment: "Insert remediation work after the completed slice.",
|
||||
sliceChanges: {
|
||||
modified: [
|
||||
{
|
||||
sliceId: "S07",
|
||||
title: "Follow-up slice (adjusted)",
|
||||
risk: "medium",
|
||||
depends: ["S06"],
|
||||
demo: "Adjusted demo",
|
||||
},
|
||||
],
|
||||
added: [
|
||||
{
|
||||
sliceId: "S08",
|
||||
title: "Remediation slice",
|
||||
risk: "high",
|
||||
depends: ["S07"],
|
||||
demo: "Remediation demo",
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
assert.match((reassessResult as any).content[0].text as string, /Reassessed roadmap for milestone M006 after S06/);
|
||||
|
||||
const reassessAliasResult = await reassessAlias!.handler({
|
||||
projectDir: base,
|
||||
milestoneId: "M006",
|
||||
completedSliceId: "S06",
|
||||
verdict: "roadmap-confirmed",
|
||||
assessment: "No further changes needed after the first reassessment.",
|
||||
sliceChanges: {
|
||||
modified: [],
|
||||
added: [],
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
assert.match((reassessAliasResult as any).content[0].text as string, /Reassessed roadmap for milestone M006 after S06/);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M006", "slices", "S06", "S06-ASSESSMENT.md")),
|
||||
"assessment artifact should exist on disk",
|
||||
);
|
||||
assert.ok(
|
||||
existsSync(join(base, ".gsd", "milestones", "M006", "M006-ROADMAP.md")),
|
||||
"roadmap artifact should exist on disk",
|
||||
);
|
||||
} finally {
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -92,6 +92,76 @@ type WorkflowToolExecutors = {
|
|||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeCompleteMilestone: (
|
||||
params: {
|
||||
milestoneId: string;
|
||||
title: string;
|
||||
oneLiner: string;
|
||||
narrative: string;
|
||||
verificationPassed: boolean;
|
||||
successCriteriaResults?: string;
|
||||
definitionOfDoneResults?: string;
|
||||
requirementOutcomes?: string;
|
||||
keyDecisions?: string[];
|
||||
keyFiles?: string[];
|
||||
lessonsLearned?: string[];
|
||||
followUps?: string;
|
||||
deviations?: string;
|
||||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeValidateMilestone: (
|
||||
params: {
|
||||
milestoneId: string;
|
||||
verdict: "pass" | "needs-attention" | "needs-remediation";
|
||||
remediationRound: number;
|
||||
successCriteriaChecklist: string;
|
||||
sliceDeliveryAudit: string;
|
||||
crossSliceIntegration: string;
|
||||
requirementCoverage: string;
|
||||
verificationClasses?: string;
|
||||
verdictRationale: string;
|
||||
remediationPlan?: string;
|
||||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeReassessRoadmap: (
|
||||
params: {
|
||||
milestoneId: string;
|
||||
completedSliceId: string;
|
||||
verdict: string;
|
||||
assessment: string;
|
||||
sliceChanges: {
|
||||
modified: Array<{
|
||||
sliceId: string;
|
||||
title: string;
|
||||
risk?: string;
|
||||
depends?: string[];
|
||||
demo?: string;
|
||||
}>;
|
||||
added: Array<{
|
||||
sliceId: string;
|
||||
title: string;
|
||||
risk?: string;
|
||||
depends?: string[];
|
||||
demo?: string;
|
||||
}>;
|
||||
removed: string[];
|
||||
};
|
||||
},
|
||||
basePath?: string,
|
||||
) => Promise<unknown>;
|
||||
executeSaveGateResult: (
|
||||
params: {
|
||||
milestoneId: string;
|
||||
sliceId: string;
|
||||
gateId: string;
|
||||
taskId?: string;
|
||||
verdict: "pass" | "flag" | "omitted";
|
||||
rationale: string;
|
||||
findings?: string;
|
||||
},
|
||||
) => Promise<unknown>;
|
||||
executeSummarySave: (
|
||||
params: {
|
||||
milestone_id: string;
|
||||
|
|
@ -217,6 +287,101 @@ async function handleSliceComplete(
|
|||
return withProjectDir(projectDir, () => executeSliceComplete(args as any, projectDir));
|
||||
}
|
||||
|
||||
async function handleCompleteMilestone(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { executeCompleteMilestone } = await getWorkflowToolExecutors();
|
||||
return withProjectDir(projectDir, () => executeCompleteMilestone(args as any, projectDir));
|
||||
}
|
||||
|
||||
async function handleValidateMilestone(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { executeValidateMilestone } = await getWorkflowToolExecutors();
|
||||
return withProjectDir(projectDir, () => executeValidateMilestone(args as any, projectDir));
|
||||
}
|
||||
|
||||
async function handleReassessRoadmap(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { executeReassessRoadmap } = await getWorkflowToolExecutors();
|
||||
return withProjectDir(projectDir, () => executeReassessRoadmap(args as any, projectDir));
|
||||
}
|
||||
|
||||
async function handleSaveGateResult(
|
||||
projectDir: string,
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> {
|
||||
const { executeSaveGateResult } = await getWorkflowToolExecutors();
|
||||
return withProjectDir(projectDir, () => executeSaveGateResult(args as any));
|
||||
}
|
||||
|
||||
const completeMilestoneSchema = {
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
title: z.string().describe("Milestone title"),
|
||||
oneLiner: z.string().describe("One-sentence summary of what the milestone achieved"),
|
||||
narrative: z.string().describe("Detailed narrative of what happened during the milestone"),
|
||||
verificationPassed: z.boolean().describe("Must be true after milestone verification succeeds"),
|
||||
successCriteriaResults: z.string().optional(),
|
||||
definitionOfDoneResults: z.string().optional(),
|
||||
requirementOutcomes: z.string().optional(),
|
||||
keyDecisions: z.array(z.string()).optional(),
|
||||
keyFiles: z.array(z.string()).optional(),
|
||||
lessonsLearned: z.array(z.string()).optional(),
|
||||
followUps: z.string().optional(),
|
||||
deviations: z.string().optional(),
|
||||
};
|
||||
|
||||
const validateMilestoneSchema = {
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
verdict: z.enum(["pass", "needs-attention", "needs-remediation"]).describe("Validation verdict"),
|
||||
remediationRound: z.number().describe("Remediation round (0 for first validation)"),
|
||||
successCriteriaChecklist: z.string().describe("Markdown checklist of success criteria with evidence"),
|
||||
sliceDeliveryAudit: z.string().describe("Markdown auditing each slice's claimed vs delivered output"),
|
||||
crossSliceIntegration: z.string().describe("Markdown describing cross-slice issues or closure"),
|
||||
requirementCoverage: z.string().describe("Markdown describing requirement coverage and gaps"),
|
||||
verificationClasses: z.string().optional(),
|
||||
verdictRationale: z.string().describe("Why this verdict was chosen"),
|
||||
remediationPlan: z.string().optional(),
|
||||
};
|
||||
|
||||
const roadmapSliceChangeSchema = z.object({
|
||||
sliceId: z.string(),
|
||||
title: z.string(),
|
||||
risk: z.string().optional(),
|
||||
depends: z.array(z.string()).optional(),
|
||||
demo: z.string().optional(),
|
||||
});
|
||||
|
||||
const reassessRoadmapSchema = {
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
completedSliceId: z.string().describe("Slice ID that just completed"),
|
||||
verdict: z.string().describe("Assessment verdict such as roadmap-confirmed or roadmap-adjusted"),
|
||||
assessment: z.string().describe("Assessment text explaining the roadmap decision"),
|
||||
sliceChanges: z.object({
|
||||
modified: z.array(roadmapSliceChangeSchema),
|
||||
added: z.array(roadmapSliceChangeSchema),
|
||||
removed: z.array(z.string()),
|
||||
}).describe("Slice changes to apply"),
|
||||
};
|
||||
|
||||
const saveGateResultSchema = {
|
||||
projectDir: z.string().describe("Absolute path to the project directory"),
|
||||
milestoneId: z.string().describe("Milestone ID (e.g. M001)"),
|
||||
sliceId: z.string().describe("Slice ID (e.g. S01)"),
|
||||
gateId: z.enum(["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"]).describe("Gate ID"),
|
||||
taskId: z.string().optional().describe("Task ID for task-scoped gates"),
|
||||
verdict: z.enum(["pass", "flag", "omitted"]).describe("Gate verdict"),
|
||||
rationale: z.string().describe("One-sentence justification"),
|
||||
findings: z.string().optional().describe("Detailed markdown findings"),
|
||||
};
|
||||
|
||||
export function registerWorkflowTools(server: McpToolServer): void {
|
||||
server.tool(
|
||||
"gsd_plan_milestone",
|
||||
|
|
@ -396,6 +561,76 @@ export function registerWorkflowTools(server: McpToolServer): void {
|
|||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_complete_milestone",
|
||||
"Record a completed milestone to the GSD database and render its SUMMARY.md.",
|
||||
completeMilestoneSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...milestoneArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleCompleteMilestone(projectDir, milestoneArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_milestone_complete",
|
||||
"Alias for gsd_complete_milestone. Record a completed milestone to the GSD database and render its SUMMARY.md.",
|
||||
completeMilestoneSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...milestoneArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleCompleteMilestone(projectDir, milestoneArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_validate_milestone",
|
||||
"Validate a milestone, persist validation results to the GSD database, and render VALIDATION.md.",
|
||||
validateMilestoneSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...validationArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleValidateMilestone(projectDir, validationArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_milestone_validate",
|
||||
"Alias for gsd_validate_milestone. Validate a milestone and render VALIDATION.md.",
|
||||
validateMilestoneSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...validationArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleValidateMilestone(projectDir, validationArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_reassess_roadmap",
|
||||
"Reassess a milestone roadmap after a slice completes, writing ASSESSMENT.md and re-rendering ROADMAP.md.",
|
||||
reassessRoadmapSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...reassessArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleReassessRoadmap(projectDir, reassessArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_roadmap_reassess",
|
||||
"Alias for gsd_reassess_roadmap. Reassess a roadmap after slice completion.",
|
||||
reassessRoadmapSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...reassessArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleReassessRoadmap(projectDir, reassessArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_save_gate_result",
|
||||
"Save a quality gate result to the GSD database.",
|
||||
saveGateResultSchema,
|
||||
async (args: Record<string, unknown>) => {
|
||||
const { projectDir, ...gateArgs } = args as { projectDir: string } & Record<string, unknown>;
|
||||
return handleSaveGateResult(projectDir, gateArgs);
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
"gsd_summary_save",
|
||||
"Save a GSD summary/research/context/assessment artifact to the database and disk.",
|
||||
|
|
|
|||
|
|
@ -9,11 +9,15 @@ import { StringEnum } from "@gsd/pi-ai";
|
|||
import { logError } from "../workflow-logger.js";
|
||||
import { getErrorMessage } from "../error-utils.js";
|
||||
import {
|
||||
executeCompleteMilestone,
|
||||
executePlanMilestone,
|
||||
executePlanSlice,
|
||||
executeReassessRoadmap,
|
||||
executeSaveGateResult,
|
||||
executeSliceComplete,
|
||||
executeSummarySave,
|
||||
executeTaskComplete,
|
||||
executeValidateMilestone,
|
||||
} from "../tools/workflow-tool-executors.js";
|
||||
|
||||
/**
|
||||
|
|
@ -833,42 +837,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
// ─── gsd_complete_milestone ────────────────────────────────────────────
|
||||
|
||||
const milestoneCompleteExecute = 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 milestone." }],
|
||||
details: { operation: "complete_milestone", error: "db_unavailable" } as any,
|
||||
};
|
||||
}
|
||||
try {
|
||||
// ── Input sanitization: normalize markdown parameters (#3013) ──────
|
||||
const { sanitizeCompleteMilestoneParams } = await import("./sanitize-complete-milestone.js");
|
||||
const sanitized = sanitizeCompleteMilestoneParams(params);
|
||||
|
||||
const { handleCompleteMilestone } = await import("../tools/complete-milestone.js");
|
||||
const result = await handleCompleteMilestone(sanitized, process.cwd());
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error completing milestone: ${result.error}` }],
|
||||
details: { operation: "complete_milestone", error: result.error } as any,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Completed milestone ${result.milestoneId}. Summary written to ${result.summaryPath}` }],
|
||||
details: {
|
||||
operation: "complete_milestone",
|
||||
milestoneId: result.milestoneId,
|
||||
summaryPath: result.summaryPath,
|
||||
} as any,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `complete_milestone tool failed: ${msg}`, { tool: "gsd_complete_milestone", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error completing milestone: ${msg}` }],
|
||||
details: { operation: "complete_milestone", error: msg } as any,
|
||||
};
|
||||
}
|
||||
return executeCompleteMilestone(params, process.cwd());
|
||||
};
|
||||
|
||||
const milestoneCompleteTool = {
|
||||
|
|
@ -910,39 +879,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
// ─── gsd_validate_milestone (gsd_milestone_validate alias) ─────────────
|
||||
|
||||
const milestoneValidateExecute = 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 validate milestone." }],
|
||||
details: { operation: "validate_milestone", error: "db_unavailable" } as any,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { handleValidateMilestone } = await import("../tools/validate-milestone.js");
|
||||
const result = await handleValidateMilestone(params, process.cwd());
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error validating milestone: ${result.error}` }],
|
||||
details: { operation: "validate_milestone", error: result.error } as any,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Validated milestone ${result.milestoneId} — verdict: ${result.verdict}. Written to ${result.validationPath}` }],
|
||||
details: {
|
||||
operation: "validate_milestone",
|
||||
milestoneId: result.milestoneId,
|
||||
verdict: result.verdict,
|
||||
validationPath: result.validationPath,
|
||||
} as any,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `validate_milestone tool failed: ${msg}`, { tool: "gsd_validate_milestone", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error validating milestone: ${msg}` }],
|
||||
details: { operation: "validate_milestone", error: msg } as any,
|
||||
};
|
||||
}
|
||||
return executeValidateMilestone(params, process.cwd());
|
||||
};
|
||||
|
||||
const milestoneValidateTool = {
|
||||
|
|
@ -1059,40 +996,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
// ─── gsd_reassess_roadmap (gsd_roadmap_reassess alias) ─────────────────
|
||||
|
||||
const reassessRoadmapExecute = 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 reassess roadmap." }],
|
||||
details: { operation: "reassess_roadmap", error: "db_unavailable" } as any,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { handleReassessRoadmap } = await import("../tools/reassess-roadmap.js");
|
||||
const result = await handleReassessRoadmap(params, process.cwd());
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error reassessing roadmap: ${result.error}` }],
|
||||
details: { operation: "reassess_roadmap", error: result.error } as any,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Reassessed roadmap for milestone ${result.milestoneId} after ${result.completedSliceId}` }],
|
||||
details: {
|
||||
operation: "reassess_roadmap",
|
||||
milestoneId: result.milestoneId,
|
||||
completedSliceId: result.completedSliceId,
|
||||
assessmentPath: result.assessmentPath,
|
||||
roadmapPath: result.roadmapPath,
|
||||
} as any,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `reassess_roadmap tool failed: ${msg}`, { tool: "gsd_reassess_roadmap", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error reassessing roadmap: ${msg}` }],
|
||||
details: { operation: "reassess_roadmap", error: msg } as any,
|
||||
};
|
||||
}
|
||||
return executeReassessRoadmap(params, process.cwd());
|
||||
};
|
||||
|
||||
const reassessRoadmapTool = {
|
||||
|
|
@ -1147,52 +1051,7 @@ export function registerDbTools(pi: ExtensionAPI): void {
|
|||
// ─── gsd_save_gate_result ──────────────────────────────────────────────
|
||||
|
||||
const saveGateResultExecute = 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." }],
|
||||
details: { operation: "save_gate_result", error: "db_unavailable" } as any,
|
||||
};
|
||||
}
|
||||
const validGates = ["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"];
|
||||
if (!validGates.includes(params.gateId)) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error: Invalid gateId "${params.gateId}". Must be one of: ${validGates.join(", ")}` }],
|
||||
details: { operation: "save_gate_result", error: "invalid_gate_id" } as any,
|
||||
};
|
||||
}
|
||||
const validVerdicts = ["pass", "flag", "omitted"];
|
||||
if (!validVerdicts.includes(params.verdict)) {
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error: Invalid verdict "${params.verdict}". Must be one of: ${validVerdicts.join(", ")}` }],
|
||||
details: { operation: "save_gate_result", error: "invalid_verdict" } as any,
|
||||
};
|
||||
}
|
||||
try {
|
||||
const { saveGateResult } = await import("../gsd-db.js");
|
||||
const { invalidateStateCache } = await import("../state.js");
|
||||
saveGateResult({
|
||||
milestoneId: params.milestoneId,
|
||||
sliceId: params.sliceId,
|
||||
gateId: params.gateId,
|
||||
taskId: params.taskId ?? "",
|
||||
verdict: params.verdict,
|
||||
rationale: params.rationale,
|
||||
findings: params.findings ?? "",
|
||||
});
|
||||
invalidateStateCache();
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Gate ${params.gateId} result saved: verdict=${params.verdict}` }],
|
||||
details: { operation: "save_gate_result", gateId: params.gateId, verdict: params.verdict } as any,
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `gsd_save_gate_result failed: ${msg}`, { tool: "gsd_save_gate_result", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text" as const, text: `Error saving gate result: ${msg}` }],
|
||||
details: { operation: "save_gate_result", error: msg } as any,
|
||||
};
|
||||
}
|
||||
return executeSaveGateResult(params);
|
||||
};
|
||||
|
||||
const saveGateResultTool = {
|
||||
|
|
|
|||
|
|
@ -172,10 +172,61 @@ test("transport compatibility now allows complete-slice over workflow MCP surfac
|
|||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
|
||||
test("transport compatibility now allows reassess-roadmap over workflow MCP surface", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_complete_milestone"],
|
||||
["gsd_milestone_status", "gsd_reassess_roadmap"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
surface: "auto-mode",
|
||||
unitType: "reassess-roadmap",
|
||||
authMode: "externalCli",
|
||||
baseUrl: "local://claude-code",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility now allows gate-evaluate over workflow MCP surface", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_save_gate_result"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
surface: "auto-mode",
|
||||
unitType: "gate-evaluate",
|
||||
authMode: "externalCli",
|
||||
baseUrl: "local://claude-code",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility now allows validate-milestone over workflow MCP surface", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_milestone_status", "gsd_validate_milestone"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
surface: "auto-mode",
|
||||
unitType: "validate-milestone",
|
||||
authMode: "externalCli",
|
||||
baseUrl: "local://claude-code",
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility now allows complete-milestone over workflow MCP surface", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_milestone_status", "gsd_complete_milestone"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
|
|
@ -186,7 +237,24 @@ test("transport compatibility still blocks units whose MCP tools are not exposed
|
|||
},
|
||||
);
|
||||
|
||||
assert.match(error ?? "", /requires gsd_complete_milestone/);
|
||||
assert.equal(error, null);
|
||||
});
|
||||
|
||||
test("transport compatibility still blocks units whose MCP tools are not exposed", () => {
|
||||
const error = getWorkflowTransportSupportError(
|
||||
"claude-code",
|
||||
["gsd_replan_slice"],
|
||||
{
|
||||
projectRoot: "/tmp/project",
|
||||
env: { GSD_WORKFLOW_MCP_COMMAND: "node" },
|
||||
surface: "auto-mode",
|
||||
unitType: "replan-slice",
|
||||
authMode: "externalCli",
|
||||
baseUrl: "local://claude-code",
|
||||
},
|
||||
);
|
||||
|
||||
assert.match(error ?? "", /requires gsd_replan_slice/);
|
||||
assert.match(error ?? "", /currently exposes only/);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,13 @@ import {
|
|||
openDatabase,
|
||||
closeDatabase,
|
||||
_getAdapter,
|
||||
insertGateRow,
|
||||
} from "../gsd-db.ts";
|
||||
import {
|
||||
executeCompleteMilestone,
|
||||
executeValidateMilestone,
|
||||
executeReassessRoadmap,
|
||||
executeSaveGateResult,
|
||||
executeSummarySave,
|
||||
executeTaskComplete,
|
||||
executeMilestoneStatus,
|
||||
|
|
@ -288,3 +293,215 @@ test("executeSliceComplete coerces string enrichment entries and writes summary/
|
|||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("executeValidateMilestone persists validation artifact and gate records", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
openTestDb(base);
|
||||
seedMilestone("M002", "Milestone Two");
|
||||
seedSlice("M002", "S02", "complete");
|
||||
|
||||
const result = await inProjectDir(base, () => executeValidateMilestone({
|
||||
milestoneId: "M002",
|
||||
verdict: "pass",
|
||||
remediationRound: 0,
|
||||
successCriteriaChecklist: "- [x] Works",
|
||||
sliceDeliveryAudit: "| Slice | Result |\n| --- | --- |\n| S02 | pass |",
|
||||
crossSliceIntegration: "No cross-slice issues.",
|
||||
requirementCoverage: "All requirements covered.",
|
||||
verdictRationale: "Everything passed.",
|
||||
}, base));
|
||||
|
||||
assert.equal(result.details.operation, "validate_milestone");
|
||||
const validationPath = String(result.details.validationPath);
|
||||
assert.ok(existsSync(validationPath), "validation file should be written to disk");
|
||||
|
||||
const db = _getAdapter();
|
||||
const gates = db!.prepare(
|
||||
"SELECT gate_id, verdict FROM quality_gates WHERE milestone_id = ? ORDER BY gate_id",
|
||||
).all("M002") as Array<Record<string, unknown>>;
|
||||
assert.ok(gates.length > 0, "validation should seed milestone quality gates");
|
||||
assert.equal(gates[0]["verdict"], "pass");
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("executeCompleteMilestone sanitizes raw params and writes milestone summary", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
openTestDb(base);
|
||||
seedMilestone("M003", "Milestone Three");
|
||||
seedSlice("M003", "S03", "complete");
|
||||
writeRoadmap(base, "M003", ["S03"]);
|
||||
const db = _getAdapter();
|
||||
db!.prepare(
|
||||
"INSERT OR REPLACE INTO tasks (milestone_id, slice_id, id, title, status) VALUES (?, ?, ?, ?, ?)",
|
||||
).run("M003", "S03", "T03", "Task T03", "complete");
|
||||
|
||||
const result = await inProjectDir(base, () => executeCompleteMilestone({
|
||||
milestoneId: "M003",
|
||||
title: "Milestone Three",
|
||||
oneLiner: "Completed milestone",
|
||||
narrative: "Everything shipped.",
|
||||
verificationPassed: "true",
|
||||
keyDecisions: ["shared executor path"],
|
||||
lessonsLearned: ["MCP transport stays generic"],
|
||||
}, base));
|
||||
|
||||
assert.equal(result.details.operation, "complete_milestone");
|
||||
const summaryPath = String(result.details.summaryPath);
|
||||
assert.ok(existsSync(summaryPath), "milestone summary should be written to disk");
|
||||
assert.match(readFileSync(summaryPath, "utf-8"), /shared executor path/);
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("executeReassessRoadmap writes assessment and updates roadmap projection", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
openTestDb(base);
|
||||
await inProjectDir(base, () => executePlanMilestone({
|
||||
milestoneId: "M004",
|
||||
title: "Milestone Four",
|
||||
vision: "Exercise roadmap reassessment.",
|
||||
slices: [
|
||||
{
|
||||
sliceId: "S04",
|
||||
title: "Completed slice",
|
||||
risk: "medium",
|
||||
depends: [],
|
||||
demo: "Completed slice works",
|
||||
goal: "Complete the first slice.",
|
||||
successCriteria: "S04 is complete.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Baseline flow is wired.",
|
||||
observabilityImpact: "Executor test covers reassessment.",
|
||||
},
|
||||
{
|
||||
sliceId: "S05",
|
||||
title: "Follow-up slice",
|
||||
risk: "medium",
|
||||
depends: ["S04"],
|
||||
demo: "Follow-up slice is adjusted",
|
||||
goal: "Handle the follow-up work.",
|
||||
successCriteria: "Roadmap gets updated.",
|
||||
proofLevel: "integration",
|
||||
integrationClosure: "Downstream work stays aligned.",
|
||||
observabilityImpact: "Assessment artifact is rendered.",
|
||||
},
|
||||
],
|
||||
}, base));
|
||||
await inProjectDir(base, () => executePlanSlice({
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
goal: "Complete the first slice.",
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T04",
|
||||
title: "Finish slice",
|
||||
description: "Close the completed slice.",
|
||||
estimate: "5m",
|
||||
files: ["src/file.ts"],
|
||||
verify: "node --test",
|
||||
inputs: ["M004-ROADMAP.md"],
|
||||
expectedOutput: ["S04-SUMMARY.md", "S04-UAT.md"],
|
||||
},
|
||||
],
|
||||
}, base));
|
||||
await inProjectDir(base, () => executeTaskComplete({
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
taskId: "T04",
|
||||
oneLiner: "Completed task",
|
||||
narrative: "Task finished.",
|
||||
verification: "node --test",
|
||||
}, base));
|
||||
await inProjectDir(base, () => executeSliceComplete({
|
||||
milestoneId: "M004",
|
||||
sliceId: "S04",
|
||||
sliceTitle: "Completed slice",
|
||||
oneLiner: "Completed slice",
|
||||
narrative: "Slice finished.",
|
||||
verification: "node --test",
|
||||
uatContent: "## UAT\n\nPASS",
|
||||
}, base));
|
||||
|
||||
const result = await inProjectDir(base, () => executeReassessRoadmap({
|
||||
milestoneId: "M004",
|
||||
completedSliceId: "S04",
|
||||
verdict: "roadmap-adjusted",
|
||||
assessment: "Added a remediation slice.",
|
||||
sliceChanges: {
|
||||
modified: [
|
||||
{
|
||||
sliceId: "S05",
|
||||
title: "Adjusted follow-up slice",
|
||||
risk: "high",
|
||||
depends: ["S04"],
|
||||
demo: "Adjusted follow-up demo",
|
||||
},
|
||||
],
|
||||
added: [
|
||||
{
|
||||
sliceId: "S06",
|
||||
title: "Remediation slice",
|
||||
risk: "medium",
|
||||
depends: ["S05"],
|
||||
demo: "Remediation slice demo",
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
},
|
||||
}, base));
|
||||
|
||||
assert.equal(result.details.operation, "reassess_roadmap");
|
||||
const assessmentPath = String(result.details.assessmentPath);
|
||||
const roadmapPath = String(result.details.roadmapPath);
|
||||
assert.ok(existsSync(assessmentPath), "assessment file should be written");
|
||||
assert.ok(existsSync(roadmapPath), "roadmap should be re-rendered");
|
||||
assert.match(readFileSync(roadmapPath, "utf-8"), /S06/);
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
||||
test("executeSaveGateResult validates inputs and persists verdicts", async () => {
|
||||
const base = makeTmpBase();
|
||||
try {
|
||||
openTestDb(base);
|
||||
seedMilestone("M005", "Milestone Five");
|
||||
seedSlice("M005", "S05", "pending");
|
||||
insertGateRow({
|
||||
milestoneId: "M005",
|
||||
sliceId: "S05",
|
||||
gateId: "Q3",
|
||||
scope: "slice",
|
||||
});
|
||||
|
||||
const result = await inProjectDir(base, () => executeSaveGateResult({
|
||||
milestoneId: "M005",
|
||||
sliceId: "S05",
|
||||
gateId: "Q3",
|
||||
verdict: "pass",
|
||||
rationale: "Looks good.",
|
||||
findings: "No issues found.",
|
||||
}));
|
||||
|
||||
assert.equal(result.details.operation, "save_gate_result");
|
||||
const db = _getAdapter();
|
||||
const row = db!.prepare(
|
||||
"SELECT status, verdict, rationale FROM quality_gates WHERE milestone_id = ? AND slice_id = ? AND gate_id = ? AND task_id = ''",
|
||||
).get("M005", "S05", "Q3") as Record<string, unknown> | undefined;
|
||||
assert.equal(row?.status, "complete");
|
||||
assert.equal(row?.verdict, "pass");
|
||||
assert.equal(row?.rationale, "Looks good.");
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
import { ensureDbOpen } from "../bootstrap/dynamic-tools.js";
|
||||
import { sanitizeCompleteMilestoneParams } from "../bootstrap/sanitize-complete-milestone.js";
|
||||
import { shouldBlockContextArtifactSave } from "../bootstrap/write-gate.js";
|
||||
import {
|
||||
getMilestone,
|
||||
getSliceStatusSummary,
|
||||
getSliceTaskCounts,
|
||||
_getAdapter,
|
||||
saveGateResult,
|
||||
} from "../gsd-db.js";
|
||||
import { saveArtifactToDb } from "../db-writer.js";
|
||||
import type { CompleteMilestoneParams } from "./complete-milestone.js";
|
||||
import { handleCompleteMilestone } from "./complete-milestone.js";
|
||||
import { handleCompleteTask } from "./complete-task.js";
|
||||
import type { CompleteSliceParams } from "../types.js";
|
||||
import { handleCompleteSlice } from "./complete-slice.js";
|
||||
|
|
@ -14,7 +18,12 @@ import type { PlanMilestoneParams } from "./plan-milestone.js";
|
|||
import { handlePlanMilestone } from "./plan-milestone.js";
|
||||
import type { PlanSliceParams } from "./plan-slice.js";
|
||||
import { handlePlanSlice } from "./plan-slice.js";
|
||||
import type { ReassessRoadmapParams } from "./reassess-roadmap.js";
|
||||
import { handleReassessRoadmap } from "./reassess-roadmap.js";
|
||||
import type { ValidateMilestoneParams } from "./validate-milestone.js";
|
||||
import { handleValidateMilestone } from "./validate-milestone.js";
|
||||
import { logError, logWarning } from "../workflow-logger.js";
|
||||
import { invalidateStateCache } from "../state.js";
|
||||
|
||||
export const SUPPORTED_SUMMARY_ARTIFACT_TYPES = ["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT", "CONTEXT-DRAFT"] as const;
|
||||
|
||||
|
|
@ -124,9 +133,22 @@ export interface TaskCompleteParams {
|
|||
verificationEvidence?: VerificationEvidenceInput[];
|
||||
}
|
||||
|
||||
export type CompleteMilestoneExecutorParams = Partial<CompleteMilestoneParams> & Record<string, unknown>;
|
||||
export type SliceCompleteExecutorParams = CompleteSliceParams;
|
||||
export type PlanMilestoneExecutorParams = PlanMilestoneParams;
|
||||
export type PlanSliceExecutorParams = PlanSliceParams;
|
||||
export type ValidateMilestoneExecutorParams = ValidateMilestoneParams;
|
||||
export type ReassessRoadmapExecutorParams = ReassessRoadmapParams;
|
||||
|
||||
export interface SaveGateResultParams {
|
||||
milestoneId: string;
|
||||
sliceId: string;
|
||||
gateId: string;
|
||||
taskId?: string;
|
||||
verdict: "pass" | "flag" | "omitted";
|
||||
rationale: string;
|
||||
findings?: string;
|
||||
}
|
||||
|
||||
export async function executeTaskComplete(
|
||||
params: TaskCompleteParams,
|
||||
|
|
@ -253,6 +275,173 @@ export async function executeSliceComplete(
|
|||
}
|
||||
}
|
||||
|
||||
export async function executeCompleteMilestone(
|
||||
params: CompleteMilestoneExecutorParams,
|
||||
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 milestone." }],
|
||||
details: { operation: "complete_milestone", error: "db_unavailable" },
|
||||
};
|
||||
}
|
||||
try {
|
||||
const sanitized = sanitizeCompleteMilestoneParams(params);
|
||||
const result = await handleCompleteMilestone(sanitized, basePath);
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error completing milestone: ${result.error}` }],
|
||||
details: { operation: "complete_milestone", error: result.error },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Completed milestone ${result.milestoneId}. Summary written to ${result.summaryPath}` }],
|
||||
details: {
|
||||
operation: "complete_milestone",
|
||||
milestoneId: result.milestoneId,
|
||||
summaryPath: result.summaryPath,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `complete_milestone tool failed: ${msg}`, { tool: "gsd_complete_milestone", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Error completing milestone: ${msg}` }],
|
||||
details: { operation: "complete_milestone", error: msg },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeValidateMilestone(
|
||||
params: ValidateMilestoneExecutorParams,
|
||||
basePath: string = process.cwd(),
|
||||
): Promise<ToolExecutionResult> {
|
||||
const dbAvailable = await ensureDbOpen();
|
||||
if (!dbAvailable) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: GSD database is not available. Cannot validate milestone." }],
|
||||
details: { operation: "validate_milestone", error: "db_unavailable" },
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await handleValidateMilestone(params, basePath);
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error validating milestone: ${result.error}` }],
|
||||
details: { operation: "validate_milestone", error: result.error },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Validated milestone ${result.milestoneId} — verdict: ${result.verdict}. Written to ${result.validationPath}` }],
|
||||
details: {
|
||||
operation: "validate_milestone",
|
||||
milestoneId: result.milestoneId,
|
||||
verdict: result.verdict,
|
||||
validationPath: result.validationPath,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `validate_milestone tool failed: ${msg}`, { tool: "gsd_validate_milestone", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Error validating milestone: ${msg}` }],
|
||||
details: { operation: "validate_milestone", error: msg },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeReassessRoadmap(
|
||||
params: ReassessRoadmapExecutorParams,
|
||||
basePath: string = process.cwd(),
|
||||
): Promise<ToolExecutionResult> {
|
||||
const dbAvailable = await ensureDbOpen();
|
||||
if (!dbAvailable) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: GSD database is not available. Cannot reassess roadmap." }],
|
||||
details: { operation: "reassess_roadmap", error: "db_unavailable" },
|
||||
};
|
||||
}
|
||||
try {
|
||||
const result = await handleReassessRoadmap(params, basePath);
|
||||
if ("error" in result) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error reassessing roadmap: ${result.error}` }],
|
||||
details: { operation: "reassess_roadmap", error: result.error },
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: [{ type: "text", text: `Reassessed roadmap for milestone ${result.milestoneId} after ${result.completedSliceId}` }],
|
||||
details: {
|
||||
operation: "reassess_roadmap",
|
||||
milestoneId: result.milestoneId,
|
||||
completedSliceId: result.completedSliceId,
|
||||
assessmentPath: result.assessmentPath,
|
||||
roadmapPath: result.roadmapPath,
|
||||
},
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `reassess_roadmap tool failed: ${msg}`, { tool: "gsd_reassess_roadmap", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Error reassessing roadmap: ${msg}` }],
|
||||
details: { operation: "reassess_roadmap", error: msg },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeSaveGateResult(
|
||||
params: SaveGateResultParams,
|
||||
): Promise<ToolExecutionResult> {
|
||||
const dbAvailable = await ensureDbOpen();
|
||||
if (!dbAvailable) {
|
||||
return {
|
||||
content: [{ type: "text", text: "Error: GSD database is not available." }],
|
||||
details: { operation: "save_gate_result", error: "db_unavailable" },
|
||||
};
|
||||
}
|
||||
|
||||
const validGates = ["Q3", "Q4", "Q5", "Q6", "Q7", "Q8"];
|
||||
if (!validGates.includes(params.gateId)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: Invalid gateId "${params.gateId}". Must be one of: ${validGates.join(", ")}` }],
|
||||
details: { operation: "save_gate_result", error: "invalid_gate_id" },
|
||||
};
|
||||
}
|
||||
|
||||
const validVerdicts = ["pass", "flag", "omitted"];
|
||||
if (!validVerdicts.includes(params.verdict)) {
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: Invalid verdict "${params.verdict}". Must be one of: ${validVerdicts.join(", ")}` }],
|
||||
details: { operation: "save_gate_result", error: "invalid_verdict" },
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
saveGateResult({
|
||||
milestoneId: params.milestoneId,
|
||||
sliceId: params.sliceId,
|
||||
gateId: params.gateId,
|
||||
taskId: params.taskId ?? "",
|
||||
verdict: params.verdict,
|
||||
rationale: params.rationale,
|
||||
findings: params.findings ?? "",
|
||||
});
|
||||
invalidateStateCache();
|
||||
return {
|
||||
content: [{ type: "text", text: `Gate ${params.gateId} result saved: verdict=${params.verdict}` }],
|
||||
details: { operation: "save_gate_result", gateId: params.gateId, verdict: params.verdict },
|
||||
};
|
||||
} catch (err) {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
logError("tool", `gsd_save_gate_result failed: ${msg}`, { tool: "gsd_save_gate_result", error: String(err) });
|
||||
return {
|
||||
content: [{ type: "text", text: `Error saving gate result: ${msg}` }],
|
||||
details: { operation: "save_gate_result", error: msg },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function executePlanMilestone(
|
||||
params: PlanMilestoneExecutorParams,
|
||||
basePath: string = process.cwd(),
|
||||
|
|
|
|||
|
|
@ -20,14 +20,21 @@ export interface WorkflowCapabilityOptions {
|
|||
}
|
||||
|
||||
const MCP_WORKFLOW_TOOL_SURFACE = new Set([
|
||||
"gsd_complete_milestone",
|
||||
"gsd_complete_task",
|
||||
"gsd_complete_slice",
|
||||
"gsd_milestone_complete",
|
||||
"gsd_milestone_status",
|
||||
"gsd_milestone_validate",
|
||||
"gsd_plan_milestone",
|
||||
"gsd_plan_slice",
|
||||
"gsd_reassess_roadmap",
|
||||
"gsd_roadmap_reassess",
|
||||
"gsd_save_gate_result",
|
||||
"gsd_slice_complete",
|
||||
"gsd_summary_save",
|
||||
"gsd_task_complete",
|
||||
"gsd_validate_milestone",
|
||||
]);
|
||||
|
||||
function parseLookupOutput(output: Buffer | string): string {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue