diff --git a/src/resources/extensions/gsd/auto-prompts.ts b/src/resources/extensions/gsd/auto-prompts.ts index e0017d786..d683102dc 100644 --- a/src/resources/extensions/gsd/auto-prompts.ts +++ b/src/resources/extensions/gsd/auto-prompts.ts @@ -1307,6 +1307,12 @@ export async function buildCompleteMilestonePrompt( roadmapPath: roadmapRel, inlinedContext, milestoneSummaryPath, + skillActivation: buildSkillActivationBlock({ + base, + milestoneId: mid, + milestoneTitle: midTitle, + extraContext: [inlinedContext], + }), }); } @@ -1390,6 +1396,12 @@ export async function buildValidateMilestonePrompt( inlinedContext, validationPath: validationOutputPath, remediationRound: String(remediationRound), + skillActivation: buildSkillActivationBlock({ + base, + milestoneId: mid, + milestoneTitle: midTitle, + extraContext: [inlinedContext], + }), }); } @@ -1500,6 +1512,12 @@ export async function buildRunUatPrompt( uatResultPath, uatType, inlinedContext, + skillActivation: buildSkillActivationBlock({ + base, + milestoneId: mid, + sliceId, + extraContext: [inlinedContext], + }), }); } @@ -1552,11 +1570,16 @@ export async function buildReassessRoadmapPrompt( milestoneTitle: midTitle, completedSliceId, roadmapPath: roadmapRel, - completedSliceSummaryPath: summaryRel, assessmentPath, inlinedContext, deferredCaptures, commitInstruction: reassessCommitInstruction, + skillActivation: buildSkillActivationBlock({ + base, + milestoneId: mid, + milestoneTitle: midTitle, + extraContext: [inlinedContext, deferredCaptures], + }), }); } diff --git a/src/resources/extensions/gsd/bootstrap/db-tools.ts b/src/resources/extensions/gsd/bootstrap/db-tools.ts index 70edc4e30..f1f0ecd1f 100644 --- a/src/resources/extensions/gsd/bootstrap/db-tools.ts +++ b/src/resources/extensions/gsd/bootstrap/db-tools.ts @@ -813,6 +813,74 @@ export function registerDbTools(pi: ExtensionAPI): void { pi.registerTool(milestoneCompleteTool); registerAlias(pi, milestoneCompleteTool, "gsd_milestone_complete", "gsd_complete_milestone"); + // ─── gsd_validate_milestone (gsd_milestone_validate alias) ───────────── + + const milestoneValidateExecute = async (_toolCallId: string, params: any, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) => { + const dbAvailable = await ensureDbOpen(); + if (!dbAvailable) { + return { + content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot validate milestone." }], + details: { operation: "validate_milestone", error: "db_unavailable" } as any, + }; + } + try { + const { handleValidateMilestone } = await import("../tools/validate-milestone.js"); + const result = await handleValidateMilestone(params, process.cwd()); + if ("error" in result) { + return { + content: [{ type: "text" as const, text: `Error validating milestone: ${result.error}` }], + details: { operation: "validate_milestone", error: result.error } as any, + }; + } + return { + content: [{ type: "text" as const, text: `Validated milestone ${result.milestoneId} — verdict: ${result.verdict}. Written to ${result.validationPath}` }], + details: { + operation: "validate_milestone", + milestoneId: result.milestoneId, + verdict: result.verdict, + validationPath: result.validationPath, + } as any, + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + process.stderr.write(`gsd-db: validate_milestone tool failed: ${msg}\n`); + return { + content: [{ type: "text" as const, text: `Error validating milestone: ${msg}` }], + details: { operation: "validate_milestone", error: msg } as any, + }; + } + }; + + const milestoneValidateTool = { + name: "gsd_validate_milestone", + label: "Validate Milestone", + description: + "Validate a milestone before completion — persist validation results to the DB, render VALIDATION.md to disk. " + + "Records verdict (pass/needs-attention/needs-remediation) and rationale.", + promptSnippet: "Validate a GSD milestone (DB write + VALIDATION.md render)", + promptGuidelines: [ + "Use gsd_validate_milestone when all slices are done and the milestone needs validation before completion.", + "Parameters: milestoneId, verdict, remediationRound, successCriteriaChecklist, sliceDeliveryAudit, crossSliceIntegration, requirementCoverage, verdictRationale, remediationPlan (optional).", + "If verdict is 'needs-remediation', also provide remediationPlan and use gsd_reassess_roadmap to add remediation slices to the roadmap.", + "On success, returns validationPath where VALIDATION.md was written.", + ], + parameters: Type.Object({ + milestoneId: Type.String({ description: "Milestone ID (e.g. M001)" }), + verdict: StringEnum(["pass", "needs-attention", "needs-remediation"], { description: "Validation verdict" }), + remediationRound: Type.Number({ description: "Remediation round (0 for first validation)" }), + successCriteriaChecklist: Type.String({ description: "Markdown checklist of success criteria with pass/fail and evidence" }), + sliceDeliveryAudit: Type.String({ description: "Markdown table auditing each slice's claimed vs delivered output" }), + crossSliceIntegration: Type.String({ description: "Markdown describing any cross-slice boundary mismatches" }), + requirementCoverage: Type.String({ description: "Markdown describing any unaddressed requirements" }), + verdictRationale: Type.String({ description: "Why this verdict was chosen" }), + remediationPlan: Type.Optional(Type.String({ description: "Remediation plan (required if verdict is needs-remediation)" })), + }), + execute: milestoneValidateExecute, + }; + + pi.registerTool(milestoneValidateTool); + registerAlias(pi, milestoneValidateTool, "gsd_milestone_validate", "gsd_validate_milestone"); + // ─── gsd_replan_slice (gsd_slice_replan alias) ───────────────────────── const replanSliceExecute = async (_toolCallId: string, params: any, _signal: AbortSignal | undefined, _onUpdate: unknown, _ctx: unknown) => { diff --git a/src/resources/extensions/gsd/prompts/complete-milestone.md b/src/resources/extensions/gsd/prompts/complete-milestone.md index 23fc9cfa1..be36a9c88 100644 --- a/src/resources/extensions/gsd/prompts/complete-milestone.md +++ b/src/resources/extensions/gsd/prompts/complete-milestone.md @@ -21,8 +21,8 @@ Then: 4. Verify each **success criterion** from the milestone definition in `{{roadmapPath}}`. For each criterion, confirm it was met with specific evidence from slice summaries, test results, or observable behavior. List any criterion that was NOT met. 5. Verify the milestone's **definition of done** — all slices are `[x]`, all slice summaries exist, and any cross-slice integration points work correctly. 6. Validate **requirement status transitions**. For each requirement that changed status during this milestone, confirm the transition is supported by evidence. Requirements can move between Active, Validated, Deferred, Blocked, or Out of Scope — but only with proof. -7. Write `{{milestoneSummaryPath}}` using the milestone-summary template. Fill all frontmatter fields and narrative sections. The `requirement_outcomes` field must list every requirement that changed status with `from_status`, `to_status`, and `proof`. -8. Update `.gsd/REQUIREMENTS.md` if any requirement status transitions were validated in step 5. +7. **Persist completion through `gsd_complete_milestone`.** Call it with: `milestoneId`, `title`, `oneLiner`, `narrative`, `successCriteriaResults`, `definitionOfDoneResults`, `requirementOutcomes`, `keyDecisions`, `keyFiles`, `lessonsLearned`, `followUps`, `deviations`. The tool updates the milestone status in the DB, renders `{{milestoneSummaryPath}}`, and validates all slices are complete before proceeding. +8. Update `.gsd/REQUIREMENTS.md` if any requirement status transitions were validated in step 6. 9. Update `.gsd/PROJECT.md` to reflect milestone completion and current project state. 10. Review all slice summaries for cross-cutting lessons, patterns, or gotchas that emerged during this milestone. Append any non-obvious, reusable insights to `.gsd/KNOWLEDGE.md`. 11. Do not commit manually — the system auto-commits your changes after this unit completes. @@ -31,6 +31,4 @@ Then: **File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories. -**You MUST write `{{milestoneSummaryPath}}` AND update PROJECT.md before finishing.** - When done, say: "Milestone {{milestoneId}} complete." diff --git a/src/resources/extensions/gsd/prompts/plan-slice.md b/src/resources/extensions/gsd/prompts/plan-slice.md index 3c05f993a..7e6721c48 100644 --- a/src/resources/extensions/gsd/prompts/plan-slice.md +++ b/src/resources/extensions/gsd/prompts/plan-slice.md @@ -63,7 +63,7 @@ Then: - a matching task plan file with description, steps, must-haves, verification, inputs, and expected output - **Inputs and Expected Output must list concrete backtick-wrapped file paths** (e.g. `` `src/types.ts` ``). These are machine-parsed to derive task dependencies — vague prose without paths breaks parallel execution. Every task must have at least one output file path. - Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise -6. **Persist planning state through DB-backed tools.** Call `gsd_plan_slice` with the full slice planning payload (goal, demo, must-haves, verification, tasks, and metadata). Then call `gsd_plan_task` for each task to persist its planning fields. These tools write to the DB and render `{{outputPath}}` and `{{slicePath}}/tasks/T##-PLAN.md` files automatically. Do **not** rely on direct `PLAN.md` writes as the source of truth; the DB-backed tools are the canonical write path for slice and task planning state. +6. **Persist planning state through `gsd_plan_slice`.** Call it with the full slice planning payload (goal, demo, must-haves, verification, tasks, and metadata). The tool inserts all tasks in the same transaction, writes to the DB, and renders `{{outputPath}}` and `{{slicePath}}/tasks/T##-PLAN.md` files automatically. Do **not** call `gsd_plan_task` separately — `gsd_plan_slice` handles task persistence. Do **not** rely on direct `PLAN.md` writes as the source of truth; the DB-backed tool is the canonical write path for slice and task planning state. 7. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on: - **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true. - **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task. diff --git a/src/resources/extensions/gsd/prompts/reassess-roadmap.md b/src/resources/extensions/gsd/prompts/reassess-roadmap.md index b59932c6a..d1a49ceef 100644 --- a/src/resources/extensions/gsd/prompts/reassess-roadmap.md +++ b/src/resources/extensions/gsd/prompts/reassess-roadmap.md @@ -50,14 +50,14 @@ If all criteria have at least one remaining owning slice, the coverage check pas **If the roadmap is still good:** -Write `{{assessmentPath}}` with a brief confirmation that roadmap coverage still holds after {{completedSliceId}}. If requirements exist, explicitly note whether requirement coverage remains sound. If `gsd_reassess_roadmap` is available, use it with `verdict: "roadmap-confirmed"`, an empty `sliceChanges` object, and the assessment text — the tool writes the assessment to the DB and renders ASSESSMENT.md. +Use `gsd_reassess_roadmap` with `verdict: "roadmap-confirmed"`, an empty `sliceChanges` object, and the assessment text — the tool writes the assessment to the DB and renders `{{assessmentPath}}`. If requirements exist, explicitly note whether requirement coverage remains sound. **If changes are needed:** -1. **Persist changes through `gsd_reassess_roadmap`.** Pass: `milestoneId`, `completedSliceId`, `verdict` (e.g. "roadmap-adjusted"), `assessment` (text explaining the decision), and `sliceChanges` with `modified` (array of sliceId, title, risk, depends, demo), `added` (same shape), `removed` (array of slice ID strings). The tool structurally enforces preservation of completed slices, writes the assessment to the DB, re-renders ROADMAP.md, and renders ASSESSMENT.md. Skip step 2 when this tool succeeds. -2. **Degraded fallback — direct file writes:** If `gsd_reassess_roadmap` is not available, rewrite the remaining (unchecked) slices in `{{roadmapPath}}` directly. Keep completed slices exactly as they are (`[x]`). Update the boundary map for changed slices. Update the proof strategy if risks changed. Update requirement coverage if ownership or scope changed. -3. Write `{{assessmentPath}}` explaining what changed and why — keep it brief and concrete. -4. If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it. -5. {{commitInstruction}} +**Persist changes through `gsd_reassess_roadmap`.** Pass: `milestoneId`, `completedSliceId`, `verdict` (e.g. "roadmap-adjusted"), `assessment` (text explaining the decision), and `sliceChanges` with `modified` (array of sliceId, title, risk, depends, demo), `added` (same shape), `removed` (array of slice ID strings). The tool structurally enforces preservation of completed slices, writes the assessment to the DB, re-renders `{{roadmapPath}}`, and renders `{{assessmentPath}}`. + +If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it. + +{{commitInstruction}} When done, say: "Roadmap reassessed." diff --git a/src/resources/extensions/gsd/prompts/replan-slice.md b/src/resources/extensions/gsd/prompts/replan-slice.md index 3185ce02f..f8ec1551a 100644 --- a/src/resources/extensions/gsd/prompts/replan-slice.md +++ b/src/resources/extensions/gsd/prompts/replan-slice.md @@ -32,19 +32,8 @@ Consider these captures when rewriting the remaining tasks — they represent th 1. Read the blocker task summary carefully. Understand exactly what was discovered and why it blocks the current plan. 2. Analyze the remaining `[ ]` tasks in the slice plan. Determine which are still valid, which need modification, and which should be replaced. -3. **Persist replan state through `gsd_replan_slice`.** Call it with the following parameters: `milestoneId`, `sliceId`, `blockerTaskId`, `blockerDescription`, `whatChanged`, `updatedTasks` (array of task objects with taskId, title, description, estimate, files, verify, inputs, expectedOutput), `removedTaskIds` (array of task ID strings). The tool structurally enforces preservation of completed tasks, writes replan history to the DB, re-renders PLAN.md, and renders REPLAN.md. Skip steps 4–5 when this tool succeeds. -4. **Degraded fallback — direct file writes:** If `gsd_replan_slice` is not available, fall back to writing files directly. Write `{{replanPath}}` documenting: - - What blocker was discovered and in which task - - What changed in the plan and why - - Which incomplete tasks were modified, added, or removed - - Any new risks or considerations introduced by the replan -5. If using the degraded fallback, rewrite `{{planPath}}` with the updated slice plan: - - Keep all `[x]` tasks exactly as they were (same IDs, same descriptions, same checkmarks) - - Update the `[ ]` tasks to address the blocker - - Ensure the slice Goal and Demo sections are still achievable with the new tasks, or update them if the blocker fundamentally changes what the slice can deliver - - Update the Files Likely Touched section if the replan changes which files are affected - - If a DB-backed planning tool exists for this phase, use it as the source of truth and make any rewritten `PLAN.md` reflect that persisted state rather than bypassing it -6. If any incomplete task had a `T0x-PLAN.md`, remove or rewrite it to match the new task description. -7. Do not commit manually — the system auto-commits your changes after this unit completes. +3. **Persist replan state through `gsd_replan_slice`.** Call it with: `milestoneId`, `sliceId`, `blockerTaskId`, `blockerDescription`, `whatChanged`, `updatedTasks` (array of task objects with taskId, title, description, estimate, files, verify, inputs, expectedOutput), `removedTaskIds` (array of task ID strings). The tool structurally enforces preservation of completed tasks, writes replan history to the DB, re-renders `{{planPath}}`, and renders `{{replanPath}}`. +4. If any incomplete task had a `T0x-PLAN.md`, remove or rewrite it to match the new task description. +5. Do not commit manually — the system auto-commits your changes after this unit completes. When done, say: "Slice {{sliceId}} replanned." diff --git a/src/resources/extensions/gsd/prompts/validate-milestone.md b/src/resources/extensions/gsd/prompts/validate-milestone.md index 0af036251..170767b6d 100644 --- a/src/resources/extensions/gsd/prompts/validate-milestone.md +++ b/src/resources/extensions/gsd/prompts/validate-milestone.md @@ -16,6 +16,8 @@ All relevant context has been preloaded below — the roadmap, all slice summari {{inlinedContext}} +{{skillActivation}} + ## Validation Steps 1. For each **success criterion** in `{{roadmapPath}}`, check whether slice summaries and UAT results provide evidence that it was met. Record pass/fail per criterion. @@ -25,47 +27,15 @@ All relevant context has been preloaded below — the roadmap, all slice summari 5. Determine a verdict: - `pass` — all criteria met, all slices delivered, no gaps - `needs-attention` — minor gaps that do not block completion (document them) - - `needs-remediation` — material gaps found; add remediation slices to the roadmap + - `needs-remediation` — material gaps found; remediation slices must be added to the roadmap -## Output +## Persist Validation -Write `{{validationPath}}` with this structure: - -```markdown ---- -verdict: -remediation_round: {{remediationRound}} ---- - -# Milestone Validation: {{milestoneId}} - -## Success Criteria Checklist -- [x] Criterion 1 — evidence: ... -- [ ] Criterion 2 — gap: ... - -## Slice Delivery Audit -| Slice | Claimed | Delivered | Status | -|-------|---------|-----------|--------| -| S01 | ... | ... | pass | - -## Cross-Slice Integration -(any boundary mismatches) - -## Requirement Coverage -(any unaddressed requirements) - -## Verdict Rationale -(why this verdict was chosen) - -## Remediation Plan -(only if verdict is needs-remediation — list new slices to add to the roadmap) -``` +**Persist validation results through `gsd_validate_milestone`.** Call it with: `milestoneId`, `verdict`, `remediationRound`, `successCriteriaChecklist`, `sliceDeliveryAudit`, `crossSliceIntegration`, `requirementCoverage`, `verdictRationale`, and `remediationPlan` (if verdict is `needs-remediation`). The tool writes the validation to the DB and renders VALIDATION.md to disk. If verdict is `needs-remediation`: -- Add new slices to `{{roadmapPath}}` with unchecked `[ ]` status -- These slices will be planned and executed before validation re-runs - -**You MUST write `{{validationPath}}` before finishing.** +- After calling `gsd_validate_milestone`, use `gsd_reassess_roadmap` to add remediation slices. Pass `milestoneId`, a synthetic `completedSliceId` (e.g. "VALIDATION"), `verdict: "roadmap-adjusted"`, `assessment` text, and `sliceChanges` with the new slices in the `added` array. The tool persists the changes to the DB and re-renders ROADMAP.md. +- These remediation slices will be planned and executed before validation re-runs. **File system safety:** When scanning milestone directories for evidence, use `ls` or `find` to list directory contents first — never pass a directory path (e.g. `tasks/`, `slices/`) directly to the `read` tool. The `read` tool only accepts file paths, not directories. diff --git a/src/resources/extensions/gsd/tests/prompt-contracts.test.ts b/src/resources/extensions/gsd/tests/prompt-contracts.test.ts index 44e86d8fa..621791dc8 100644 --- a/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +++ b/src/resources/extensions/gsd/tests/prompt-contracts.test.ts @@ -147,12 +147,12 @@ test("plan-slice prompt no longer frames direct PLAN writes as the source of tru assert.match(prompt, /Do \*\*not\*\* rely on direct `PLAN\.md` writes as the source of truth/i); }); -test("plan-slice prompt explicitly names gsd_plan_slice and gsd_plan_task as DB-backed planning tools", () => { +test("plan-slice prompt explicitly names gsd_plan_slice as DB-backed planning tool", () => { const prompt = readPrompt("plan-slice"); assert.match(prompt, /gsd_plan_slice/); assert.match(prompt, /gsd_plan_task/); - // The prompt should describe these as the canonical write path - assert.match(prompt, /DB-backed tools are the canonical write path/i); + // The prompt should describe the DB-backed tool as the canonical write path + assert.match(prompt, /DB-backed tool is the canonical write path/i); }); test("plan-slice prompt does not instruct direct file writes as a primary step", () => { @@ -161,14 +161,18 @@ test("plan-slice prompt does not instruct direct file writes as a primary step", assert.doesNotMatch(prompt, /^\d+\.\s+Write `?\{\{outputPath\}\}`?\s*$/m); }); -test("plan-slice prompt instructs calling gsd_plan_task for each task", () => { +test("plan-slice prompt clarifies gsd_plan_slice handles task persistence", () => { const prompt = readPrompt("plan-slice"); - assert.match(prompt, /call `gsd_plan_task` for each task/i); + // gsd_plan_slice persists tasks in its transaction — no separate gsd_plan_task calls needed + assert.match(prompt, /gsd_plan_task/); + assert.match(prompt, /gsd_plan_slice` handles task persistence/i); }); -test("replan-slice prompt requires DB-backed planning state when available", () => { +test("replan-slice prompt uses gsd_replan_slice as canonical DB-backed tool", () => { const prompt = readPrompt("replan-slice"); - assert.match(prompt, /DB-backed planning tool exists for this phase, use it as the source of truth/i); + assert.match(prompt, /gsd_replan_slice/); + // Degraded fallback (direct file writes) was removed — DB tools are always available + assert.doesNotMatch(prompt, /Degraded fallback/i); }); test("reassess-roadmap prompt references gsd_reassess_roadmap tool", () => { diff --git a/src/resources/extensions/gsd/tests/tool-naming.test.ts b/src/resources/extensions/gsd/tests/tool-naming.test.ts index 1ce5ebe1d..96609f507 100644 --- a/src/resources/extensions/gsd/tests/tool-naming.test.ts +++ b/src/resources/extensions/gsd/tests/tool-naming.test.ts @@ -34,6 +34,7 @@ const RENAME_MAP: Array<{ canonical: string; alias: string }> = [ { canonical: "gsd_replan_slice", alias: "gsd_slice_replan" }, { canonical: "gsd_reassess_roadmap", alias: "gsd_roadmap_reassess" }, { canonical: "gsd_complete_milestone", alias: "gsd_milestone_complete" }, + { canonical: "gsd_validate_milestone", alias: "gsd_milestone_validate" }, ]; // ─── Registration count ────────────────────────────────────────────────────── @@ -43,7 +44,7 @@ console.log('\n── Tool naming: registration count ──'); const pi = makeMockPi(); registerDbTools(pi); -assert.deepStrictEqual(pi.tools.length, 24, 'Should register exactly 24 tools (12 canonical + 12 aliases)'); +assert.deepStrictEqual(pi.tools.length, 26, 'Should register exactly 26 tools (13 canonical + 13 aliases)'); // ─── Both names exist for each pair ────────────────────────────────────────── diff --git a/src/resources/extensions/gsd/tools/validate-milestone.ts b/src/resources/extensions/gsd/tools/validate-milestone.ts new file mode 100644 index 000000000..eae1d8245 --- /dev/null +++ b/src/resources/extensions/gsd/tools/validate-milestone.ts @@ -0,0 +1,127 @@ +/** + * validate-milestone handler — the core operation behind gsd_validate_milestone. + * + * Persists milestone validation results to the assessments table, + * renders VALIDATION.md to disk, and invalidates caches. + */ + +import { join } from "node:path"; + +import { + transaction, + _getAdapter, +} from "../gsd-db.js"; +import { resolveMilestonePath, clearPathCache } from "../paths.js"; +import { saveFile, clearParseCache } from "../files.js"; +import { invalidateStateCache } from "../state.js"; + +export interface ValidateMilestoneParams { + milestoneId: string; + verdict: "pass" | "needs-attention" | "needs-remediation"; + remediationRound: number; + successCriteriaChecklist: string; + sliceDeliveryAudit: string; + crossSliceIntegration: string; + requirementCoverage: string; + verdictRationale: string; + remediationPlan?: string; +} + +export interface ValidateMilestoneResult { + milestoneId: string; + verdict: string; + validationPath: string; +} + +function renderValidationMarkdown(params: ValidateMilestoneParams): string { + let md = `--- +verdict: ${params.verdict} +remediation_round: ${params.remediationRound} +--- + +# Milestone Validation: ${params.milestoneId} + +## Success Criteria Checklist +${params.successCriteriaChecklist} + +## Slice Delivery Audit +${params.sliceDeliveryAudit} + +## Cross-Slice Integration +${params.crossSliceIntegration} + +## Requirement Coverage +${params.requirementCoverage} + +## Verdict Rationale +${params.verdictRationale} +`; + + if (params.verdict === "needs-remediation" && params.remediationPlan) { + md += `\n## Remediation Plan\n${params.remediationPlan}\n`; + } + + return md; +} + +export async function handleValidateMilestone( + params: ValidateMilestoneParams, + basePath: string, +): Promise { + if (!params.milestoneId || typeof params.milestoneId !== "string" || params.milestoneId.trim() === "") { + return { error: "milestoneId is required and must be a non-empty string" }; + } + const validVerdicts = ["pass", "needs-attention", "needs-remediation"]; + if (!validVerdicts.includes(params.verdict)) { + return { error: `verdict must be one of: ${validVerdicts.join(", ")}` }; + } + + // ── Filesystem render ────────────────────────────────────────────────── + const validationMd = renderValidationMarkdown(params); + + let validationPath: string; + const milestoneDir = resolveMilestonePath(basePath, params.milestoneId); + if (milestoneDir) { + validationPath = join(milestoneDir, `${params.milestoneId}-VALIDATION.md`); + } else { + const gsdDir = join(basePath, ".gsd"); + const manualDir = join(gsdDir, "milestones", params.milestoneId); + validationPath = join(manualDir, `${params.milestoneId}-VALIDATION.md`); + } + + try { + await saveFile(validationPath, validationMd); + } catch (renderErr) { + process.stderr.write( + `gsd-db: validate_milestone — disk render failed: ${(renderErr as Error).message}\n`, + ); + return { error: `disk render failed: ${(renderErr as Error).message}` }; + } + + // ── DB write — store in assessments table ────────────────────────────── + const validatedAt = new Date().toISOString(); + + transaction(() => { + const adapter = _getAdapter()!; + adapter.prepare( + `INSERT OR REPLACE INTO assessments (path, milestone_id, slice_id, task_id, status, scope, full_content, created_at) + VALUES (:path, :mid, NULL, NULL, :verdict, 'milestone-validation', :content, :created_at)`, + ).run({ + ":path": validationPath, + ":mid": params.milestoneId, + ":verdict": params.verdict, + ":content": validationMd, + ":created_at": validatedAt, + }); + }); + + invalidateStateCache(); + clearPathCache(); + clearParseCache(); + + return { + milestoneId: params.milestoneId, + verdict: params.verdict, + validationPath, + }; +}