From f757a18417e561d237bd950d75eff441bd6a33ce Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Sat, 2 May 2026 20:31:45 +0200 Subject: [PATCH] feat(sf): /sf escalate user command + resolveEscalation (PDD) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the user-facing loop for ADR-011 P2. The full escalation end-to-end now works: agent files → loop pauses → user resolves via /sf escalate → loop continues. PDD spec for this change: Purpose: let the user resolve a paused task escalation. Without this, escalation_pending=1 has no exit ramp other than manual SQL. Consumer: users at the prompt — '/sf escalate list', '/sf escalate show /', '/sf escalate resolve / [-- ]'. Contract: 1. /sf escalate list → enumerate pending escalations in the active milestone, showing slice/task, question, options, recommendation. 2. /sf escalate show / → print the artifact's question + options with tradeoffs + recommendation + resolution status (resolved or unresolved). 3. /sf escalate resolve / [-- ] → resolveEscalation in escalation.ts: - 'accept' selects the recommended option - any option id from the artifact is also valid - invalid choice → returns 'invalid-choice' with valid list - already resolved → 'already-resolved' with prior timestamp - not found → 'not-found' with the task path On success: artifact gains respondedAt/userChoice/userRationale, DB flags cleared, UOK audit event 'escalation-user-responded' emitted. Failure boundary: - DB unavailable → 'SF database is not available. Run /sf doctor.' - Active milestone missing → 'No active milestone — nothing to list.' - Malformed artifact path → readEscalationArtifact returns null → handler returns 'not-found'. - clearTaskEscalationFlags called inside the resolver — never leaves the row in a half-resolved state. Evidence: smoke test exercises 4 contract conditions end-to-end: invalid-choice, accept→resolved (chosen option = recommendation), already-resolved on re-run, not-found for unknown task. Typecheck clean. Non-goals: - reject-blocker choice (gsd-2 has it; needs a blocker_source DB column SF doesn't have) - Carry-forward injection (claimEscalationOverride — findUnappliedEscalationOverride flow). The override is logged in the artifact for the user; agent context injection lands when the executor's prompt builder is wired to read it. - Cross-milestone listing (current implementation: active milestone only — matches /sf escalate list's most useful default behavior). Invariants: - Safety: invalid-choice and not-found return without writing — no half-state. - Safety: clearTaskEscalationFlags zeros pending+awaiting in one UPDATE — reader can never see half-cleared state. - Liveness: after resolve, next state derivation cycle sees escalation_pending=0 → phase != 'escalating-task' → dispatch routes normally. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../extensions/sf/commands-escalate.ts | 189 ++++++++++++++++++ .../extensions/sf/commands/catalog.ts | 1 + .../extensions/sf/commands/handlers/ops.ts | 5 + src/resources/extensions/sf/escalation.ts | 114 ++++++++++- 4 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 src/resources/extensions/sf/commands-escalate.ts diff --git a/src/resources/extensions/sf/commands-escalate.ts b/src/resources/extensions/sf/commands-escalate.ts new file mode 100644 index 000000000..19534a3ae --- /dev/null +++ b/src/resources/extensions/sf/commands-escalate.ts @@ -0,0 +1,189 @@ +// SF Command — `/sf escalate` (ADR-011 P2) +// +// Subcommands: +// list — show pending escalations across all active milestones +// show / — print the escalation question + options for one task +// resolve /