fix(gsd): migrate completion/validation prompts to DB-backed tools (#2449)

* fix(gsd): migrate completion/validation prompts to DB-backed tools and fix pipeline inconsistencies (#2444)

- Create gsd_validate_milestone tool (handler + DB registration) using assessments table
- Update complete-milestone.md to use gsd_complete_milestone instead of manual file writes
- Update validate-milestone.md to use gsd_validate_milestone + gsd_reassess_roadmap for remediation
- Add buildSkillActivationBlock() to 4 missing prompt builders (complete-milestone, validate-milestone, run-uat, reassess-roadmap)
- Remove dead completedSliceSummaryPath variable from reassess-roadmap builder
- Remove dead "degraded fallback" sections from replan-slice.md and reassess-roadmap.md
- Fix plan-slice.md double-tool instruction (gsd_plan_slice already persists tasks)
- Fix inconsistent commit/write instructions in complete-milestone.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update tests for new tool registration and prompt changes

- Add gsd_validate_milestone to tool-naming RENAME_MAP (24→26 tools)
- Update prompt-contracts assertions for removed fallback text and singular DB tool phrasing
- Restore {{roadmapPath}}, {{assessmentPath}}, {{planPath}}, {{replanPath}} template vars in prompts for context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore {{milestoneSummaryPath}} template var in complete-milestone prompt

Test expects the milestone summary path reference in the prompt content.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-24 23:08:27 -06:00 committed by GitHub
parent 9a6a341b57
commit b9ff5d5052
10 changed files with 251 additions and 71 deletions

View file

@ -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],
}),
});
}

View file

@ -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) => {

View file

@ -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."

View file

@ -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.

View file

@ -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."

View file

@ -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 45 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."

View file

@ -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: <pass|needs-attention|needs-remediation>
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.

View file

@ -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", () => {

View file

@ -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 ──────────────────────────────────────────

View file

@ -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<ValidateMilestoneResult | { error: string }> {
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,
};
}