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:
parent
99965091d4
commit
62dacb6270
2 changed files with 49 additions and 1 deletions
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export type Phase =
|
|||
| "validating-milestone"
|
||||
| "completing-milestone"
|
||||
| "replanning-slice"
|
||||
| "escalating-task"
|
||||
| "complete"
|
||||
| "paused"
|
||||
| "blocked";
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue