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) <noreply@anthropic.com>
This commit is contained in:
TÂCHES 2026-03-24 23:40:56 -06:00 committed by GitHub
parent 98f5daeda8
commit c77148632b
5 changed files with 24 additions and 2 deletions

View file

@ -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<string, unknown>): 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,
};
}

View file

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

View file

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

View file

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

View file

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