From 62dacb62709a922ba5c08ce679cf08410802bf32 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 20:00:16 +0200 Subject: [PATCH] feat(sf): foundation for mid-execution escalation (ADR-011 P2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type-level + DB scaffolding for the escalation feature gsd-2 has but SF lacks. Pure additive — no behavior change yet. Mirrors the same incremental pattern that worked for progressive planning (types + DDL first, state derivation + dispatch + module port in subsequent fires). PDD spec: Purpose: lay the foundation so a task agent can write tasks.escalation_pending=1 + escalation_artifact_path= when it hits a decision the user must make. Future fires will: (1) add detectPendingEscalation() to state.ts, (2) add a dispatch rule that returns 'stop' on phase='escalating-task', (3) port the escalation helper module from gsd-2. Consumer: task agents (execute-task) when they hit ambiguity that shouldn't be silently resolved. Operators running future /sf escalate list/resolve commands. Contract: - types.ts:23 Phase union now includes 'escalating-task'. - sf-db.ts:370-371 fresh CREATE TABLE for tasks gains escalation_pending + escalation_artifact_path. - sf-db.ts:1430+ schema_version 23 migration adds the columns + an opportunistic index for fast pending-escalation lookups. - TaskRow type gains escalation_pending?: number and escalation_artifact_path?: string | null. rowToTask returns them with safe defaults (0 and null). Failure boundary: index creation is wrapped in try/catch — backends without index support fall through silently. Pre-migration installs treat the column as 0 default (no escalation pending) on first read, matching post-migration default. Evidence: typecheck passes; smoke test deferred to next fire when the state derivation rule lands and we have something observable to test. Non-goals: - state.ts emission of phase='escalating-task' (next fire) - auto-dispatch.ts pause rule (next fire) - escalation.ts helper module port (next fire — 367 LOC in gsd-2) - /sf escalate user command (later fire) - Escalation artifact format/validation (later fire) Invariants: - Safety: ALTER TABLE adds nullable/defaulted columns; existing rows behave identically (escalation_pending defaults to 0). - Liveness: migration runs in same atomic transaction block as other version 23 work — never half-applied. Assumptions verified: - SF already has EscalationOption + EscalationArtifact types (types.ts:692-704) — they were stubs with no producers; this commit is the producer-side scaffolding. - schema_version 22 already exists and is the current latest; 23 is the next available. ADR-011 reference: gsd-2's docs/dev/ADR-011-progressive-planning- escalation.md covers both progressive planning (already ported in this session) and mid-execution escalation (in progress). SF's own ADR-011 file (docs/dev/ADR-011-swarm-chat-and-debate-mode.md) is unrelated to gsd-2's ADR-011 — same number, different topic. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/sf-db.ts | 49 +++++++++++++++++++++++++++- src/resources/extensions/sf/types.ts | 1 + 2 files changed, 49 insertions(+), 1 deletion(-) 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";