# Feature Spec: Dynamic Agent Swapping for Blocked Features (AOP)

> **Purpose of this document**: Define an implementation-ready design for dynamically swapping agent providers (model, provider, or configuration) when a feature reaches a blocked state, enabling automatic escalation to more capable agents or alternative reasoning approaches while preserving deterministic orchestration, session continuity, and state consistency.

**Version:** 1.0  
**Date:** 2026-03-05  
**Status:** Draft  
**Roadmap Mapping:** M48

---

## 0. Standards and Dependencies

### 0.1 Required Standards

All implementation MUST preserve:

- deterministic orchestration and normalized error envelopes
- MCP/in-process tool contract parity
- role-based tool authorization and `operation_id` idempotency on mutating tools
- canonical runtime artifacts under `.aop/**`
- existing feature topology invariants (`1 + 3N` logical sessions)
- coverage thresholds: Lines >=90%, Branches >=90%, Functions >=90%, Statements >=90%

### 0.2 Required Upstream Inputs

Implementing agents MUST read:

- `spec-files/completed/agentic_orchestrator_spec.md`
- `spec-files/outstanding/agentic_orchestrator_persistent_worker_runtime_spec.md`
- `apps/control-plane/src/providers/providers.ts`
- `apps/control-plane/src/providers/cli-worker-provider.ts`
- `apps/control-plane/src/providers/api-worker-provider.ts`
- `apps/control-plane/src/providers/worker-provider-factory.ts`
- `apps/control-plane/src/supervisor/session-orchestrator.ts`
- `apps/control-plane/src/supervisor/runtime.ts`
- `apps/control-plane/src/supervisor/run-coordinator.ts`
- `apps/control-plane/src/supervisor/build-wave-executor.ts`
- `apps/control-plane/src/supervisor/qa-wave-executor.ts`
- `apps/control-plane/src/supervisor/planning-wave-executor.ts`
- `apps/control-plane/src/application/services/reactions-service.ts`
- `apps/control-plane/src/application/services/activity-monitor-service.ts`
- `agentic/orchestrator/agents.yaml`
- `agentic/orchestrator/schemas/agents.schema.json`
- `agentic/orchestrator/policy.yaml`
- `agentic/orchestrator/schemas/policy.schema.json`
- `agentic/orchestrator/schemas/state.schema.json`

### 0.3 Scope

This spec implements:

- automatic detection of blocked features requiring agent escalation
- configurable escalation policies per blocked reason (collision, gate failure, idle, budget)
- dynamic provider/model/config swapping for individual feature clusters
- session migration with context preservation
- rollback capability when escalation fails
- observability artifacts for swap events and outcomes
- cost tracking for escalated agent usage

Out of scope:

- changing core orchestration loop or phase transitions
- modifying plan/patch/gate semantics
- replacing role-level tool authorization rules
- changing merge governance and approval policy
- automatic downgrade after successful unblock (manual only in v1)

### 0.4 OOP and Architecture Guardrails (Mandatory)

Implementation MUST follow:

- **KISS**: one escalation manager with pluggable policy evaluators
- **SRP**: escalation decision, session migration, and provider selection remain in separate modules
- **SOLID-oriented design**:
  - open/closed extension for new escalation policies without supervisor branching
  - interface segregation between escalation policy and session lifecycle
  - dependency inversion from supervisor to escalation/provider ports
- **Hexagonal alignment**: escalation logic and provider swapping remain in `supervisor/*` and `providers/*`, not in kernel
- **Composition over inheritance**: escalation manager composed into run coordinator
- **High cohesion / low coupling**: no direct state/plan logic inside escalation manager

---

## 1. Problem Statement and Motivation

### 1.1 Current Behavior

When a feature reaches `blocked` status, the current system:

- marks the feature as blocked in state
- notifies via configured channels (desktop, Slack, webhook)
- waits for manual intervention or automatic retry (gate failures only)
- continues using the same agent provider/model/config for all retry attempts

This means:

- features blocked by model limitations (context window, reasoning capability, knowledge cutoff) remain stuck
- no automatic escalation to more capable agents
- manual intervention required to change provider/model
- no systematic learning from which agents succeed on which types of blocks

### 1.2 Blocked State Triggers

Features can reach `blocked` status via multiple paths:

1. **Collision-blocked** (`planning` phase): plan submission rejected due to file/area/contract collision with another active feature
2. **Gate-blocked** (`building` or `qa` phase): gate execution failed after max retries exhausted
3. **Idle-blocked** (any phase): agent appears stuck (no progress for `idle_threshold_ms`)
4. **Budget-blocked** (any phase): feature cost exceeds `per_feature_limit_usd` (status becomes `paused_budget`, not `blocked`)

### 1.3 Why Dynamic Agent Swapping?

Different agent providers have different strengths:

- **Claude Sonnet 3.5**: excellent reasoning, large context window, strong code generation
- **GPT-4**: strong general knowledge, good at following complex instructions
- **Codex/Copilot**: fast, cost-effective for simple changes
- **Local models**: privacy-preserving, no API costs, but limited capability

Automatic escalation enables:

- **Resilience**: blocked features can automatically try a more capable agent
- **Cost optimization**: start with cheaper agents, escalate only when needed
- **Learning**: track which agents succeed on which types of blocks
- **Reduced manual intervention**: fewer features requiring human unblocking

---

## 2. Architecture Overview

### 2.1 Key Components

```
RunCoordinator
  └─> EscalationManager
        ├─> EscalationPolicyEvaluator (per blocked reason)
        ├─> ProviderSwapOrchestrator
        │     ├─> SessionMigrator
        │     └─> WorkerProviderFactory
        └─> EscalationLedger (observability)
```

### 2.2 Escalation Flow

```
1. Feature reaches blocked status
2. RunCoordinator detects blocked feature in rebalance
3. EscalationManager.shouldEscalate(featureId, blockedReason) → policy decision
4. If yes: EscalationManager.escalate(featureId, blockedReason)
   a. Resolve target provider/model/config from policy
   b. Close existing feature cluster sessions
   c. Create new WorkerProvider instance with target config
   d. Create new feature cluster sessions with new provider
   e. Update state.cluster with new session IDs
   f. Update state.escalation metadata
   g. Record escalation event in ledger
5. Feature returns to active pool with new agent
6. Normal orchestration loop continues
```

### 2.3 Session Migration Strategy

**Option A: Clean slate (v1 implementation)**

- Close all existing sessions (planner, builder, qa)
- Create new sessions with new provider
- Agents read full context from state/plan/evidence
- No explicit context transfer

**Option B: Context transfer (future enhancement)**

- Extract conversation history from old sessions
- Inject summary into new session system prompts
- Preserve reasoning continuity

This spec implements **Option A** for simplicity and determinism.

---

## 3. Configuration Schema Extensions

### 3.1 `agents.yaml` Extensions

Add new `escalation` section to `agents.yaml`:

```yaml
version: 1
roles:
  planner:
    system_prompt_path: agentic/orchestrator/prompts/planner.system.md
  builder:
    system_prompt_path: agentic/orchestrator/prompts/builder.system.md
  qa:
    system_prompt_path: agentic/orchestrator/prompts/qa.system.md

runtime:
  default_provider: custom
  default_model: local-default
  # ... existing runtime config ...

escalation:
  enabled: true
  max_escalations_per_feature: 2
  cooldown_between_escalations_ms: 60000

  policies:
    collision_blocked:
      enabled: true
      escalate_after_iterations: 2
      target:
        provider: claude
        model: claude-sonnet-3.5-20241022
        provider_config_ref: '${ANTHROPIC_API_KEY}'

    gate_blocked:
      enabled: true
      escalate_after_retries: 2
      target:
        provider: claude
        model: claude-sonnet-3.5-20241022
        provider_config_ref: '${ANTHROPIC_API_KEY}'

    idle_blocked:
      enabled: true
      escalate_after_idle_ms: 600000
      target:
        provider: claude
        model: claude-sonnet-3.5-20241022
        provider_config_ref: '${ANTHROPIC_API_KEY}'

    budget_blocked:
      enabled: false
      # Budget-blocked features are paused, not escalated by default

  # Optional: role-specific escalation targets
  role_overrides:
    planner:
      provider: claude
      model: claude-sonnet-3.5-20241022
    builder:
      provider: codex
      model: gpt-4-turbo
    qa:
      provider: claude
      model: claude-sonnet-3.5-20241022
```

### 3.2 `agents.schema.json` Extensions

Add to `agentic/orchestrator/schemas/agents.schema.json`:

```json
{
  "properties": {
    "escalation": {
      "type": "object",
      "description": "Dynamic agent swapping configuration for blocked features",
      "properties": {
        "enabled": {
          "type": "boolean",
          "default": false
        },
        "max_escalations_per_feature": {
          "type": "integer",
          "minimum": 0,
          "maximum": 5,
          "default": 2
        },
        "cooldown_between_escalations_ms": {
          "type": "integer",
          "minimum": 0,
          "default": 60000
        },
        "policies": {
          "type": "object",
          "properties": {
            "collision_blocked": { "$ref": "#/$defs/escalation_policy" },
            "gate_blocked": { "$ref": "#/$defs/escalation_policy" },
            "idle_blocked": { "$ref": "#/$defs/escalation_policy" },
            "budget_blocked": { "$ref": "#/$defs/escalation_policy" }
          }
        },
        "role_overrides": {
          "type": "object",
          "properties": {
            "planner": { "$ref": "#/$defs/provider_selection" },
            "builder": { "$ref": "#/$defs/provider_selection" },
            "qa": { "$ref": "#/$defs/provider_selection" }
          }
        }
      }
    }
  },
  "$defs": {
    "escalation_policy": {
      "type": "object",
      "properties": {
        "enabled": { "type": "boolean", "default": false },
        "escalate_after_iterations": { "type": "integer", "minimum": 1 },
        "escalate_after_retries": { "type": "integer", "minimum": 1 },
        "escalate_after_idle_ms": { "type": "integer", "minimum": 0 },
        "target": { "$ref": "#/$defs/provider_selection" }
      }
    },
    "provider_selection": {
      "type": "object",
      "required": ["provider", "model"],
      "properties": {
        "provider": { "type": "string" },
        "model": { "type": "string" },
        "provider_config_ref": { "type": "string" }
      }
    }
  }
}
```

### 3.3 `state.schema.json` Extensions

Add to `agentic/orchestrator/schemas/state.schema.json`:

```json
{
  "properties": {
    "escalation": {
      "type": "object",
      "description": "Agent escalation history for this feature",
      "properties": {
        "count": {
          "type": "integer",
          "minimum": 0,
          "description": "Number of times this feature has been escalated"
        },
        "history": {
          "type": "array",
          "items": {
            "type": "object",
            "required": ["timestamp", "reason", "from", "to"],
            "properties": {
              "timestamp": { "type": "string", "format": "date-time" },
              "reason": {
                "type": "string",
                "enum": ["collision_blocked", "gate_blocked", "idle_blocked", "manual"]
              },
              "from": {
                "type": "object",
                "properties": {
                  "provider": { "type": "string" },
                  "model": { "type": "string" }
                }
              },
              "to": {
                "type": "object",
                "properties": {
                  "provider": { "type": "string" },
                  "model": { "type": "string" }
                }
              },
              "outcome": {
                "type": "string",
                "enum": ["pending", "unblocked", "still_blocked", "failed", "rolled_back"]
              }
            }
          }
        },
        "last_escalation_at": {
          "type": "string",
          "format": "date-time"
        },
        "current_provider": {
          "type": "object",
          "properties": {
            "provider": { "type": "string" },
            "model": { "type": "string" },
            "escalated": { "type": "boolean" }
          }
        }
      }
    }
  }
}
```

---

## 4. Implementation Components

### 4.1 EscalationManager

**Location:** `apps/control-plane/src/supervisor/escalation-manager.ts`

**Responsibilities:**

- Evaluate whether a blocked feature should be escalated
- Orchestrate provider swap and session migration
- Track escalation history and outcomes
- Enforce escalation limits and cooldowns

**Interface:**

```typescript
export interface EscalationConfig {
  enabled: boolean;
  max_escalations_per_feature: number;
  cooldown_between_escalations_ms: number;
  policies: {
    collision_blocked?: EscalationPolicy;
    gate_blocked?: EscalationPolicy;
    idle_blocked?: EscalationPolicy;
    budget_blocked?: EscalationPolicy;
  };
  role_overrides?: {
    planner?: ProviderSelection;
    builder?: ProviderSelection;
    qa?: ProviderSelection;
  };
}

export interface EscalationPolicy {
  enabled: boolean;
  escalate_after_iterations?: number;
  escalate_after_retries?: number;
  escalate_after_idle_ms?: number;
  target: ProviderSelection;
}

export interface ProviderSelection {
  provider: string;
  model: string;
  provider_config_ref?: string;
}

export type BlockedReason =
  | 'collision_blocked'
  | 'gate_blocked'
  | 'idle_blocked'
  | 'budget_blocked';

export interface EscalationDecision {
  should_escalate: boolean;
  reason: BlockedReason;
  target_provider?: ProviderSelection;
  blocked_iterations?: number;
  cooldown_remaining_ms?: number;
}

export interface EscalationOutcome {
  success: boolean;
  feature_id: string;
  reason: BlockedReason;
  from: ProviderSelection;
  to: ProviderSelection;
  new_session_ids: {
    planner: string;
    builder: string;
    qa: string;
  };
  error?: string;
}

export interface EscalationManagerDependencies {
  kernel: SupervisorKernelPort;
  toolCaller: SupervisorToolCaller;
  sessionOrchestrator: SessionOrchestrator;
  workerProviderFactory: WorkerProviderFactory;
  promptProvider: AgentPromptProvider;
  config: EscalationConfig;
  notifier?: NotifierService;
}

export class EscalationManager {
  private readonly kernel: SupervisorKernelPort;
  private readonly toolCaller: SupervisorToolCaller;
  private readonly sessionOrchestrator: SessionOrchestrator;
  private readonly workerProviderFactory: WorkerProviderFactory;
  private readonly promptProvider: AgentPromptProvider;
  private readonly config: EscalationConfig;
  private readonly notifier: NotifierService | undefined;
  private readonly ledger: Map<string, EscalationLedgerEntry[]>;

  constructor(dependencies: EscalationManagerDependencies);

  async shouldEscalate(featureId: string, reason: BlockedReason): Promise<EscalationDecision>;

  async escalate(featureId: string, reason: BlockedReason): Promise<EscalationOutcome>;

  async rollback(featureId: string, toProvider: ProviderSelection): Promise<EscalationOutcome>;

  getEscalationHistory(featureId: string): EscalationLedgerEntry[];
}
```

**Key Methods:**

```typescript
private async evaluatePolicy(
  featureId: string,
  reason: BlockedReason,
  state: FeatureStatePayload
): Promise<EscalationDecision>;

private async createEscalatedProvider(
  target: ProviderSelection
): Promise<WorkerProvider>;

private async migrateFeatureCluster(
  featureId: string,
  oldProvider: WorkerProvider,
  newProvider: WorkerProvider
): Promise<{ planner: string; builder: string; qa: string }>;

private async recordEscalation(
  featureId: string,
  reason: BlockedReason,
  from: ProviderSelection,
  to: ProviderSelection,
  outcome: 'pending' | 'unblocked' | 'still_blocked' | 'failed'
): Promise<void>;
```

### 4.2 ProviderSwapOrchestrator

**Location:** `apps/control-plane/src/supervisor/provider-swap-orchestrator.ts`

**Responsibilities:**

- Coordinate session closure and creation
- Preserve context artifacts during swap
- Handle swap failures and rollback
- Update state metadata

**Interface:**

```typescript
export interface ProviderSwapOrchestratorDependencies {
  sessionOrchestrator: SessionOrchestrator;
  toolCaller: SupervisorToolCaller;
  promptProvider: AgentPromptProvider;
}

export class ProviderSwapOrchestrator {
  constructor(dependencies: ProviderSwapOrchestratorDependencies);

  async swapProvider(
    featureId: string,
    oldProvider: WorkerProvider,
    newProvider: WorkerProvider,
  ): Promise<SwapOutcome>;

  async rollbackSwap(
    featureId: string,
    targetProvider: WorkerProvider,
    backupSessionIds: { planner: string; builder: string; qa: string },
  ): Promise<void>;
}

export interface SwapOutcome {
  success: boolean;
  new_session_ids: {
    planner: string;
    builder: string;
    qa: string;
  };
  old_session_ids: {
    planner: string;
    builder: string;
    qa: string;
  };
  error?: string;
}
```

### 4.3 EscalationPolicyEvaluator

**Location:** `apps/control-plane/src/supervisor/escalation-policy-evaluator.ts`

**Responsibilities:**

- Evaluate escalation policies per blocked reason
- Check escalation limits and cooldowns
- Resolve target provider from policy

**Interface:**

```typescript
export interface EscalationPolicyEvaluatorDependencies {
  config: EscalationConfig;
}

export class EscalationPolicyEvaluator {
  constructor(dependencies: EscalationPolicyEvaluatorDependencies);

  evaluate(
    reason: BlockedReason,
    state: FeatureStatePayload,
    blockedIterations: number,
  ): EscalationDecision;

  resolveTargetProvider(
    reason: BlockedReason,
    role?: 'planner' | 'builder' | 'qa',
  ): ProviderSelection | null;

  checkCooldown(
    lastEscalationAt: string | null,
    cooldownMs: number,
  ): { ready: boolean; remaining_ms: number };

  checkEscalationLimit(escalationCount: number, maxEscalations: number): boolean;
}
```

### 4.4 EscalationLedger

**Location:** `apps/control-plane/src/supervisor/escalation-ledger.ts`

**Responsibilities:**

- Record escalation events
- Track outcomes
- Provide analytics on escalation effectiveness

**Interface:**

```typescript
export interface EscalationLedgerEntry {
  feature_id: string;
  timestamp: string;
  reason: BlockedReason;
  from: ProviderSelection;
  to: ProviderSelection;
  outcome: 'pending' | 'unblocked' | 'still_blocked' | 'failed' | 'rolled_back';
  iterations_blocked: number;
  cost_delta_usd?: number;
}

export class EscalationLedger {
  private readonly entries: Map<string, EscalationLedgerEntry[]>;

  record(entry: EscalationLedgerEntry): void;

  getHistory(featureId: string): EscalationLedgerEntry[];

  updateOutcome(
    featureId: string,
    timestamp: string,
    outcome: EscalationLedgerEntry['outcome'],
  ): void;

  getAnalytics(): {
    total_escalations: number;
    by_reason: Record<BlockedReason, number>;
    by_outcome: Record<string, number>;
    success_rate: number;
    avg_cost_delta_usd: number;
  };
}
```

---

## 5. Integration Points

### 5.1 RunCoordinator Integration

**Location:** `apps/control-plane/src/supervisor/run-coordinator.ts`

**Changes:**

1. Add `EscalationManager` to dependencies
2. Check for escalation candidates during `rebalanceActiveFeatures`
3. Trigger escalation before returning feature to active pool

**Pseudocode:**

```typescript
private async rebalanceActiveFeatures(
  currentActiveFeatureIds: string[]
): Promise<string[]> {
  const survivingActiveFeatureIds: string[] = [];

  for (const featureId of sortedCurrent) {
    const status = await this.readFeatureStatus(featureId);

    // Terminal status check (existing)
    if (status && RunCoordinator.TERMINAL_STATUSES.has(status)) {
      await this.sessionOrchestrator.closeFeatureCluster(featureId);
      continue;
    }

    // NEW: Check for escalation opportunity
    if (status === STATUS.BLOCKED) {
      const state = await this.readFeatureState(featureId);
      const blockedReason = this.inferBlockedReason(state);

      const decision = await this.escalationManager.shouldEscalate(
        featureId,
        blockedReason
      );

      if (decision.should_escalate) {
        const outcome = await this.escalationManager.escalate(
          featureId,
          blockedReason
        );

        if (outcome.success) {
          // Unblock feature and return to active pool
          await this.toolCaller.callTool('orchestrator', TOOLS.FEATURE_STATE_PATCH, {
            feature_id: featureId,
            expected_version: state.front_matter.version,
            patch: {
              front_matter: {
                status: this.resolveResumeStatus(state),
                status_reason: `Escalated to ${outcome.to.provider}/${outcome.to.model}`,
              },
            },
          });

          if (this.notifier) {
            await this.notifier.notify('feature_escalated', {
              feature_id: featureId,
              message: `Feature ${featureId} escalated from ${outcome.from.provider}/${outcome.from.model} to ${outcome.to.provider}/${outcome.to.model}`,
              details: { reason: blockedReason, outcome },
            });
          }
        }
      }
    }

    survivingActiveFeatureIds.push(featureId);
  }

  // ... rest of rebalance logic
}

private inferBlockedReason(state: FeatureStatePayload): BlockedReason {
  const statusReason = state.front_matter.status_reason?.toLowerCase() ?? '';

  if (statusReason.includes('collision')) {
    return 'collision_blocked';
  }
  if (statusReason.includes('gate') || statusReason.includes('failed')) {
    return 'gate_blocked';
  }
  if (statusReason.includes('idle') || statusReason.includes('stuck')) {
    return 'idle_blocked';
  }

  return 'gate_blocked'; // default
}

private resolveResumeStatus(state: FeatureStatePayload): FeatureStatus {
  const lastStatus = state.front_matter.status;

  // Resume to the phase that was blocked
  if (lastStatus === STATUS.BLOCKED) {
    const roleStatus = state.front_matter.role_status;
    if (roleStatus.planner === 'blocked') return STATUS.PLANNING;
    if (roleStatus.builder === 'blocked') return STATUS.BUILDING;
    if (roleStatus.qa === 'blocked') return STATUS.QA;
  }

  return STATUS.PLANNING; // default fallback
}
```

### 5.2 SupervisorRuntime Integration

**Location:** `apps/control-plane/src/supervisor/runtime.ts`

**Changes:**

1. Instantiate `EscalationManager` in constructor
2. Pass escalation config from `agents.yaml`
3. Expose escalation manager to `RunCoordinator`

**Pseudocode:**

```typescript
constructor(
  kernel: SupervisorKernelPort,
  provider: WorkerProvider,
  toolClient: ToolClient,
  options: SupervisorOptions = {},
) {
  // ... existing initialization ...

  const agentsConfig = this.kernel.getAgentsConfig();
  const escalationConfig = this.resolveEscalationConfig(agentsConfig);

  this.escalationManager = new EscalationManager({
    kernel: this.kernel,
    toolCaller: this,
    sessionOrchestrator: this.sessionOrchestrator,
    workerProviderFactory: this.workerProviderFactory,
    promptProvider: this,
    config: escalationConfig,
    notifier,
  });

  this.runCoordinator = new RunCoordinator({
    // ... existing dependencies ...
    escalationManager: this.escalationManager,
  });
}

private resolveEscalationConfig(
  agentsConfig: Record<string, unknown>
): EscalationConfig {
  const escalation = agentsConfig.escalation;

  if (!escalation || typeof escalation !== 'object') {
    return {
      enabled: false,
      max_escalations_per_feature: 2,
      cooldown_between_escalations_ms: 60000,
      policies: {},
    };
  }

  return escalation as EscalationConfig;
}
```

### 5.3 WorkerProviderFactory Integration

**Location:** `apps/control-plane/src/providers/worker-provider-factory.ts`

**Changes:**

1. Add method to create provider from `ProviderSelection`
2. Support dynamic provider instantiation

**New Method:**

```typescript
export interface WorkerProviderFactory {
  // ... existing methods ...

  createFromSelection(
    selection: ProviderSelection,
    mode: WorkerProviderMode
  ): Promise<WorkerProvider>;
}

// Implementation
async createFromSelection(
  selection: ProviderSelection,
  mode: WorkerProviderMode
): Promise<WorkerProvider> {
  const input: CreateWorkerProviderInput = {
    provider: selection.provider,
    model: selection.model,
    providerConfigRef: selection.provider_config_ref ?? null,
    mode,
  };

  return await this.create(input);
}
```

---

## 6. Escalation Decision Logic

### 6.1 Collision-Blocked Escalation

**Trigger:** Feature blocked due to file/area/contract collision

**Policy Check:**

1. Is `escalation.policies.collision_blocked.enabled` true?
2. Has feature been blocked for >= `escalate_after_iterations` iterations?
3. Has feature been escalated < `max_escalations_per_feature` times?
4. Has cooldown period elapsed since last escalation?

**Target Resolution:**

- Use `escalation.policies.collision_blocked.target` provider/model
- Apply `role_overrides` if specified

**Rationale:** More capable agents may find alternative implementation approaches that avoid collisions.

### 6.2 Gate-Blocked Escalation

**Trigger:** Feature blocked due to gate failure after max retries

**Policy Check:**

1. Is `escalation.policies.gate_blocked.enabled` true?
2. Has feature exhausted >= `escalate_after_retries` retry attempts?
3. Has feature been escalated < `max_escalations_per_feature` times?
4. Has cooldown period elapsed since last escalation?

**Target Resolution:**

- Use `escalation.policies.gate_blocked.target` provider/model
- Apply `role_overrides` if specified

**Rationale:** More capable agents may write higher-quality code that passes gates, or better understand test failure messages.

### 6.3 Idle-Blocked Escalation

**Trigger:** Feature appears stuck (no progress for `idle_threshold_ms`)

**Policy Check:**

1. Is `escalation.policies.idle_blocked.enabled` true?
2. Has feature been idle for >= `escalate_after_idle_ms`?
3. Has feature been escalated < `max_escalations_per_feature` times?
4. Has cooldown period elapsed since last escalation?

**Target Resolution:**

- Use `escalation.policies.idle_blocked.target` provider/model
- Apply `role_overrides` if specified

**Rationale:** Agent may be stuck due to model limitations (context window, reasoning capability). Fresh agent may unstick.

### 6.4 Budget-Blocked Escalation

**Trigger:** Feature cost exceeds `per_feature_limit_usd`

**Policy Check:**

1. Is `escalation.policies.budget_blocked.enabled` true?
2. Has feature been escalated < `max_escalations_per_feature` times?

**Target Resolution:**

- Use `escalation.policies.budget_blocked.target` provider/model
- Typically escalate to cheaper model, not more expensive

**Rationale:** Budget-blocked features are paused by default. Escalation here means switching to a cheaper model to complete work within budget.

**Default:** `enabled: false` (budget-blocked features remain paused)

---

## 7. Session Migration Protocol

### 7.1 Migration Steps

1. **Capture current state:**
   - Read `state.md` front matter
   - Record current session IDs
   - Record current provider/model

2. **Close old sessions:**
   - Call `provider.closeSession(planner_session_id)`
   - Call `provider.closeSession(builder_session_id)`
   - Call `provider.closeSession(qa_session_id)`
   - Best-effort; failures logged but not blocking

3. **Create new provider instance:**
   - Call `workerProviderFactory.createFromSelection(target)`
   - New provider instance with target config

4. **Create new sessions:**
   - Load role prompts
   - Call `newProvider.createSession('planner', featureId, plannerPrompt)`
   - Call `newProvider.createSession('builder', featureId, builderPrompt)`
   - Call `newProvider.createSession('qa', featureId, qaPrompt)`

5. **Update state:**
   - Patch `state.cluster` with new session IDs
   - Patch `state.escalation` with escalation event
   - Patch `state.escalation.current_provider` with new provider/model

6. **Update index:**
   - Call `kernel.updateFeatureSessionAssignment` with new session IDs

7. **Record escalation:**
   - Append to escalation ledger
   - Notify via configured channels

### 7.2 Rollback Protocol

If migration fails at any step:

1. Attempt to close any partially-created new sessions
2. Attempt to recreate old sessions with original provider
3. If recreation fails, mark feature as `failed` with reason
4. Record rollback event in ledger
5. Notify failure

### 7.3 Context Preservation

Agents read context from canonical artifacts:

- `spec.md` - feature requirements
- `state.md` - current status, locks, collisions
- `plan.json` - accepted plan
- `evidence/` - gate logs and coverage reports
- `decisions.md` - decision log

No explicit context transfer needed; new agents bootstrap from these artifacts.

---

## 8. Observability and Monitoring

### 8.1 State Metadata

Add to `state.md` front matter:

```yaml
escalation:
  count: 2
  history:
    - timestamp: '2026-03-05T10:30:00Z'
      reason: gate_blocked
      from:
        provider: custom
        model: local-default
      to:
        provider: claude
        model: claude-sonnet-3.5-20241022
      outcome: unblocked
    - timestamp: '2026-03-05T11:45:00Z'
      reason: idle_blocked
      from:
        provider: claude
        model: claude-sonnet-3.5-20241022
      to:
        provider: codex
        model: gpt-4-turbo
      outcome: pending
  last_escalation_at: '2026-03-05T11:45:00Z'
  current_provider:
    provider: codex
    model: gpt-4-turbo
    escalated: true
```

### 8.2 Dashboard Integration

Add to `report.dashboard` output:

```json
{
  "features": [
    {
      "feature_id": "my_feature",
      "status": "building",
      "escalation": {
        "count": 2,
        "current_provider": {
          "provider": "codex",
          "model": "gpt-4-turbo",
          "escalated": true
        },
        "last_escalation_at": "2026-03-05T11:45:00Z"
      }
    }
  ],
  "escalation_analytics": {
    "total_escalations": 15,
    "by_reason": {
      "collision_blocked": 3,
      "gate_blocked": 10,
      "idle_blocked": 2,
      "budget_blocked": 0
    },
    "by_outcome": {
      "unblocked": 12,
      "still_blocked": 2,
      "failed": 1
    },
    "success_rate": 0.8,
    "avg_cost_delta_usd": 2.35
  }
}
```

### 8.3 Notification Events

Add new notification event types:

- `feature_escalated` - feature escalated to new provider
- `escalation_failed` - escalation attempt failed
- `escalation_unblocked` - escalated feature successfully unblocked
- `escalation_limit_reached` - feature hit max escalations

---

## 9. Cost Tracking Integration

### 9.1 Cost Attribution

When a feature is escalated:

1. Record escalation timestamp in cost ledger
2. Track costs before and after escalation separately
3. Calculate cost delta attributable to escalation

### 9.2 Budget Enforcement

Escalated features still subject to budget limits:

1. Check budget before each wave execution
2. If escalated feature exceeds budget, pause with `paused_budget` status
3. Do not escalate again (escalation count already incremented)

### 9.3 Cost Analytics

Track escalation ROI:

- Average cost increase per escalation
- Success rate by escalation reason
- Cost per unblock by provider/model

---

## 10. CLI Integration

### 10.1 Manual Escalation Command

Add new CLI command: `aop escalate`

**Usage:**

```bash
# Escalate specific feature to specific provider
aop escalate --feature-id my_feature --provider claude --model claude-sonnet-3.5-20241022

# Escalate with reason
aop escalate --feature-id my_feature --reason gate_blocked

# Rollback to original provider
aop escalate --feature-id my_feature --rollback
```

**Behavior:**

- Bypasses policy checks (manual override)
- Still enforces max escalation limit
- Records escalation with reason `manual`
- Returns escalation outcome

### 10.2 Status Command Enhancement

Enhance `aop status` to show escalation info:

```bash
$ aop status

Feature: my_feature
Status: building
Provider: claude/claude-sonnet-3.5-20241022 (escalated)
Escalations: 2/2
Last escalation: 2026-03-05T11:45:00Z (idle_blocked)
```

---

## 11. Testing Strategy

### 11.1 Unit Tests

**Test Files:**

- `apps/control-plane/test/escalation-manager.spec.ts`
- `apps/control-plane/test/escalation-policy-evaluator.spec.ts`
- `apps/control-plane/test/provider-swap-orchestrator.spec.ts`
- `apps/control-plane/test/escalation-ledger.spec.ts`

**Coverage Requirements:**

- Lines >= 90%
- Branches >= 90%
- Functions >= 90%
- Statements >= 90%

**Key Test Cases:**

1. `GIVEN_collision_blocked_feature_WHEN_policy_enabled_THEN_escalates`
2. `GIVEN_gate_blocked_feature_WHEN_max_retries_reached_THEN_escalates`
3. `GIVEN_idle_blocked_feature_WHEN_idle_threshold_exceeded_THEN_escalates`
4. `GIVEN_feature_at_max_escalations_WHEN_blocked_again_THEN_does_not_escalate`
5. `GIVEN_cooldown_active_WHEN_escalation_requested_THEN_defers`
6. `GIVEN_escalation_fails_WHEN_session_creation_errors_THEN_rolls_back`
7. `GIVEN_role_override_configured_WHEN_escalating_THEN_uses_role_specific_provider`
8. `GIVEN_escalated_feature_WHEN_unblocked_THEN_records_success_outcome`

### 11.2 Integration Tests

**Test File:** `apps/control-plane/test/escalation-integration.spec.ts`

**Key Scenarios:**

1. End-to-end escalation flow (blocked -> escalate -> unblock)
2. Multi-escalation scenario (escalate twice, hit limit)
3. Rollback scenario (escalation fails, rollback succeeds)
4. Cost tracking across escalation boundary
5. Session migration with context preservation

### 11.3 Manual Testing Checklist

- [ ] Feature blocked by collision escalates to Claude
- [ ] Feature blocked by gate failure escalates after 2 retries
- [ ] Feature idle for 10 minutes escalates
- [ ] Escalated feature unblocks and completes successfully
- [ ] Feature at max escalations does not escalate again
- [ ] Manual escalation via CLI works
- [ ] Rollback via CLI works
- [ ] Dashboard shows escalation status
- [ ] Notifications sent on escalation events
- [ ] Cost tracking attributes costs correctly

---

## 12. Rollout Plan

### 12.1 Phase 1: Foundation (Week 1)

- [ ] Implement `EscalationConfig` schema extensions
- [ ] Implement `EscalationPolicyEvaluator`
- [ ] Implement `EscalationLedger`
- [ ] Unit tests for policy evaluation
- [ ] Schema validation tests

### 12.2 Phase 2: Core Logic (Week 2)

- [ ] Implement `EscalationManager`
- [ ] Implement `ProviderSwapOrchestrator`
- [ ] Implement session migration protocol
- [ ] Unit tests for escalation manager
- [ ] Unit tests for swap orchestrator

### 12.3 Phase 3: Integration (Week 3)

- [ ] Integrate `EscalationManager` into `RunCoordinator`
- [ ] Integrate `EscalationManager` into `SupervisorRuntime`
- [ ] Extend `WorkerProviderFactory` for dynamic creation
- [ ] Integration tests
- [ ] Manual testing

### 12.4 Phase 4: Observability (Week 4)

- [ ] Add escalation metadata to `state.md`
- [ ] Add escalation analytics to dashboard
- [ ] Add notification events
- [ ] Implement CLI `escalate` command
- [ ] Enhance `status` command
- [ ] Documentation updates

### 12.5 Phase 5: Production Hardening (Week 5)

- [ ] Cost tracking integration
- [ ] Rollback protocol testing
- [ ] Error handling hardening
- [ ] Performance testing
- [ ] Production deployment

---

## 13. Success Criteria

### 13.1 Functional Requirements

- [ ] Blocked features automatically escalate per policy
- [ ] Session migration preserves context
- [ ] Escalation limits enforced
- [ ] Cooldowns enforced
- [ ] Rollback works on failure
- [ ] Manual escalation via CLI works
- [ ] Cost tracking attributes correctly

### 13.2 Quality Requirements

- [ ] All unit tests pass
- [ ] All integration tests pass
- [ ] Coverage >= 90% (lines, branches, functions, statements)
- [ ] No lint warnings
- [ ] Type-check passes
- [ ] Schema validation passes

### 13.3 Operational Requirements

- [ ] Dashboard shows escalation status
- [ ] Notifications sent on escalation events
- [ ] Escalation analytics available
- [ ] Manual escalation documented
- [ ] Rollback procedure documented

---

## 14. Future Enhancements

### 14.1 Context Transfer (v2)

Extract conversation history from old sessions and inject into new sessions for better continuity.

### 14.2 Automatic Downgrade (v2)

After successful unblock, automatically downgrade to cheaper agent if feature remains stable.

### 14.3 Learning-Based Escalation (v3)

Use historical escalation outcomes to predict which provider/model is most likely to unblock a given feature.

### 14.4 Multi-Model Ensemble (v3)

Run multiple agents in parallel on blocked features and select best outcome.

### 14.5 Partial Escalation (v3)

Escalate only specific roles (e.g., escalate builder but keep planner on cheap model).

---

## 15. Open Questions

1. **Should escalation reset retry counters?**
   - Proposal: Yes, escalation gives feature a fresh start with new agent
   - Alternative: No, escalation is just a provider swap, retry count persists

2. **Should escalated features get priority in active pool?**
   - Proposal: No, maintain FIFO ordering
   - Alternative: Yes, escalated features get priority to maximize ROI

3. **Should we support gradual escalation chains?**
   - Example: local -> codex -> claude -> claude-opus
   - Proposal: v1 supports single escalation target per reason; v2 adds chains

4. **Should escalation be reversible automatically?**
   - Proposal: Manual rollback only in v1; automatic downgrade in v2

5. **Should we track escalation effectiveness per feature type?**
   - Proposal: v1 tracks global analytics; v3 adds per-feature-type learning

---

## 16. References

- `spec-files/completed/agentic_orchestrator_spec.md` - Core orchestration spec
- `spec-files/outstanding/agentic_orchestrator_persistent_worker_runtime_spec.md` - Session lifecycle
- `apps/control-plane/src/supervisor/runtime.ts` - Supervisor runtime
- `apps/control-plane/src/supervisor/run-coordinator.ts` - Run coordination
- `apps/control-plane/src/supervisor/session-orchestrator.ts` - Session management
- `apps/control-plane/src/providers/worker-provider-factory.ts` - Provider factory
- `apps/control-plane/src/application/services/reactions-service.ts` - Reactions policy
- `agentic/orchestrator/agents.yaml` - Agent configuration
- `agentic/orchestrator/policy.yaml` - Orchestration policy

---

**End of Specification**
