feat(sf): foundation for mid-execution escalation (ADR-011 P2)

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=<file> 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) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-02 20:00:16 +02:00
parent 99965091d4
commit 62dacb6270
2 changed files with 49 additions and 1 deletions

View file

@ -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<T = unknown>(
@ -2597,6 +2641,9 @@ function rowToTask(row: Record<string, unknown>): 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,
};
}

View file

@ -20,6 +20,7 @@ export type Phase =
| "validating-milestone"
| "completing-milestone"
| "replanning-slice"
| "escalating-task"
| "complete"
| "paused"
| "blocked";