From c77148632bfc77455a408f43c72df8417ee00592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Tue, 24 Mar 2026 23:40:56 -0600 Subject: [PATCH] fix(gsd): preserve rich task plans on DB roundtrip (#2450) (#2453) Add `full_plan_md` TEXT column to the tasks table, following the established `full_summary_md` pattern. When populated, `renderTaskPlanFromDb()` writes the stored markdown directly instead of regenerating a minimal version from individual DB fields. - DB schema: add `full_plan_md` column (migration v11) - `TaskPlanningRecord` / `upsertTaskPlanning`: accept and persist `fullPlanMd` - `renderTaskPlanFromDb`: prefer `full_plan_md` when non-empty - plan-task, plan-slice, replan-slice tools: pass `fullPlanMd` through Co-authored-by: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/gsd-db.ts | 17 ++++++++++++++++- .../extensions/gsd/markdown-renderer.ts | 2 +- .../extensions/gsd/tools/plan-slice.ts | 2 ++ src/resources/extensions/gsd/tools/plan-task.ts | 2 ++ .../extensions/gsd/tools/replan-slice.ts | 3 +++ 5 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index eb05aa6ee..a32001cf3 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -301,6 +301,7 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { inputs TEXT NOT NULL DEFAULT '[]', expected_output TEXT NOT NULL DEFAULT '[]', observability_impact TEXT NOT NULL DEFAULT '', + full_plan_md TEXT NOT NULL DEFAULT '', sequence INTEGER DEFAULT 0, -- DEAD CODE: no tool exposes sequence — always 0 PRIMARY KEY (milestone_id, slice_id, id), FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id) @@ -616,6 +617,15 @@ function migrateSchema(db: DbAdapter): void { }); } + if (currentVersion < 11) { + ensureColumn(db, "tasks", "full_plan_md", `ALTER TABLE tasks ADD COLUMN full_plan_md TEXT NOT NULL DEFAULT ''`); + + db.prepare("INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)").run({ + ":version": 11, + ":applied_at": new Date().toISOString(), + }); + } + db.exec("COMMIT"); } catch (err) { db.exec("ROLLBACK"); @@ -923,6 +933,7 @@ export interface TaskPlanningRecord { inputs: string[]; expectedOutput: string[]; observabilityImpact: string; + fullPlanMd?: string; } export function insertMilestone(m: { @@ -1163,7 +1174,8 @@ export function upsertTaskPlanning(milestoneId: string, sliceId: string, taskId: verify = COALESCE(:verify, verify), inputs = COALESCE(:inputs, inputs), expected_output = COALESCE(:expected_output, expected_output), - observability_impact = COALESCE(:observability_impact, observability_impact) + observability_impact = COALESCE(:observability_impact, observability_impact), + full_plan_md = COALESCE(:full_plan_md, full_plan_md) WHERE milestone_id = :milestone_id AND slice_id = :slice_id AND id = :id`, ).run({ ":milestone_id": milestoneId, @@ -1177,6 +1189,7 @@ export function upsertTaskPlanning(milestoneId: string, sliceId: string, taskId: ":inputs": planning.inputs ? JSON.stringify(planning.inputs) : null, ":expected_output": planning.expectedOutput ? JSON.stringify(planning.expectedOutput) : null, ":observability_impact": planning.observabilityImpact ?? null, + ":full_plan_md": planning.fullPlanMd ?? null, }); } @@ -1268,6 +1281,7 @@ export interface TaskRow { inputs: string[]; expected_output: string[]; observability_impact: string; + full_plan_md: string; sequence: number; } @@ -1296,6 +1310,7 @@ function rowToTask(row: Record): TaskRow { inputs: JSON.parse((row["inputs"] as string) || "[]"), expected_output: JSON.parse((row["expected_output"] as string) || "[]"), observability_impact: (row["observability_impact"] as string) ?? "", + full_plan_md: (row["full_plan_md"] as string) ?? "", sequence: (row["sequence"] as number) ?? 0, }; } diff --git a/src/resources/extensions/gsd/markdown-renderer.ts b/src/resources/extensions/gsd/markdown-renderer.ts index 551ce010c..0afc7d140 100644 --- a/src/resources/extensions/gsd/markdown-renderer.ts +++ b/src/resources/extensions/gsd/markdown-renderer.ts @@ -387,7 +387,7 @@ export async function renderTaskPlanFromDb( mkdirSync(tasksDir, { recursive: true }); const absPath = join(tasksDir, buildTaskFileName(taskId, "PLAN")); const artifactPath = toArtifactPath(absPath, basePath); - const content = renderTaskPlanMarkdown(task); + const content = task.full_plan_md.trim() ? task.full_plan_md : renderTaskPlanMarkdown(task); await writeAndStore(absPath, artifactPath, content, { artifact_type: "PLAN", diff --git a/src/resources/extensions/gsd/tools/plan-slice.ts b/src/resources/extensions/gsd/tools/plan-slice.ts index f430e9756..2a9d648eb 100644 --- a/src/resources/extensions/gsd/tools/plan-slice.ts +++ b/src/resources/extensions/gsd/tools/plan-slice.ts @@ -20,6 +20,7 @@ export interface PlanSliceTaskInput { inputs: string[]; expectedOutput: string[]; observabilityImpact?: string; + fullPlanMd?: string; } export interface PlanSliceParams { @@ -167,6 +168,7 @@ export async function handlePlanSlice( inputs: task.inputs, expectedOutput: task.expectedOutput, observabilityImpact: task.observabilityImpact ?? "", + fullPlanMd: task.fullPlanMd, }); } }); diff --git a/src/resources/extensions/gsd/tools/plan-task.ts b/src/resources/extensions/gsd/tools/plan-task.ts index 94826b4c3..7d91a49e8 100644 --- a/src/resources/extensions/gsd/tools/plan-task.ts +++ b/src/resources/extensions/gsd/tools/plan-task.ts @@ -15,6 +15,7 @@ export interface PlanTaskParams { inputs: string[]; expectedOutput: string[]; observabilityImpact?: string; + fullPlanMd?: string; } export interface PlanTaskResult { @@ -94,6 +95,7 @@ export async function handlePlanTask( inputs: params.inputs, expectedOutput: params.expectedOutput, observabilityImpact: params.observabilityImpact ?? "", + fullPlanMd: params.fullPlanMd, }); }); } catch (err) { diff --git a/src/resources/extensions/gsd/tools/replan-slice.ts b/src/resources/extensions/gsd/tools/replan-slice.ts index 2d9c1a066..1e103327e 100644 --- a/src/resources/extensions/gsd/tools/replan-slice.ts +++ b/src/resources/extensions/gsd/tools/replan-slice.ts @@ -21,6 +21,7 @@ export interface ReplanSliceTaskInput { verify: string; inputs: string[]; expectedOutput: string[]; + fullPlanMd?: string; } export interface ReplanSliceParams { @@ -136,6 +137,7 @@ export async function handleReplanSlice( verify: updatedTask.verify || "", inputs: updatedTask.inputs || [], expectedOutput: updatedTask.expectedOutput || [], + fullPlanMd: updatedTask.fullPlanMd, }); } else { // Insert new task then set planning fields @@ -154,6 +156,7 @@ export async function handleReplanSlice( verify: updatedTask.verify || "", inputs: updatedTask.inputs || [], expectedOutput: updatedTask.expectedOutput || [], + fullPlanMd: updatedTask.fullPlanMd, }); } }