From a558ff6c64916f0593dad9f8932dca50ff471913 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 20:07:56 +0200 Subject: [PATCH] feat(sf): dispatch pause-for-escalation rule (PDD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the basic escalation loop. With this commit, end-to-end: - Task agent writes escalation_pending=1 + escalation_artifact_path to the tasks DB row (DB schema from 62dacb627). - State derivation detects the pause and emits phase='escalating-task' with /sf escalate hint in nextAction (ea8819906). - Auto-dispatch sees phase='escalating-task' FIRST in the rule order and returns 'stop' with the nextAction message — no other rule runs. PDD spec: Purpose: never let the loop continue past a pending escalation. Consumer: auto-mode dispatcher (DISPATCH_RULES first entry). Contract: 1. state.phase !== 'escalating-task' → return null (fall through). 2. state.phase === 'escalating-task' → return action='stop' with the state's nextAction (the /sf escalate hint state.ts produced). 3. Rule sits at index 0 of DISPATCH_RULES so phase-agnostic rules below (rewrite-docs, UAT, reassess) cannot bypass it. Failure boundary: pure phase check, no fs/db access — nothing to fail. Evidence: typecheck clean. State derivation already smoke-tested in ea8819906 — once that returns phase='escalating-task', this rule emits the stop. End-to-end happy path is just two function calls. Non-goals: - Tools to write escalation_pending (the producer side — task agents need a tool for this; later fire) - /sf escalate user command (later fire) - Resolution flow (escalation.ts has the schema; resolveEscalation helper from gsd-2 is not yet ported — later fire) Invariants: - Safety: phase !== 'escalating-task' → 1 condition check, return null. Zero overhead in the common case. - Liveness: when paused, dispatch returns immediately — never runs another rule that could mutate slice state. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/resources/extensions/sf/auto-dispatch.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/resources/extensions/sf/auto-dispatch.ts b/src/resources/extensions/sf/auto-dispatch.ts index 630b0af18..ee74aab49 100644 --- a/src/resources/extensions/sf/auto-dispatch.ts +++ b/src/resources/extensions/sf/auto-dispatch.ts @@ -522,6 +522,25 @@ When done, say: "Validation attention remediated; ready for revalidation."`; // ─── Rules ──────────────────────────────────────────────────────────────── export const DISPATCH_RULES: DispatchRule[] = [ + { + // ADR-011 Phase 2 (gsd-2 ADR): mid-execution escalation pause. + // Must evaluate FIRST — phase-agnostic rules below (rewrite-docs gate, + // UAT checks, reassess) cannot bypass a pending user decision. + // state.ts emits phase='escalating-task' only when there's an actionable + // escalation; this rule turns that into a clean 'stop' with the message + // state.nextAction already populated. + name: "escalating-task → pause-for-escalation", + match: async ({ state, mid }) => { + if (state.phase !== "escalating-task") return null; + return { + action: "stop", + reason: + state.nextAction || + `${mid}: task escalation awaits user resolution. Run /sf escalate list to see pending items.`, + level: "info", + }; + }, + }, { name: "rewrite-docs (override gate)", match: async ({ mid, midTitle, state, basePath, session: _session }) => {