fix: make enrichment tool params optional for limited-toolcall models (#3302)
* fix: make enrichment tool params optional for limited-toolcall models Models with limited tool-calling capability (kimi-k2.5, glm-5-turbo) cannot populate 23 top-level parameters in a single tool call, causing stuck-loop retries that burn tokens. This reduces required params to only core identification and content fields (7 for slice_complete, 4 for plan_milestone, 6 for task_complete, 5 for complete_milestone, 4 for plan_slice) by making enrichment/metadata arrays optional with sensible defaults in the handlers. Closes #2771 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(complete-task): add null-coalescing defaults to renderSummaryMarkdown Add regression test verifying handleCompleteTask does not crash when optional enrichment fields (keyFiles, keyDecisions, verificationEvidence, blockerDiscovered) are omitted. The code paths already apply ?? defaults after the #2720 refactor, but no test covered the minimal-params case. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: retrigger CI * fix: add null coalescing for optional string params in complete-task params.deviations and params.knownIssues are optional (string | undefined) but the task row expects string. Add ?? "" to satisfy TS2322. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: trek-e <trek-e@users.noreply.github.com>
This commit is contained in:
parent
7bbc0dd621
commit
4f6b3433d6
9 changed files with 630 additions and 166 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(", ")}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `---
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue