diff --git a/src/resources/extensions/sf/bootstrap/db-tools.ts b/src/resources/extensions/sf/bootstrap/db-tools.ts index ad7f15455..48df9c4ec 100644 --- a/src/resources/extensions/sf/bootstrap/db-tools.ts +++ b/src/resources/extensions/sf/bootstrap/db-tools.ts @@ -1358,6 +1358,49 @@ export function registerDbTools(pi: ExtensionAPI): void { description: "Whether a plan-invalidating blocker was discovered", }), ), + // ADR-011 Phase 2: mid-execution escalation — agent flags an ambiguity + // for the user. Only honored when phases.mid_execution_escalation=true. + escalation: Type.Optional( + Type.Object( + { + question: Type.String({ + description: + "The question the user needs to answer — one clear sentence.", + }), + options: Type.Array( + Type.Object({ + id: Type.String({ + description: + "Short id (e.g. 'A', 'B') used by /sf escalate resolve.", + }), + label: Type.String({ description: "One-line label." }), + tradeoffs: Type.String({ + description: "1-2 sentences on the tradeoffs of this option.", + }), + }), + { + minItems: 2, + maxItems: 4, + description: "2–4 options the user can choose between.", + }, + ), + recommendation: Type.String({ + description: "Option id the executor recommends.", + }), + recommendationRationale: Type.String({ + description: "Why the recommendation — 1–2 sentences.", + }), + continueWithDefault: Type.Boolean({ + description: + "When true, loop continues (artifact logged for later review). When false, auto-mode pauses until the user resolves via /sf escalate resolve.", + }), + }, + { + description: + "ADR-011 P2: optional escalation payload. Only honored when phases.mid_execution_escalation is true.", + }, + ), + ), verificationEvidence: Type.Optional( Type.Array( Type.Union([ diff --git a/src/resources/extensions/sf/preferences-validation.ts b/src/resources/extensions/sf/preferences-validation.ts index f88bf496e..9da653b70 100644 --- a/src/resources/extensions/sf/preferences-validation.ts +++ b/src/resources/extensions/sf/preferences-validation.ts @@ -766,6 +766,12 @@ export function validatePreferences(preferences: SFPreferences): { if ((p as any).require_slice_discussion !== undefined) (validatedPhases as any).require_slice_discussion = !!(p as any) .require_slice_discussion; + if ((p as any).mid_execution_escalation !== undefined) + (validatedPhases as any).mid_execution_escalation = !!(p as any) + .mid_execution_escalation; + if ((p as any).progressive_planning !== undefined) + (validatedPhases as any).progressive_planning = !!(p as any) + .progressive_planning; // Warn on unknown phase keys const knownPhaseKeys = new Set([ "skip_research", @@ -774,6 +780,8 @@ export function validatePreferences(preferences: SFPreferences): { "skip_milestone_validation", "reassess_after_slice", "require_slice_discussion", + "mid_execution_escalation", + "progressive_planning", ]); for (const key of Object.keys(p)) { if (!knownPhaseKeys.has(key)) { diff --git a/src/resources/extensions/sf/tools/workflow-tool-executors.ts b/src/resources/extensions/sf/tools/workflow-tool-executors.ts index d1da72d47..e172b4082 100644 --- a/src/resources/extensions/sf/tools/workflow-tool-executors.ts +++ b/src/resources/extensions/sf/tools/workflow-tool-executors.ts @@ -218,6 +218,18 @@ type VerificationEvidenceInput = } | string; +/** ADR-011 Phase 2: optional escalation payload on sf_task_complete. + * When phases.mid_execution_escalation is enabled, the executor calls + * buildEscalationArtifact + writeEscalationArtifact after the task is + * recorded, flipping the appropriate task escalation flag. */ +export interface TaskCompleteEscalationInput { + question: string; + options: Array<{ id: string; label: string; tradeoffs: string }>; + recommendation: string; + recommendationRationale: string; + continueWithDefault: boolean; +} + export interface TaskCompleteParams { taskId: string; sliceId: string; @@ -231,6 +243,7 @@ export interface TaskCompleteParams { keyDecisions?: string[]; blockerDiscovered?: boolean; verificationEvidence?: VerificationEvidenceInput[]; + escalation?: TaskCompleteEscalationInput; } export type CompleteMilestoneExecutorParams = Partial & @@ -293,11 +306,57 @@ export async function executeTaskComplete( isError: true, }; } + + // ADR-011 P2: optional escalation payload. Only honored when the + // phases.mid_execution_escalation preference is true. When false (default), + // any escalation field on the params is silently ignored — keeps the + // payload backwards-compatible for callers that always send it. + let escalationPath: string | undefined; + let escalationStatus: "pending" | "awaiting-review" | undefined; + if (params.escalation) { + try { + const { loadEffectiveSFPreferences } = await import( + "../preferences.js" + ); + const prefs = loadEffectiveSFPreferences()?.preferences; + if (prefs?.phases?.mid_execution_escalation === true) { + const { buildEscalationArtifact, writeEscalationArtifact } = + await import("../escalation.js"); + const artifact = buildEscalationArtifact({ + taskId: result.taskId, + sliceId: result.sliceId, + milestoneId: result.milestoneId, + question: params.escalation.question, + options: params.escalation.options, + recommendation: params.escalation.recommendation, + recommendationRationale: + params.escalation.recommendationRationale, + continueWithDefault: params.escalation.continueWithDefault, + }); + escalationPath = writeEscalationArtifact(basePath, artifact); + escalationStatus = params.escalation.continueWithDefault + ? "awaiting-review" + : "pending"; + } + } catch (err) { + // Escalation is additive — never block task completion if it fails. + logError( + "tool", + `sf_task_complete escalation write failed: ${err instanceof Error ? err.message : String(err)}`, + { tool: "sf_task_complete", op: "escalation" }, + ); + } + } + + const baseText = `Completed task ${result.taskId} (${result.sliceId}/${result.milestoneId})`; + const escalationSuffix = escalationStatus + ? ` — escalation ${escalationStatus} at ${escalationPath}` + : ""; return { content: [ { type: "text", - text: `Completed task ${result.taskId} (${result.sliceId}/${result.milestoneId})`, + text: baseText + escalationSuffix, }, ], details: { @@ -306,6 +365,8 @@ export async function executeTaskComplete( sliceId: result.sliceId, milestoneId: result.milestoneId, summaryPath: result.summaryPath, + ...(escalationPath ? { escalationArtifactPath: escalationPath } : {}), + ...(escalationStatus ? { escalationStatus } : {}), }, }; } catch (err) {