diff --git a/src/resources/extensions/sf/sf-db.ts b/src/resources/extensions/sf/sf-db.ts index a4ad583f1..55caae6f4 100644 --- a/src/resources/extensions/sf/sf-db.ts +++ b/src/resources/extensions/sf/sf-db.ts @@ -130,7 +130,7 @@ function openRawDb(path: string): unknown { return new DatabaseSync(path); } -const SCHEMA_VERSION = 21; +const SCHEMA_VERSION = 23; function indexExists(db: DbAdapter, name: string): boolean { return !!db @@ -367,11 +367,17 @@ function initSchema(db: DbAdapter, fileBacked: boolean): void { full_plan_md TEXT NOT NULL DEFAULT '', verification_status TEXT NOT NULL DEFAULT '', sequence INTEGER DEFAULT 0, -- Ordering hint: tools may set this to control execution order + escalation_pending INTEGER NOT NULL DEFAULT 0, -- ADR-011 P2 (gsd-2): pause-on-escalation flag + escalation_artifact_path TEXT DEFAULT NULL, -- ADR-011 P2 (gsd-2): path to T##-ESCALATION.json PRIMARY KEY (milestone_id, slice_id, id), FOREIGN KEY (milestone_id, slice_id) REFERENCES slices(milestone_id, id) ) `); + db.exec(` + CREATE INDEX IF NOT EXISTS idx_tasks_escalation_pending ON tasks(milestone_id, slice_id, escalation_pending) + `); + db.exec(` CREATE TABLE IF NOT EXISTS verification_evidence ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -1423,6 +1429,40 @@ function migrateSchema(db: DbAdapter): void { }); } + if (currentVersion < 23) { + // ADR-011 Phase 2 (gsd-2 ADR): mid-execution escalation. escalation_pending=1 + // marks a task that paused for a user decision; escalation_artifact_path + // points to the T##-ESCALATION.json file containing options + recommendation. + // State derivation will emit phase='escalating-task' when any task in the + // active slice has escalation_pending=1; dispatch returns 'stop' so the + // loop never bypasses a pending decision. + ensureColumn( + db, + "tasks", + "escalation_pending", + `ALTER TABLE tasks ADD COLUMN escalation_pending INTEGER NOT NULL DEFAULT 0`, + ); + ensureColumn( + db, + "tasks", + "escalation_artifact_path", + `ALTER TABLE tasks ADD COLUMN escalation_artifact_path TEXT DEFAULT NULL`, + ); + try { + db.exec( + "CREATE INDEX IF NOT EXISTS idx_tasks_escalation_pending ON tasks(milestone_id, slice_id, escalation_pending)", + ); + } catch { + /* index creation is opportunistic — fall through if backend lacks it */ + } + db.prepare( + "INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)", + ).run({ + ":version": 23, + ":applied_at": new Date().toISOString(), + }); + } + db.exec("COMMIT"); } catch (err) { db.exec("ROLLBACK"); @@ -2506,6 +2546,10 @@ export interface TaskRow { full_plan_md: string; sequence: number; verification_status?: string; + /** ADR-011 P2: 1 = task is paused waiting for the user to resolve an escalation. */ + escalation_pending?: number; + /** ADR-011 P2: relative path to the T##-ESCALATION.json artifact next to T##-PLAN.md. */ + escalation_artifact_path?: string | null; } function safeParseJsonArray( @@ -2597,6 +2641,9 @@ function rowToTask(row: Record): TaskRow { full_plan_md: (row["full_plan_md"] as string) ?? "", sequence: (row["sequence"] as number) ?? 0, verification_status: (row["verification_status"] as string) ?? "", + escalation_pending: (row["escalation_pending"] as number) ?? 0, + escalation_artifact_path: + (row["escalation_artifact_path"] as string | null) ?? null, }; } diff --git a/src/resources/extensions/sf/types.ts b/src/resources/extensions/sf/types.ts index c81aa2676..86e948a90 100644 --- a/src/resources/extensions/sf/types.ts +++ b/src/resources/extensions/sf/types.ts @@ -20,6 +20,7 @@ export type Phase = | "validating-milestone" | "completing-milestone" | "replanning-slice" + | "escalating-task" | "complete" | "paused" | "blocked";