diff --git a/src/resources/extensions/gsd/bootstrap/db-tools.ts b/src/resources/extensions/gsd/bootstrap/db-tools.ts index 0d547c9aa..3f6f9d998 100644 --- a/src/resources/extensions/gsd/bootstrap/db-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/db-tools.ts @@ -501,28 +501,10 @@ export function registerDbTools(pi: ExtensionAPI): void { "Use the canonical name gsd_plan_milestone; gsd_milestone_plan is only an alias.", ], parameters: Type.Object({ + // ── Core identification + content (required) ────────────────────── milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), title: Type.String({ description: "Milestone title" }), - status: Type.Optional(Type.String({ description: "Milestone status (defaults to active)" })), - dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Milestone dependencies" })), vision: Type.String({ description: "Milestone vision" }), - successCriteria: Type.Array(Type.String(), { description: "Top-level success criteria bullets" }), - keyRisks: Type.Array(Type.Object({ - risk: Type.String({ description: "Risk statement" }), - whyItMatters: Type.String({ description: "Why the risk matters" }), - }), { description: "Structured risk entries" }), - proofStrategy: Type.Array(Type.Object({ - riskOrUnknown: Type.String({ description: "Risk or unknown to retire" }), - retireIn: Type.String({ description: "Where it will be retired" }), - whatWillBeProven: Type.String({ description: "What proof will be produced" }), - }), { description: "Structured proof strategy entries" }), - verificationContract: Type.String({ description: "Verification contract text" }), - verificationIntegration: Type.String({ description: "Integration verification text" }), - verificationOperational: Type.String({ description: "Operational verification text" }), - verificationUat: Type.String({ description: "UAT verification text" }), - definitionOfDone: Type.Array(Type.String(), { description: "Definition of done bullets" }), - requirementCoverage: Type.String({ description: "Requirement coverage text" }), - boundaryMapMarkdown: Type.String({ description: "Boundary map markdown block" }), slices: Type.Array(Type.Object({ sliceId: Type.String({ description: "Slice ID (e.g. S01)" }), title: Type.String({ description: "Slice title" }), @@ -535,6 +517,26 @@ export function registerDbTools(pi: ExtensionAPI): void { integrationClosure: Type.String({ description: "Slice integration closure" }), observabilityImpact: Type.String({ description: "Slice observability impact" }), }), { description: "Planned slices for the milestone" }), + // ── Enrichment metadata (optional — defaults to empty) ──────────── + status: Type.Optional(Type.String({ description: "Milestone status (defaults to active)" })), + dependsOn: Type.Optional(Type.Array(Type.String(), { description: "Milestone dependencies" })), + successCriteria: Type.Optional(Type.Array(Type.String(), { description: "Top-level success criteria bullets" })), + keyRisks: Type.Optional(Type.Array(Type.Object({ + risk: Type.String({ description: "Risk statement" }), + whyItMatters: Type.String({ description: "Why the risk matters" }), + }), { description: "Structured risk entries" })), + proofStrategy: Type.Optional(Type.Array(Type.Object({ + riskOrUnknown: Type.String({ description: "Risk or unknown to retire" }), + retireIn: Type.String({ description: "Where it will be retired" }), + whatWillBeProven: Type.String({ description: "What proof will be produced" }), + }), { description: "Structured proof strategy entries" })), + verificationContract: Type.Optional(Type.String({ description: "Verification contract text" })), + verificationIntegration: Type.Optional(Type.String({ description: "Integration verification text" })), + verificationOperational: Type.Optional(Type.String({ description: "Operational verification text" })), + verificationUat: Type.Optional(Type.String({ description: "UAT verification text" })), + definitionOfDone: Type.Optional(Type.Array(Type.String(), { description: "Definition of done bullets" })), + requirementCoverage: Type.Optional(Type.String({ description: "Requirement coverage text" })), + boundaryMapMarkdown: Type.Optional(Type.String({ description: "Boundary map markdown block" })), }), execute: planMilestoneExecute, }; @@ -594,13 +596,10 @@ export function registerDbTools(pi: ExtensionAPI): void { "Use the canonical name gsd_plan_slice; gsd_slice_plan is only an alias.", ], parameters: Type.Object({ + // ── Core identification + content (required) ────────────────────── milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), sliceId: Type.String({ description: "Slice ID (e.g. S01)" }), goal: Type.String({ description: "Slice goal" }), - successCriteria: Type.String({ description: "Slice success criteria block" }), - proofLevel: Type.String({ description: "Slice proof level" }), - integrationClosure: Type.String({ description: "Slice integration closure" }), - observabilityImpact: Type.String({ description: "Slice observability impact" }), tasks: Type.Array(Type.Object({ taskId: Type.String({ description: "Task ID (e.g. T01)" }), title: Type.String({ description: "Task title" }), @@ -612,6 +611,11 @@ export function registerDbTools(pi: ExtensionAPI): void { expectedOutput: Type.Array(Type.String(), { description: "Expected output files or artifacts" }), observabilityImpact: Type.Optional(Type.String({ description: "Task observability impact" })), }), { description: "Planned tasks for the slice" }), + // ── Enrichment metadata (optional — defaults to empty) ──────────── + successCriteria: Type.Optional(Type.String({ description: "Slice success criteria block" })), + proofLevel: Type.Optional(Type.String({ description: "Slice proof level" })), + integrationClosure: Type.Optional(Type.String({ description: "Slice integration closure" })), + observabilityImpact: Type.Optional(Type.String({ description: "Slice observability impact" })), }), execute: planSliceExecute, }; @@ -743,18 +747,20 @@ export function registerDbTools(pi: ExtensionAPI): void { "Idempotent — calling with the same params twice will upsert (INSERT OR REPLACE) without error.", ], parameters: Type.Object({ + // ── Core identification + content (required) ────────────────────── taskId: Type.String({ description: "Task ID (e.g. T01)" }), sliceId: Type.String({ description: "Slice ID (e.g. S01)" }), milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), oneLiner: Type.String({ description: "One-line summary of what was accomplished" }), narrative: Type.String({ description: "Detailed narrative of what happened during the task" }), verification: Type.String({ description: "What was verified and how — commands run, tests passed, behavior confirmed" }), - deviations: Type.String({ description: "Deviations from the task plan, or 'None.'" }), - knownIssues: Type.String({ description: "Known issues discovered but not fixed, or 'None.'" }), - keyFiles: Type.Array(Type.String(), { description: "List of key files created or modified" }), - keyDecisions: Type.Array(Type.String(), { description: "List of key decisions made during this task" }), - blockerDiscovered: Type.Boolean({ description: "Whether a plan-invalidating blocker was discovered" }), - verificationEvidence: Type.Array( + // ── Enrichment metadata (optional — defaults to empty) ──────────── + deviations: Type.Optional(Type.String({ description: "Deviations from the task plan, or 'None.'" })), + knownIssues: Type.Optional(Type.String({ description: "Known issues discovered but not fixed, or 'None.'" })), + keyFiles: Type.Optional(Type.Array(Type.String(), { description: "List of key files created or modified" })), + keyDecisions: Type.Optional(Type.Array(Type.String(), { description: "List of key decisions made during this task" })), + blockerDiscovered: Type.Optional(Type.Boolean({ description: "Whether a plan-invalidating blocker was discovered" })), + verificationEvidence: Type.Optional(Type.Array( Type.Object({ command: Type.String({ description: "Verification command that was run" }), exitCode: Type.Number({ description: "Exit code of the command" }), @@ -762,7 +768,7 @@ export function registerDbTools(pi: ExtensionAPI): void { durationMs: Type.Number({ description: "Duration of the command in milliseconds" }), }), { description: "Array of verification evidence entries" }, - ), + )), }), execute: taskCompleteExecute, }; @@ -823,59 +829,61 @@ export function registerDbTools(pi: ExtensionAPI): void { "Idempotent — calling with the same params twice will not crash.", ], parameters: Type.Object({ + // ── Core identification + content (required) ────────────────────── sliceId: Type.String({ description: "Slice ID (e.g. S01)" }), milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), sliceTitle: Type.String({ description: "Title of the slice" }), oneLiner: Type.String({ description: "One-line summary of what the slice accomplished" }), narrative: Type.String({ description: "Detailed narrative of what happened across all tasks" }), verification: Type.String({ description: "What was verified across all tasks" }), - deviations: Type.String({ description: "Deviations from the slice plan, or 'None.'" }), - knownLimitations: Type.String({ description: "Known limitations or gaps, or 'None.'" }), - followUps: Type.String({ description: "Follow-up work discovered during execution, or 'None.'" }), - keyFiles: Type.Array(Type.String(), { description: "Key files created or modified" }), - keyDecisions: Type.Array(Type.String(), { description: "Key decisions made during this slice" }), - patternsEstablished: Type.Array(Type.String(), { description: "Patterns established by this slice" }), - observabilitySurfaces: Type.Array(Type.String(), { description: "Observability surfaces added" }), - provides: Type.Array(Type.String(), { description: "What this slice provides to downstream slices" }), - requirementsSurfaced: Type.Array(Type.String(), { description: "New requirements surfaced" }), - drillDownPaths: Type.Array(Type.String(), { description: "Paths to task summaries for drill-down" }), - affects: Type.Array(Type.String(), { description: "Downstream slices affected" }), - requirementsAdvanced: Type.Array( + uatContent: Type.String({ description: "UAT test content (markdown body)" }), + // ── Enrichment metadata (optional — defaults to empty) ──────────── + deviations: Type.Optional(Type.String({ description: "Deviations from the slice plan, or 'None.'" })), + knownLimitations: Type.Optional(Type.String({ description: "Known limitations or gaps, or 'None.'" })), + followUps: Type.Optional(Type.String({ description: "Follow-up work discovered during execution, or 'None.'" })), + keyFiles: Type.Optional(Type.Array(Type.String(), { description: "Key files created or modified" })), + keyDecisions: Type.Optional(Type.Array(Type.String(), { description: "Key decisions made during this slice" })), + patternsEstablished: Type.Optional(Type.Array(Type.String(), { description: "Patterns established by this slice" })), + observabilitySurfaces: Type.Optional(Type.Array(Type.String(), { description: "Observability surfaces added" })), + provides: Type.Optional(Type.Array(Type.String(), { description: "What this slice provides to downstream slices" })), + requirementsSurfaced: Type.Optional(Type.Array(Type.String(), { description: "New requirements surfaced" })), + drillDownPaths: Type.Optional(Type.Array(Type.String(), { description: "Paths to task summaries for drill-down" })), + affects: Type.Optional(Type.Array(Type.String(), { description: "Downstream slices affected" })), + requirementsAdvanced: Type.Optional(Type.Array( Type.Object({ id: Type.String({ description: "Requirement ID" }), how: Type.String({ description: "How it was advanced" }), }), { description: "Requirements advanced by this slice" }, - ), - requirementsValidated: Type.Array( + )), + requirementsValidated: Type.Optional(Type.Array( Type.Object({ id: Type.String({ description: "Requirement ID" }), proof: Type.String({ description: "What proof validates it" }), }), { description: "Requirements validated by this slice" }, - ), - requirementsInvalidated: Type.Array( + )), + requirementsInvalidated: Type.Optional(Type.Array( Type.Object({ id: Type.String({ description: "Requirement ID" }), what: Type.String({ description: "What changed" }), }), { description: "Requirements invalidated or re-scoped" }, - ), - filesModified: Type.Array( + )), + filesModified: Type.Optional(Type.Array( Type.Object({ path: Type.String({ description: "File path" }), description: Type.String({ description: "What changed" }), }), { description: "Files modified with descriptions" }, - ), - requires: Type.Array( + )), + requires: Type.Optional(Type.Array( Type.Object({ slice: Type.String({ description: "Dependency slice ID" }), provides: Type.String({ description: "What was consumed from it" }), }), { description: "Upstream slice dependencies consumed" }, - ), - uatContent: Type.String({ description: "UAT test content (markdown body)" }), + )), }), execute: sliceCompleteExecute, }; @@ -1016,19 +1024,21 @@ export function registerDbTools(pi: ExtensionAPI): void { "On success, returns summaryPath where the MILESTONE-SUMMARY.md was written.", ], parameters: Type.Object({ + // ── Core identification + content (required) ────────────────────── milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), title: Type.String({ description: "Milestone title" }), oneLiner: Type.String({ description: "One-sentence summary of what the milestone achieved" }), narrative: Type.String({ description: "Detailed narrative of what happened during the milestone" }), - successCriteriaResults: Type.String({ description: "Markdown detailing how each success criterion was met or not met" }), - definitionOfDoneResults: Type.String({ description: "Markdown detailing how each definition-of-done item was met" }), - requirementOutcomes: Type.String({ description: "Markdown detailing requirement status transitions with evidence" }), - keyDecisions: Type.Array(Type.String(), { description: "Key architectural/pattern decisions made during the milestone" }), - keyFiles: Type.Array(Type.String(), { description: "Key files created or modified during the milestone" }), - lessonsLearned: Type.Array(Type.String(), { description: "Lessons learned during the milestone" }), + verificationPassed: Type.Boolean({ description: "Must be true — confirms that code change verification, success criteria, and definition of done checks all passed before completion" }), + // ── Enrichment metadata (optional — defaults to empty) ──────────── + successCriteriaResults: Type.Optional(Type.String({ description: "Markdown detailing how each success criterion was met or not met" })), + definitionOfDoneResults: Type.Optional(Type.String({ description: "Markdown detailing how each definition-of-done item was met" })), + requirementOutcomes: Type.Optional(Type.String({ description: "Markdown detailing requirement status transitions with evidence" })), + keyDecisions: Type.Optional(Type.Array(Type.String(), { description: "Key architectural/pattern decisions made during the milestone" })), + keyFiles: Type.Optional(Type.Array(Type.String(), { description: "Key files created or modified during the milestone" })), + lessonsLearned: Type.Optional(Type.Array(Type.String(), { description: "Lessons learned during the milestone" })), followUps: Type.Optional(Type.String({ description: "Follow-up items for future milestones" })), deviations: Type.Optional(Type.String({ description: "Deviations from the original plan" })), - verificationPassed: Type.Boolean({ description: "Must be true — confirms that code change verification, success criteria, and definition of done checks all passed before completion" }), }), execute: milestoneCompleteExecute, }; diff --git a/src/resources/extensions/gsd/tests/complete-task.test.ts b/src/resources/extensions/gsd/tests/complete-task.test.ts index 9c64b8eba..c65f1ff05 100644 --- a/src/resources/extensions/gsd/tests/complete-task.test.ts +++ b/src/resources/extensions/gsd/tests/complete-task.test.ts @@ -449,6 +449,45 @@ console.log('\n=== complete-task: handler with missing plan file ==='); cleanup(dbPath); } +// ═══════════════════════════════════════════════════════════════════════════ +// complete-task: minimal params — no optional fields (#2771 regression) +// ═══════════════════════════════════════════════════════════════════════════ + +console.log('\n=== complete-task: minimal params (no keyFiles, keyDecisions, verificationEvidence, blockerDiscovered) ==='); +{ + const dbPath = tempDbPath(); + openDatabase(dbPath); + + const { basePath, planPath } = createTempProject(); + + insertMilestone({ id: 'M001', title: 'Test Milestone' }); + insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice' }); + + // Minimal params — only required fields, all optional enrichment fields omitted + const minimalParams = { + taskId: 'T01', + sliceId: 'S01', + milestoneId: 'M001', + oneLiner: 'Basic task', + narrative: 'Did the work.', + verification: 'Looks good.', + // keyFiles, keyDecisions, verificationEvidence, blockerDiscovered intentionally omitted + }; + + const result = await handleCompleteTask(minimalParams as any, basePath); + + assertTrue(!('error' in result), 'handler should not crash with minimal params (no optional fields)'); + if (!('error' in result)) { + assertTrue(fs.existsSync(result.summaryPath), 'summary file should be written with minimal params'); + const summaryContent = fs.readFileSync(result.summaryPath, 'utf-8'); + assertMatch(summaryContent, /blocker_discovered:\s*false/, 'blocker_discovered should default to false'); + assertMatch(summaryContent, /\(none\)/, 'key_files/key_decisions should show (none) placeholder'); + } + + cleanupDir(basePath); + cleanup(dbPath); +} + // ═══════════════════════════════════════════════════════════════════════════ report(); diff --git a/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts b/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts new file mode 100644 index 000000000..6521d1bda --- /dev/null +++ b/src/resources/extensions/gsd/tests/tool-param-optionality.test.ts @@ -0,0 +1,349 @@ +/** + * tool-param-optionality — Verifies that enrichment/metadata parameters on + * planning and completion tools are optional, not required. + * + * Models with limited tool-calling capability (e.g. kimi-k2.5, glm-5-turbo) + * cannot reliably populate 20+ top-level parameters in a single tool call. + * This test ensures that only the core identification and content parameters + * are required, while enrichment arrays (patterns, requirements, files, etc.) + * are optional — so any model can call the tool successfully. + * + * See: https://github.com/gsd-build/gsd-2/issues/2771 + */ + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { registerDbTools } from "../bootstrap/db-tools.ts"; +import { Value } from "@sinclair/typebox/value"; + +// ─── Mock PI ────────────────────────────────────────────────────────────────── + +function makeMockPi() { + const tools: any[] = []; + return { + registerTool: (tool: any) => tools.push(tool), + tools, + } as any; +} + +const pi = makeMockPi(); +registerDbTools(pi); + +function getTool(name: string) { + return pi.tools.find((t: any) => t.name === name); +} + +// ─── Helper: count required top-level properties ───────────────────────────── + +function getRequiredProps(tool: any): string[] { + const schema = tool.parameters; + return schema.required ?? []; +} + +function getOptionalProps(tool: any): string[] { + const schema = tool.parameters; + const allProps = Object.keys(schema.properties ?? {}); + const required = new Set(schema.required ?? []); + return allProps.filter((p: string) => !required.has(p)); +} + +// ─── gsd_slice_complete: enrichment arrays must be optional ────────────────── + +test("gsd_slice_complete — enrichment arrays are optional", () => { + const tool = getTool("gsd_slice_complete"); + assert.ok(tool, "gsd_slice_complete must be registered"); + + const required = new Set(getRequiredProps(tool)); + + // Core identification and content fields MUST be required + const coreRequired = [ + "sliceId", + "milestoneId", + "sliceTitle", + "oneLiner", + "narrative", + "verification", + "uatContent", + ]; + for (const field of coreRequired) { + assert.ok(required.has(field), `core field "${field}" must be required`); + } + + // Enrichment/metadata arrays MUST be optional + const enrichmentFields = [ + "keyFiles", + "keyDecisions", + "patternsEstablished", + "observabilitySurfaces", + "provides", + "requirementsSurfaced", + "drillDownPaths", + "affects", + "requirementsAdvanced", + "requirementsValidated", + "requirementsInvalidated", + "filesModified", + "requires", + "deviations", + "knownLimitations", + "followUps", + ]; + for (const field of enrichmentFields) { + assert.ok(!required.has(field), `enrichment field "${field}" must be optional, not required`); + } +}); + +test("gsd_slice_complete — validates with only core params", () => { + const tool = getTool("gsd_slice_complete"); + assert.ok(tool, "gsd_slice_complete must be registered"); + + const minimalParams = { + sliceId: "S01", + milestoneId: "M001", + sliceTitle: "Test slice", + oneLiner: "Did the thing", + narrative: "We did it step by step.", + verification: "Tests pass.", + uatContent: "## UAT\n- [x] Works", + }; + + // Should pass schema validation with only core params + const errors = [...Value.Errors(tool.parameters, minimalParams)]; + assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.map(e => `${e.path}: ${e.message}`).join(", ")}`); +}); + +// ─── gsd_plan_milestone: enrichment arrays must be optional ────────────────── + +test("gsd_plan_milestone — enrichment arrays are optional", () => { + const tool = getTool("gsd_plan_milestone"); + assert.ok(tool, "gsd_plan_milestone must be registered"); + + const required = new Set(getRequiredProps(tool)); + + // Core fields + const coreRequired = ["milestoneId", "title", "vision", "slices"]; + for (const field of coreRequired) { + assert.ok(required.has(field), `core field "${field}" must be required`); + } + + // Enrichment fields must be optional + const enrichmentFields = [ + "successCriteria", + "keyRisks", + "proofStrategy", + "verificationContract", + "verificationIntegration", + "verificationOperational", + "verificationUat", + "definitionOfDone", + "requirementCoverage", + "boundaryMapMarkdown", + ]; + for (const field of enrichmentFields) { + assert.ok(!required.has(field), `enrichment field "${field}" must be optional, not required`); + } +}); + +test("gsd_plan_milestone — validates with only core params", () => { + const tool = getTool("gsd_plan_milestone"); + assert.ok(tool, "gsd_plan_milestone must be registered"); + + const minimalParams = { + milestoneId: "M001", + title: "Test milestone", + vision: "Build the thing.", + slices: [ + { + sliceId: "S01", + title: "First slice", + risk: "Low", + depends: [], + demo: "After this, X works", + goal: "Set up X", + successCriteria: "X is set up", + proofLevel: "unit-tests", + integrationClosure: "N/A", + observabilityImpact: "None", + }, + ], + }; + + const errors = [...Value.Errors(tool.parameters, minimalParams)]; + assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.map(e => `${e.path}: ${e.message}`).join(", ")}`); +}); + +// ─── gsd_task_complete: enrichment arrays must be optional ─────────────────── + +test("gsd_task_complete — enrichment arrays are optional", () => { + const tool = getTool("gsd_task_complete"); + assert.ok(tool, "gsd_task_complete must be registered"); + + const required = new Set(getRequiredProps(tool)); + + // Core fields + const coreRequired = [ + "taskId", + "sliceId", + "milestoneId", + "oneLiner", + "narrative", + "verification", + ]; + for (const field of coreRequired) { + assert.ok(required.has(field), `core field "${field}" must be required`); + } + + // Enrichment fields must be optional + const enrichmentFields = [ + "keyFiles", + "keyDecisions", + "deviations", + "knownIssues", + "blockerDiscovered", + "verificationEvidence", + ]; + for (const field of enrichmentFields) { + assert.ok(!required.has(field), `enrichment field "${field}" must be optional, not required`); + } +}); + +test("gsd_task_complete — validates with only core params", () => { + const tool = getTool("gsd_task_complete"); + assert.ok(tool, "gsd_task_complete must be registered"); + + const minimalParams = { + taskId: "T01", + sliceId: "S01", + milestoneId: "M001", + oneLiner: "Implemented the feature", + narrative: "Created the module and wired it up.", + verification: "npm test passes.", + }; + + const errors = [...Value.Errors(tool.parameters, minimalParams)]; + assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.map(e => `${e.path}: ${e.message}`).join(", ")}`); +}); + +// ─── gsd_complete_milestone: enrichment arrays must be optional ────────────── + +test("gsd_complete_milestone — enrichment arrays are optional", () => { + const tool = getTool("gsd_complete_milestone"); + assert.ok(tool, "gsd_complete_milestone must be registered"); + + const required = new Set(getRequiredProps(tool)); + + // Core fields + const coreRequired = [ + "milestoneId", + "title", + "oneLiner", + "narrative", + "verificationPassed", + ]; + for (const field of coreRequired) { + assert.ok(required.has(field), `core field "${field}" must be required`); + } + + // Enrichment fields must be optional + const enrichmentFields = [ + "successCriteriaResults", + "definitionOfDoneResults", + "requirementOutcomes", + "keyDecisions", + "keyFiles", + "lessonsLearned", + ]; + for (const field of enrichmentFields) { + assert.ok(!required.has(field), `enrichment field "${field}" must be optional, not required`); + } +}); + +test("gsd_complete_milestone — validates with only core params", () => { + const tool = getTool("gsd_complete_milestone"); + assert.ok(tool, "gsd_complete_milestone must be registered"); + + const minimalParams = { + milestoneId: "M001", + title: "Test milestone", + oneLiner: "Finished it.", + narrative: "All work completed.", + verificationPassed: true, + }; + + const errors = [...Value.Errors(tool.parameters, minimalParams)]; + assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.map(e => `${e.path}: ${e.message}`).join(", ")}`); +}); + +// ─── gsd_plan_slice: enrichment fields must be optional ────────────────────── + +test("gsd_plan_slice — enrichment fields are optional", () => { + const tool = getTool("gsd_plan_slice"); + assert.ok(tool, "gsd_plan_slice must be registered"); + + const required = new Set(getRequiredProps(tool)); + + // Core fields + const coreRequired = ["milestoneId", "sliceId", "goal", "tasks"]; + for (const field of coreRequired) { + assert.ok(required.has(field), `core field "${field}" must be required`); + } + + // Enrichment fields + const enrichmentFields = [ + "successCriteria", + "proofLevel", + "integrationClosure", + "observabilityImpact", + ]; + for (const field of enrichmentFields) { + assert.ok(!required.has(field), `enrichment field "${field}" must be optional, not required`); + } +}); + +test("gsd_plan_slice — validates with only core params", () => { + const tool = getTool("gsd_plan_slice"); + assert.ok(tool, "gsd_plan_slice must be registered"); + + const minimalParams = { + milestoneId: "M001", + sliceId: "S01", + goal: "Implement feature X", + tasks: [ + { + taskId: "T01", + title: "Build X", + description: "Build the thing", + estimate: "2h", + files: ["src/x.ts"], + verify: "npm test", + inputs: [], + expectedOutput: ["src/x.ts"], + }, + ], + }; + + const errors = [...Value.Errors(tool.parameters, minimalParams)]; + assert.strictEqual(errors.length, 0, `Minimal params should validate but got errors: ${errors.map(e => `${e.path}: ${e.message}`).join(", ")}`); +}); + +// ─── Required param count ceiling ──────────────────────────────────────────── + +test("no planning/completion tool requires more than 10 top-level params", () => { + const heavyTools = [ + "gsd_slice_complete", + "gsd_plan_milestone", + "gsd_task_complete", + "gsd_complete_milestone", + "gsd_plan_slice", + ]; + + for (const name of heavyTools) { + const tool = getTool(name); + assert.ok(tool, `${name} must be registered`); + const required = getRequiredProps(tool); + assert.ok( + required.length <= 10, + `${name} has ${required.length} required params (max 10) — required: ${required.join(", ")}`, + ); + } +}); diff --git a/src/resources/extensions/gsd/tools/complete-milestone.ts b/src/resources/extensions/gsd/tools/complete-milestone.ts index 14ca78262..64d36f2e2 100644 --- a/src/resources/extensions/gsd/tools/complete-milestone.ts +++ b/src/resources/extensions/gsd/tools/complete-milestone.ts @@ -30,15 +30,23 @@ export interface CompleteMilestoneParams { title: string; oneLiner: string; narrative: string; - successCriteriaResults: string; - definitionOfDoneResults: string; - requirementOutcomes: string; - keyDecisions: string[]; - keyFiles: string[]; - lessonsLearned: string[]; - followUps: string; - deviations: string; verificationPassed: boolean; + /** @optional — defaults to "Not provided." when omitted by models with limited tool-calling */ + successCriteriaResults?: string; + /** @optional — defaults to "Not provided." when omitted */ + definitionOfDoneResults?: string; + /** @optional — defaults to "Not provided." when omitted */ + requirementOutcomes?: string; + /** @optional — defaults to [] when omitted */ + keyDecisions?: string[]; + /** @optional — defaults to [] when omitted */ + keyFiles?: string[]; + /** @optional — defaults to [] when omitted */ + lessonsLearned?: string[]; + /** @optional — defaults to "None." when omitted */ + followUps?: string; + /** @optional — defaults to "None." when omitted */ + deviations?: string; /** Optional caller-provided identity for audit trail */ actorName?: string; /** Optional caller-provided reason this action was triggered */ @@ -54,16 +62,21 @@ function renderMilestoneSummaryMarkdown(params: CompleteMilestoneParams): string const now = new Date().toISOString(); const displayTitle = stripIdPrefix(params.title, params.milestoneId); - const keyDecisionsYaml = params.keyDecisions.length > 0 - ? params.keyDecisions.map(d => ` - ${d}`).join("\n") + // Apply defaults for optional enrichment fields (#2771) + const keyDecisions = params.keyDecisions ?? []; + const keyFiles = params.keyFiles ?? []; + const lessonsLearned = params.lessonsLearned ?? []; + + const keyDecisionsYaml = keyDecisions.length > 0 + ? keyDecisions.map(d => ` - ${d}`).join("\n") : " - (none)"; - const keyFilesYaml = params.keyFiles.length > 0 - ? params.keyFiles.map(f => ` - ${f}`).join("\n") + const keyFilesYaml = keyFiles.length > 0 + ? keyFiles.map(f => ` - ${f}`).join("\n") : " - (none)"; - const lessonsYaml = params.lessonsLearned.length > 0 - ? params.lessonsLearned.map(l => ` - ${l}`).join("\n") + const lessonsYaml = lessonsLearned.length > 0 + ? lessonsLearned.map(l => ` - ${l}`).join("\n") : " - (none)"; return `--- @@ -89,15 +102,15 @@ ${params.narrative} ## Success Criteria Results -${params.successCriteriaResults} +${params.successCriteriaResults ?? "Not provided."} ## Definition of Done Results -${params.definitionOfDoneResults} +${params.definitionOfDoneResults ?? "Not provided."} ## Requirement Outcomes -${params.requirementOutcomes} +${params.requirementOutcomes ?? "Not provided."} ## Deviations diff --git a/src/resources/extensions/gsd/tools/complete-slice.ts b/src/resources/extensions/gsd/tools/complete-slice.ts index d555ab518..bf374a622 100644 --- a/src/resources/extensions/gsd/tools/complete-slice.ts +++ b/src/resources/extensions/gsd/tools/complete-slice.ts @@ -46,58 +46,73 @@ export interface CompleteSliceResult { function renderSliceSummaryMarkdown(params: CompleteSliceParams): string { const now = new Date().toISOString(); - const providesYaml = params.provides.length > 0 - ? params.provides.map(p => ` - ${p}`).join("\n") + // Apply defaults for optional enrichment arrays (#2771) + const provides = params.provides ?? []; + const requires = params.requires ?? []; + const affects = params.affects ?? []; + const keyFiles = params.keyFiles ?? []; + const keyDecisions = params.keyDecisions ?? []; + const patternsEstablished = params.patternsEstablished ?? []; + const observabilitySurfaces = params.observabilitySurfaces ?? []; + const drillDownPaths = params.drillDownPaths ?? []; + const requirementsAdvanced = params.requirementsAdvanced ?? []; + const requirementsValidated = params.requirementsValidated ?? []; + const requirementsSurfaced = params.requirementsSurfaced ?? []; + const requirementsInvalidated = params.requirementsInvalidated ?? []; + const filesModified = params.filesModified ?? []; + + const providesYaml = provides.length > 0 + ? provides.map(p => ` - ${p}`).join("\n") : " - (none)"; - const requiresYaml = params.requires.length > 0 - ? params.requires.map(r => ` - slice: ${r.slice}\n provides: ${r.provides}`).join("\n") + const requiresYaml = requires.length > 0 + ? requires.map(r => ` - slice: ${r.slice}\n provides: ${r.provides}`).join("\n") : " []"; - const affectsYaml = params.affects.length > 0 - ? params.affects.map(a => ` - ${a}`).join("\n") + const affectsYaml = affects.length > 0 + ? affects.map(a => ` - ${a}`).join("\n") : " []"; - const keyFilesYaml = params.keyFiles.length > 0 - ? params.keyFiles.map(f => ` - ${f}`).join("\n") + const keyFilesYaml = keyFiles.length > 0 + ? keyFiles.map(f => ` - ${f}`).join("\n") : " - (none)"; - const keyDecisionsYaml = params.keyDecisions.length > 0 - ? params.keyDecisions.map(d => ` - ${d}`).join("\n") + const keyDecisionsYaml = keyDecisions.length > 0 + ? keyDecisions.map(d => ` - ${d}`).join("\n") : " - (none)"; - const patternsYaml = params.patternsEstablished.length > 0 - ? params.patternsEstablished.map(p => ` - ${p}`).join("\n") + const patternsYaml = patternsEstablished.length > 0 + ? patternsEstablished.map(p => ` - ${p}`).join("\n") : " - (none)"; - const observabilityYaml = params.observabilitySurfaces.length > 0 - ? params.observabilitySurfaces.map(o => ` - ${o}`).join("\n") + const observabilityYaml = observabilitySurfaces.length > 0 + ? observabilitySurfaces.map(o => ` - ${o}`).join("\n") : " - none"; - const drillDownYaml = params.drillDownPaths.length > 0 - ? params.drillDownPaths.map(d => ` - ${d}`).join("\n") + const drillDownYaml = drillDownPaths.length > 0 + ? drillDownPaths.map(d => ` - ${d}`).join("\n") : " []"; // Requirements sections - const reqAdvanced = params.requirementsAdvanced.length > 0 - ? params.requirementsAdvanced.map(r => `- ${r.id} — ${r.how}`).join("\n") + const reqAdvanced = requirementsAdvanced.length > 0 + ? requirementsAdvanced.map(r => `- ${r.id} — ${r.how}`).join("\n") : "None."; - const reqValidated = params.requirementsValidated.length > 0 - ? params.requirementsValidated.map(r => `- ${r.id} — ${r.proof}`).join("\n") + const reqValidated = requirementsValidated.length > 0 + ? requirementsValidated.map(r => `- ${r.id} — ${r.proof}`).join("\n") : "None."; - const reqSurfaced = params.requirementsSurfaced.length > 0 - ? params.requirementsSurfaced.map(r => `- ${r}`).join("\n") + const reqSurfaced = requirementsSurfaced.length > 0 + ? requirementsSurfaced.map(r => `- ${r}`).join("\n") : "None."; - const reqInvalidated = params.requirementsInvalidated.length > 0 - ? params.requirementsInvalidated.map(r => `- ${r.id} — ${r.what}`).join("\n") + const reqInvalidated = requirementsInvalidated.length > 0 + ? requirementsInvalidated.map(r => `- ${r.id} — ${r.what}`).join("\n") : "None."; // Files modified - const filesMod = params.filesModified.length > 0 - ? params.filesModified.map(f => `- \`${f.path}\` — ${f.description}`).join("\n") + const filesMod = filesModified.length > 0 + ? filesModified.map(f => `- \`${f.path}\` — ${f.description}`).join("\n") : "None."; return `--- diff --git a/src/resources/extensions/gsd/tools/complete-task.ts b/src/resources/extensions/gsd/tools/complete-task.ts index 93f5bc4df..a862ae92b 100644 --- a/src/resources/extensions/gsd/tools/complete-task.ts +++ b/src/resources/extensions/gsd/tools/complete-task.ts @@ -60,11 +60,11 @@ function paramsToTaskRow(params: CompleteTaskParams, completedAt: string): TaskR verification_result: params.verification, duration: "", completed_at: completedAt, - blocker_discovered: params.blockerDiscovered, - deviations: params.deviations, - known_issues: params.knownIssues, - key_files: params.keyFiles, - key_decisions: params.keyDecisions, + blocker_discovered: params.blockerDiscovered ?? false, + deviations: params.deviations ?? "", + known_issues: params.knownIssues ?? "", + key_files: params.keyFiles ?? [], + key_decisions: params.keyDecisions ?? [], full_summary_md: "", description: "", estimate: "", @@ -152,14 +152,14 @@ export async function handleCompleteTask( narrative: params.narrative, verificationResult: params.verification, duration: "", - blockerDiscovered: params.blockerDiscovered, - deviations: params.deviations, - knownIssues: params.knownIssues, - keyFiles: params.keyFiles, - keyDecisions: params.keyDecisions, + blockerDiscovered: params.blockerDiscovered ?? false, + deviations: params.deviations ?? "None.", + knownIssues: params.knownIssues ?? "None.", + keyFiles: params.keyFiles ?? [], + keyDecisions: params.keyDecisions ?? [], }); - for (const evidence of params.verificationEvidence) { + for (const evidence of (params.verificationEvidence ?? [])) { insertVerificationEvidence({ taskId: params.taskId, sliceId: params.sliceId, @@ -182,7 +182,7 @@ export async function handleCompleteTask( // Render summary markdown via the single source of truth (#2720) const taskRow = paramsToTaskRow(params, completedAt); - const summaryMd = renderSummaryContent(taskRow, params.sliceId, params.milestoneId, params.verificationEvidence); + const summaryMd = renderSummaryContent(taskRow, params.sliceId, params.milestoneId, params.verificationEvidence ?? []); // Resolve and write summary to disk let summaryPath: string; diff --git a/src/resources/extensions/gsd/tools/plan-milestone.ts b/src/resources/extensions/gsd/tools/plan-milestone.ts index 4b6f0e2d0..4cc39fe8b 100644 --- a/src/resources/extensions/gsd/tools/plan-milestone.ts +++ b/src/resources/extensions/gsd/tools/plan-milestone.ts @@ -34,24 +34,34 @@ export interface PlanMilestoneSliceInput { export interface PlanMilestoneParams { milestoneId: string; title: string; + vision: string; + slices: PlanMilestoneSliceInput[]; status?: string; dependsOn?: string[]; /** Optional caller-provided identity for audit trail */ actorName?: string; /** Optional caller-provided reason this action was triggered */ triggerReason?: string; - vision: string; - successCriteria: string[]; - keyRisks: Array<{ risk: string; whyItMatters: string }>; - proofStrategy: Array<{ riskOrUnknown: string; retireIn: string; whatWillBeProven: string }>; - verificationContract: string; - verificationIntegration: string; - verificationOperational: string; - verificationUat: string; - definitionOfDone: string[]; - requirementCoverage: string; - boundaryMapMarkdown: string; - slices: PlanMilestoneSliceInput[]; + /** @optional — defaults to [] when omitted by models with limited tool-calling */ + successCriteria?: string[]; + /** @optional — defaults to [] when omitted */ + keyRisks?: Array<{ risk: string; whyItMatters: string }>; + /** @optional — defaults to [] when omitted */ + proofStrategy?: Array<{ riskOrUnknown: string; retireIn: string; whatWillBeProven: string }>; + /** @optional — defaults to "Not provided." when omitted */ + verificationContract?: string; + /** @optional — defaults to "Not provided." when omitted */ + verificationIntegration?: string; + /** @optional — defaults to "Not provided." when omitted */ + verificationOperational?: string; + /** @optional — defaults to "Not provided." when omitted */ + verificationUat?: string; + /** @optional — defaults to [] when omitted */ + definitionOfDone?: string[]; + /** @optional — defaults to "Not provided." when omitted */ + requirementCoverage?: string; + /** @optional — defaults to "Not provided." when omitted */ + boundaryMapMarkdown?: string; } export interface PlanMilestoneResult { @@ -150,20 +160,21 @@ function validateParams(params: PlanMilestoneParams): PlanMilestoneParams { if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required"); if (!isNonEmptyString(params?.title)) throw new Error("title is required"); if (!isNonEmptyString(params?.vision)) throw new Error("vision is required"); - if (!isNonEmptyString(params?.verificationContract)) throw new Error("verificationContract is required"); - if (!isNonEmptyString(params?.verificationIntegration)) throw new Error("verificationIntegration is required"); - if (!isNonEmptyString(params?.verificationOperational)) throw new Error("verificationOperational is required"); - if (!isNonEmptyString(params?.verificationUat)) throw new Error("verificationUat is required"); - if (!isNonEmptyString(params?.requirementCoverage)) throw new Error("requirementCoverage is required"); - if (!isNonEmptyString(params?.boundaryMapMarkdown)) throw new Error("boundaryMapMarkdown is required"); return { ...params, dependsOn: params.dependsOn ? validateStringArray(params.dependsOn, "dependsOn") : [], - successCriteria: validateStringArray(params.successCriteria, "successCriteria"), - keyRisks: validateRiskEntries(params.keyRisks), - proofStrategy: validateProofStrategy(params.proofStrategy), - definitionOfDone: validateStringArray(params.definitionOfDone, "definitionOfDone"), + // Apply defaults for optional enrichment fields (#2771) + successCriteria: params.successCriteria ? validateStringArray(params.successCriteria, "successCriteria") : [], + keyRisks: params.keyRisks ? validateRiskEntries(params.keyRisks) : [], + proofStrategy: params.proofStrategy ? validateProofStrategy(params.proofStrategy) : [], + verificationContract: params.verificationContract ?? "Not provided.", + verificationIntegration: params.verificationIntegration ?? "Not provided.", + verificationOperational: params.verificationOperational ?? "Not provided.", + verificationUat: params.verificationUat ?? "Not provided.", + definitionOfDone: params.definitionOfDone ? validateStringArray(params.definitionOfDone, "definitionOfDone") : [], + requirementCoverage: params.requirementCoverage ?? "Not provided.", + boundaryMapMarkdown: params.boundaryMapMarkdown ?? "Not provided.", slices: validateSlices(params.slices), }; } diff --git a/src/resources/extensions/gsd/tools/plan-slice.ts b/src/resources/extensions/gsd/tools/plan-slice.ts index c40467d47..8324bdc82 100644 --- a/src/resources/extensions/gsd/tools/plan-slice.ts +++ b/src/resources/extensions/gsd/tools/plan-slice.ts @@ -35,11 +35,15 @@ export interface PlanSliceParams { milestoneId: string; sliceId: string; goal: string; - successCriteria: string; - proofLevel: string; - integrationClosure: string; - observabilityImpact: string; tasks: PlanSliceTaskInput[]; + /** @optional — defaults to "Not provided." when omitted by models with limited tool-calling */ + successCriteria?: string; + /** @optional — defaults to "Not provided." when omitted */ + proofLevel?: string; + /** @optional — defaults to "Not provided." when omitted */ + integrationClosure?: string; + /** @optional — defaults to "Not provided." when omitted */ + observabilityImpact?: string; /** Optional caller-provided identity for audit trail */ actorName?: string; /** Optional caller-provided reason this action was triggered */ @@ -112,13 +116,14 @@ function validateParams(params: PlanSliceParams): PlanSliceParams { if (!isNonEmptyString(params?.milestoneId)) throw new Error("milestoneId is required"); if (!isNonEmptyString(params?.sliceId)) throw new Error("sliceId is required"); if (!isNonEmptyString(params?.goal)) throw new Error("goal is required"); - if (!isNonEmptyString(params?.successCriteria)) throw new Error("successCriteria is required"); - if (!isNonEmptyString(params?.proofLevel)) throw new Error("proofLevel is required"); - if (!isNonEmptyString(params?.integrationClosure)) throw new Error("integrationClosure is required"); - if (!isNonEmptyString(params?.observabilityImpact)) throw new Error("observabilityImpact is required"); return { ...params, + // Apply defaults for optional enrichment fields (#2771) + successCriteria: params.successCriteria ?? "Not provided.", + proofLevel: params.proofLevel ?? "Not provided.", + integrationClosure: params.integrationClosure ?? "Not provided.", + observabilityImpact: params.observabilityImpact ?? "Not provided.", tasks: validateTasks(params.tasks), }; } diff --git a/src/resources/extensions/gsd/types.ts b/src/resources/extensions/gsd/types.ts index c06891e07..83fd1f99d 100644 --- a/src/resources/extensions/gsd/types.ts +++ b/src/resources/extensions/gsd/types.ts @@ -515,12 +515,18 @@ export interface CompleteTaskParams { oneLiner: string; narrative: string; verification: string; - keyFiles: string[]; - keyDecisions: string[]; - deviations: string; - knownIssues: string; - blockerDiscovered: boolean; - verificationEvidence: Array<{ + /** @optional — defaults to [] when omitted by models with limited tool-calling */ + keyFiles?: string[]; + /** @optional — defaults to [] when omitted by models with limited tool-calling */ + keyDecisions?: string[]; + /** @optional — defaults to "None." when omitted */ + deviations?: string; + /** @optional — defaults to "None." when omitted */ + knownIssues?: string; + /** @optional — defaults to false when omitted */ + blockerDiscovered?: boolean; + /** @optional — defaults to [] when omitted by models with limited tool-calling */ + verificationEvidence?: Array<{ command: string; exitCode: number; verdict: string; @@ -541,23 +547,39 @@ export interface CompleteSliceParams { oneLiner: string; narrative: string; verification: string; - keyFiles: string[]; - keyDecisions: string[]; - patternsEstablished: string[]; - observabilitySurfaces: string[]; - deviations: string; - knownLimitations: string; - followUps: string; - requirementsAdvanced: Array<{ id: string; how: string }>; - requirementsValidated: Array<{ id: string; proof: string }>; - requirementsSurfaced: string[]; - requirementsInvalidated: Array<{ id: string; what: string }>; - filesModified: Array<{ path: string; description: string }>; uatContent: string; - provides: string[]; - requires: Array<{ slice: string; provides: string }>; - affects: string[]; - drillDownPaths: string[]; + /** @optional — defaults to [] when omitted by models with limited tool-calling */ + keyFiles?: string[]; + /** @optional — defaults to [] when omitted */ + keyDecisions?: string[]; + /** @optional — defaults to [] when omitted */ + patternsEstablished?: string[]; + /** @optional — defaults to [] when omitted */ + observabilitySurfaces?: string[]; + /** @optional — defaults to "None." when omitted */ + deviations?: string; + /** @optional — defaults to "None." when omitted */ + knownLimitations?: string; + /** @optional — defaults to "None." when omitted */ + followUps?: string; + /** @optional — defaults to [] when omitted */ + requirementsAdvanced?: Array<{ id: string; how: string }>; + /** @optional — defaults to [] when omitted */ + requirementsValidated?: Array<{ id: string; proof: string }>; + /** @optional — defaults to [] when omitted */ + requirementsSurfaced?: string[]; + /** @optional — defaults to [] when omitted */ + requirementsInvalidated?: Array<{ id: string; what: string }>; + /** @optional — defaults to [] when omitted */ + filesModified?: Array<{ path: string; description: string }>; + /** @optional — defaults to [] when omitted */ + provides?: string[]; + /** @optional — defaults to [] when omitted */ + requires?: Array<{ slice: string; provides: string }>; + /** @optional — defaults to [] when omitted */ + affects?: string[]; + /** @optional — defaults to [] when omitted */ + drillDownPaths?: string[]; /** Optional caller-provided identity for audit trail */ actorName?: string; /** Optional caller-provided reason this action was triggered */