@harness-engineering/orchestrator 0.2.16 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { Issue, AgentEvent, WorkflowConfig, IssueTrackerClient, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, TrackerConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult } from '@harness-engineering/types';
1
+ import { Issue, AgentEvent, WorkflowConfig, IssueTrackerClient, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, TrackerConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig } from '@harness-engineering/types';
2
2
  import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation } from '@harness-engineering/intelligence';
3
3
  import { GraphStore } from '@harness-engineering/graph';
4
4
  import { execFile } from 'node:child_process';
@@ -1093,22 +1093,38 @@ interface MaintenanceStatus {
1093
1093
  history: RunResult[];
1094
1094
  }
1095
1095
 
1096
- /**
1097
- * The central orchestrator that manages the lifecycle of coding agents.
1098
- *
1099
- * It polls an issue tracker for candidate tasks, manages ephemeral workspaces,
1100
- * runs agents to resolve issues, and updates the tracker with progress.
1101
- *
1102
- * @fires Orchestrator#state_change Emitted when the internal state machine transitions
1103
- * @fires Orchestrator#agent_event Emitted when an agent produces an output or thought
1104
- */
1105
1096
  declare class Orchestrator extends EventEmitter {
1106
1097
  private state;
1107
1098
  private config;
1108
1099
  private tracker;
1109
1100
  private workspace;
1110
1101
  private hooks;
1111
- private runner;
1102
+ /**
1103
+ * Spec 2 SC30 / Task 11: per-dispatch backend factory replaces the
1104
+ * Phase 1 `runner` / `localRunner` two-runner split. Each
1105
+ * `dispatchIssue()` call asks the factory for a `RoutingUseCase`-routed
1106
+ * `AgentBackend`, then wraps it in a fresh `AgentRunner`.
1107
+ *
1108
+ * `AgentRunner` is stateless (just `{ backend, options }`), so
1109
+ * per-dispatch construction is safe and avoids the cross-call state
1110
+ * the old two-runner split had to coordinate.
1111
+ *
1112
+ * Null only in the legacy fallback path: when `migrateAgentConfig`
1113
+ * throws (legacy configs missing supplemental fields, e.g.
1114
+ * `agent.backend='anthropic'` with no `agent.model`) AND no
1115
+ * `overrides.backend` is supplied, factory construction is skipped to
1116
+ * preserve the prior behavior of failing at dispatch time rather than
1117
+ * construction time. Eliminating this fallback is autopilot Phase 4+.
1118
+ */
1119
+ private backendFactory;
1120
+ /**
1121
+ * Test-only: when overrides.backend is provided, dispatch uses this
1122
+ * instance directly (bypassing the factory). Mirrors Phase 1
1123
+ * `overrides.backend → this.runner.backend` behavior so existing
1124
+ * MockBackend-injection tests keep working without touching the
1125
+ * factory's routing path.
1126
+ */
1127
+ private overrideBackend;
1112
1128
  private renderer;
1113
1129
  private promptTemplate;
1114
1130
  private server?;
@@ -1116,7 +1132,22 @@ declare class Orchestrator extends EventEmitter {
1116
1132
  private heartbeatInterval?;
1117
1133
  private logger;
1118
1134
  private interactionQueue;
1119
- private localRunner;
1135
+ /**
1136
+ * Per-named-backend resolver map (Spec 2 SC37). Each `local`/`pi` entry
1137
+ * in `agent.backends` spawns one `LocalModelResolver`. Legacy
1138
+ * single-backend configs converge here via `migrateAgentConfig` (Task 9),
1139
+ * so this map is the single source of truth post-migration.
1140
+ */
1141
+ private localResolvers;
1142
+ /**
1143
+ * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
1144
+ * (SC39): each local/pi resolver gets its own listener emitting a
1145
+ * `NamedLocalModelStatus` event tagged with `backendName` + `endpoint`.
1146
+ * The previous single-resolver field (`localModelStatusUnsubscribe`)
1147
+ * is replaced by this list so multi-local configs can teardown all
1148
+ * listeners on `stop()` without a Map mutation.
1149
+ */
1150
+ private localModelStatusUnsubscribes;
1120
1151
  private pipeline;
1121
1152
  private analysisArchive;
1122
1153
  private graphStore;
@@ -1156,7 +1187,6 @@ declare class Orchestrator extends EventEmitter {
1156
1187
  execFileFn?: ExecFileFn;
1157
1188
  });
1158
1189
  private createTracker;
1159
- private createBackend;
1160
1190
  /**
1161
1191
  * Creates a TaskRunner for the maintenance scheduler.
1162
1192
  * CheckCommandRunner and CommandExecutor use real child_process execution.
@@ -1168,17 +1198,35 @@ declare class Orchestrator extends EventEmitter {
1168
1198
  * Extracted from start() to keep function length under threshold.
1169
1199
  */
1170
1200
  private initMaintenance;
1171
- private createLocalBackend;
1172
1201
  private createIntelligencePipeline;
1173
1202
  /**
1174
- * Create the AnalysisProvider for the intelligence pipeline.
1203
+ * Create the AnalysisProvider for an intelligence-pipeline layer
1204
+ * (`sel` by default; `pesl` when constructing a distinct PESL
1205
+ * provider per Spec 2 SC35).
1206
+ *
1207
+ * Spec 2 Phase 4 (SC31–SC36) — resolution order:
1208
+ * 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
1209
+ * 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
1210
+ * `routing.default`) to pick a `BackendDef` from `agent.backends`,
1211
+ * then translate via `buildAnalysisProvider` (the per-type factory).
1175
1212
  *
1176
- * Resolution order:
1177
- * 1. Explicit `intelligence.provider` config (separate key/endpoint)
1178
- * 2. Local backend config (agent.localBackend + localEndpoint/localModel)
1179
- * 3. Primary agent backend config (agent.apiKey + agent.backend)
1213
+ * Closes the Phase 2 deferral (P2-DEF-638): the legacy
1214
+ * `this.config.agent.backend` read at the bottom of this method is
1215
+ * removed; routing is the sole source for non-explicit configs.
1216
+ *
1217
+ * Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
1218
+ * inlined here). Phase 4 extracts the per-type tree into
1219
+ * `buildAnalysisProvider`, dropping this method to ≤ 5 branches
1220
+ * (under the 15 threshold).
1180
1221
  */
1181
1222
  private createAnalysisProvider;
1223
+ /**
1224
+ * Look up the routed BackendDef for an intelligence layer, falling
1225
+ * back through `routing.intelligence.<layer>` → `routing.default`
1226
+ * → null. Returns the resolved name alongside the def so callers can
1227
+ * key into the per-name resolver map.
1228
+ */
1229
+ private resolveRoutedBackendForIntelligence;
1182
1230
  private createProviderFromExplicitConfig;
1183
1231
  /**
1184
1232
  * Lazily initializes the ClaimManager if it hasn't been created yet.
@@ -1262,6 +1310,14 @@ declare class Orchestrator extends EventEmitter {
1262
1310
  * Used by the dashboard's "Dispatch Now" action.
1263
1311
  */
1264
1312
  dispatchAdHoc(issue: Issue): Promise<void>;
1313
+ /**
1314
+ * Initialize the LocalModelResolver and intelligence pipeline.
1315
+ *
1316
+ * Runs the initial probe (so resolver state reflects server availability)
1317
+ * before constructing the intelligence pipeline. Subscribes the dashboard
1318
+ * broadcast stub to status changes. Called exactly once from start().
1319
+ */
1320
+ private initLocalModelAndPipeline;
1265
1321
  /**
1266
1322
  * Starts the polling loop and the internal HTTP server.
1267
1323
  * Runs startup reconciliation to release orphaned claims before the first tick.
@@ -1292,4 +1348,169 @@ declare function launchTUI(orchestrator: Orchestrator): {
1292
1348
  waitUntilExit: () => Promise<void>;
1293
1349
  };
1294
1350
 
1295
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type TickEvent, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, triageIssue, validateWorkflowConfig };
1351
+ interface BackendRouterOptions {
1352
+ backends: Record<string, BackendDef>;
1353
+ routing: RoutingConfig;
1354
+ }
1355
+ /**
1356
+ * BackendRouter
1357
+ *
1358
+ * Owns the lookup from a `RoutingUseCase` (a discriminated query — tier,
1359
+ * intelligence layer, maintenance, chat) to a named backend.
1360
+ * Construction-time validation guarantees every name referenced by
1361
+ * `routing` is present in `backends` so runtime lookups are total and
1362
+ * never throw on unknown-name references (D6/D7).
1363
+ *
1364
+ * Lookups for tier/intelligence use cases that fall through to undefined
1365
+ * mappings return `routing.default` without throwing — this matches the
1366
+ * spec's "every use case inherits default unless explicitly routed"
1367
+ * semantics. The `maintenance` and `chat` kinds always resolve to
1368
+ * `routing.default` (SC19, SC20).
1369
+ */
1370
+ declare class BackendRouter {
1371
+ private readonly backends;
1372
+ private readonly routing;
1373
+ constructor(opts: BackendRouterOptions);
1374
+ /**
1375
+ * Returns the backend name for a given use case.
1376
+ *
1377
+ * - `tier`: per-tier override, falling back to `routing.default`.
1378
+ * - `intelligence`: per-layer override under `routing.intelligence`,
1379
+ * falling back to `routing.default`.
1380
+ * - `maintenance` / `chat`: always `routing.default`.
1381
+ */
1382
+ resolve(useCase: RoutingUseCase): string;
1383
+ /**
1384
+ * Returns the BackendDef reference for the resolved name. Returns the
1385
+ * exact reference held in `backends` (no copy) so identity comparisons
1386
+ * succeed (SC21).
1387
+ */
1388
+ resolveDefinition(useCase: RoutingUseCase): BackendDef;
1389
+ private validateReferences;
1390
+ }
1391
+
1392
+ /**
1393
+ * Options for `OrchestratorBackendFactory`.
1394
+ *
1395
+ * `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own
1396
+ * agent-config fields. `getResolverModelFor` is a registration hook the
1397
+ * orchestrator calls to bind each `local`/`pi` backend to its
1398
+ * `LocalModelResolver` (so multi-resolver array-fallback works without
1399
+ * leaking resolver lifetimes into the factory).
1400
+ */
1401
+ interface OrchestratorBackendFactoryOptions {
1402
+ backends: Record<string, BackendDef>;
1403
+ routing: RoutingConfig;
1404
+ sandboxPolicy: 'none' | 'docker';
1405
+ container?: ContainerConfig;
1406
+ secrets?: SecretConfig;
1407
+ /**
1408
+ * Hook for resolver injection. Invoked per `local`/`pi` backend at
1409
+ * `forUseCase()` time with the backend's name. When the hook returns a
1410
+ * function, the factory rebuilds the local/pi instance using that
1411
+ * function as `getModel` (overriding the head-of-array placeholder
1412
+ * baked into `createBackend`). Returning `undefined` means "no
1413
+ * resolver registered for this name" — the placeholder stays in place.
1414
+ *
1415
+ * This indirection keeps the factory ignorant of `LocalModelResolver`'s
1416
+ * existence and lifecycle while still letting it produce backends that
1417
+ * route through the resolver Map.
1418
+ */
1419
+ getResolverModelFor?: (backendName: string) => (() => string | null) | undefined;
1420
+ }
1421
+ /**
1422
+ * High-level factory wrapping `BackendRouter` + `createBackend` plus
1423
+ * orchestrator-side concerns (sandbox wrapping, resolver binding).
1424
+ *
1425
+ * Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh
1426
+ * `AgentBackend` whose class matches the routed `BackendDef.type`.
1427
+ * `local`/`pi` defs are bound to their per-name resolver before being
1428
+ * returned, and the result is wrapped in `ContainerBackend` when
1429
+ * sandboxPolicy is 'docker'.
1430
+ */
1431
+ declare class OrchestratorBackendFactory {
1432
+ private readonly router;
1433
+ private readonly opts;
1434
+ constructor(opts: OrchestratorBackendFactoryOptions);
1435
+ /**
1436
+ * Resolve `useCase` to a backend name, materialize a fresh
1437
+ * `AgentBackend`, optionally rebind its model resolver, and apply
1438
+ * sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
1439
+ * holds the per-dispatch reference and discards it when the run ends.
1440
+ */
1441
+ /**
1442
+ * Resolve `useCase` to its routed backend name, exposing the
1443
+ * router lookup without materializing a backend. Used by callers
1444
+ * (e.g., the orchestrator's dispatch site) that need to label
1445
+ * telemetry with the routed name BEFORE constructing the backend.
1446
+ *
1447
+ * Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
1448
+ * + `StreamRecorder` with the legacy `agent.backend` field, which
1449
+ * is `undefined` for pure-modern configs. Threading the routed name
1450
+ * through dispatch eliminates that gap.
1451
+ */
1452
+ resolveName(useCase: RoutingUseCase): string;
1453
+ forUseCase(useCase: RoutingUseCase): AgentBackend;
1454
+ /**
1455
+ * Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
1456
+ * mirroring `createBackend`'s local/pi branches but substituting the
1457
+ * head-of-array placeholder with the orchestrator-owned resolver.
1458
+ */
1459
+ private buildLocalLikeWithResolver;
1460
+ /**
1461
+ * Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
1462
+ * backend per call so each dispatch sees a fresh container handle map
1463
+ * (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
1464
+ */
1465
+ private wrapInContainer;
1466
+ }
1467
+
1468
+ /**
1469
+ * Result of running `migrateAgentConfig`.
1470
+ *
1471
+ * `config` is the *effective* AgentConfig — either the input unchanged
1472
+ * (when `agent.backends` is already set or no migration is needed) or
1473
+ * the input augmented with synthesized `backends` and `routing` fields.
1474
+ *
1475
+ * `warnings` is a list of human-readable deprecation messages, one per
1476
+ * legacy field encountered. Each message names the deprecated field
1477
+ * (dotted path) and includes a pointer to the migration guide. The
1478
+ * orchestrator emits these as `warn`-level log entries at startup.
1479
+ */
1480
+ interface MigrationResult {
1481
+ config: AgentConfig;
1482
+ warnings: string[];
1483
+ }
1484
+ /**
1485
+ * Translate legacy `agent.backend` / `agent.localBackend` /
1486
+ * `agent.escalation.autoExecute` into the new `agent.backends` +
1487
+ * `agent.routing` shape (Spec 2 D3, D4, D8, D11).
1488
+ *
1489
+ * Behavior matrix:
1490
+ * - `agent.backends` already set, no legacy fields:
1491
+ * no-op; returns input config unchanged, warnings = [].
1492
+ * - `agent.backends` already set + at least one legacy field:
1493
+ * no-op on config; warnings name each ignored legacy field (D4).
1494
+ * - No `agent.backends`, at least one of `agent.backend` /
1495
+ * `agent.localBackend` / `agent.localEndpoint` / etc. set:
1496
+ * synthesize `backends.primary` (always, from `agent.backend`)
1497
+ * and `backends.local` (when `localBackend` is set), plus a
1498
+ * `routing` map driven by `escalation.autoExecute` (D3).
1499
+ * - No `agent.backends`, no legacy fields:
1500
+ * no-op; the caller's downstream Zod validation surfaces the gap
1501
+ * as a missing-required-field error.
1502
+ *
1503
+ * Throws on internal inconsistencies (e.g., `agent.backend = 'pi'`
1504
+ * with no `localEndpoint`/`localModel`) — these are user-config bugs
1505
+ * that would have produced a runtime crash today.
1506
+ */
1507
+ declare function migrateAgentConfig(agent: AgentConfig): MigrationResult;
1508
+
1509
+ /**
1510
+ * Pure constructor: BackendDef -> concrete AgentBackend instance.
1511
+ * No side effects beyond the underlying class constructors.
1512
+ * Container wrapping (sandbox policy) is the orchestrator's job, not the factory's.
1513
+ */
1514
+ declare function createBackend(def: BackendDef): AgentBackend;
1515
+
1516
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, type MigrationResult, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type TickEvent, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, migrateAgentConfig, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, triageIssue, validateWorkflowConfig };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Issue, AgentEvent, WorkflowConfig, IssueTrackerClient, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, TrackerConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult } from '@harness-engineering/types';
1
+ import { Issue, AgentEvent, WorkflowConfig, IssueTrackerClient, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, RoutingDecision, Result, WorkflowDefinition, TrackerConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, BackendDef, RoutingConfig, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig } from '@harness-engineering/types';
2
2
  import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation } from '@harness-engineering/intelligence';
3
3
  import { GraphStore } from '@harness-engineering/graph';
4
4
  import { execFile } from 'node:child_process';
@@ -1093,22 +1093,38 @@ interface MaintenanceStatus {
1093
1093
  history: RunResult[];
1094
1094
  }
1095
1095
 
1096
- /**
1097
- * The central orchestrator that manages the lifecycle of coding agents.
1098
- *
1099
- * It polls an issue tracker for candidate tasks, manages ephemeral workspaces,
1100
- * runs agents to resolve issues, and updates the tracker with progress.
1101
- *
1102
- * @fires Orchestrator#state_change Emitted when the internal state machine transitions
1103
- * @fires Orchestrator#agent_event Emitted when an agent produces an output or thought
1104
- */
1105
1096
  declare class Orchestrator extends EventEmitter {
1106
1097
  private state;
1107
1098
  private config;
1108
1099
  private tracker;
1109
1100
  private workspace;
1110
1101
  private hooks;
1111
- private runner;
1102
+ /**
1103
+ * Spec 2 SC30 / Task 11: per-dispatch backend factory replaces the
1104
+ * Phase 1 `runner` / `localRunner` two-runner split. Each
1105
+ * `dispatchIssue()` call asks the factory for a `RoutingUseCase`-routed
1106
+ * `AgentBackend`, then wraps it in a fresh `AgentRunner`.
1107
+ *
1108
+ * `AgentRunner` is stateless (just `{ backend, options }`), so
1109
+ * per-dispatch construction is safe and avoids the cross-call state
1110
+ * the old two-runner split had to coordinate.
1111
+ *
1112
+ * Null only in the legacy fallback path: when `migrateAgentConfig`
1113
+ * throws (legacy configs missing supplemental fields, e.g.
1114
+ * `agent.backend='anthropic'` with no `agent.model`) AND no
1115
+ * `overrides.backend` is supplied, factory construction is skipped to
1116
+ * preserve the prior behavior of failing at dispatch time rather than
1117
+ * construction time. Eliminating this fallback is autopilot Phase 4+.
1118
+ */
1119
+ private backendFactory;
1120
+ /**
1121
+ * Test-only: when overrides.backend is provided, dispatch uses this
1122
+ * instance directly (bypassing the factory). Mirrors Phase 1
1123
+ * `overrides.backend → this.runner.backend` behavior so existing
1124
+ * MockBackend-injection tests keep working without touching the
1125
+ * factory's routing path.
1126
+ */
1127
+ private overrideBackend;
1112
1128
  private renderer;
1113
1129
  private promptTemplate;
1114
1130
  private server?;
@@ -1116,7 +1132,22 @@ declare class Orchestrator extends EventEmitter {
1116
1132
  private heartbeatInterval?;
1117
1133
  private logger;
1118
1134
  private interactionQueue;
1119
- private localRunner;
1135
+ /**
1136
+ * Per-named-backend resolver map (Spec 2 SC37). Each `local`/`pi` entry
1137
+ * in `agent.backends` spawns one `LocalModelResolver`. Legacy
1138
+ * single-backend configs converge here via `migrateAgentConfig` (Task 9),
1139
+ * so this map is the single source of truth post-migration.
1140
+ */
1141
+ private localResolvers;
1142
+ /**
1143
+ * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
1144
+ * (SC39): each local/pi resolver gets its own listener emitting a
1145
+ * `NamedLocalModelStatus` event tagged with `backendName` + `endpoint`.
1146
+ * The previous single-resolver field (`localModelStatusUnsubscribe`)
1147
+ * is replaced by this list so multi-local configs can teardown all
1148
+ * listeners on `stop()` without a Map mutation.
1149
+ */
1150
+ private localModelStatusUnsubscribes;
1120
1151
  private pipeline;
1121
1152
  private analysisArchive;
1122
1153
  private graphStore;
@@ -1156,7 +1187,6 @@ declare class Orchestrator extends EventEmitter {
1156
1187
  execFileFn?: ExecFileFn;
1157
1188
  });
1158
1189
  private createTracker;
1159
- private createBackend;
1160
1190
  /**
1161
1191
  * Creates a TaskRunner for the maintenance scheduler.
1162
1192
  * CheckCommandRunner and CommandExecutor use real child_process execution.
@@ -1168,17 +1198,35 @@ declare class Orchestrator extends EventEmitter {
1168
1198
  * Extracted from start() to keep function length under threshold.
1169
1199
  */
1170
1200
  private initMaintenance;
1171
- private createLocalBackend;
1172
1201
  private createIntelligencePipeline;
1173
1202
  /**
1174
- * Create the AnalysisProvider for the intelligence pipeline.
1203
+ * Create the AnalysisProvider for an intelligence-pipeline layer
1204
+ * (`sel` by default; `pesl` when constructing a distinct PESL
1205
+ * provider per Spec 2 SC35).
1206
+ *
1207
+ * Spec 2 Phase 4 (SC31–SC36) — resolution order:
1208
+ * 1. Explicit `intelligence.provider` config wins (preserves Phase 0–3 behavior; SC33).
1209
+ * 2. Otherwise, consult `agent.routing.intelligence.<layer>` (or
1210
+ * `routing.default`) to pick a `BackendDef` from `agent.backends`,
1211
+ * then translate via `buildAnalysisProvider` (the per-type factory).
1175
1212
  *
1176
- * Resolution order:
1177
- * 1. Explicit `intelligence.provider` config (separate key/endpoint)
1178
- * 2. Local backend config (agent.localBackend + localEndpoint/localModel)
1179
- * 3. Primary agent backend config (agent.apiKey + agent.backend)
1213
+ * Closes the Phase 2 deferral (P2-DEF-638): the legacy
1214
+ * `this.config.agent.backend` read at the bottom of this method is
1215
+ * removed; routing is the sole source for non-explicit configs.
1216
+ *
1217
+ * Cyclomatic complexity: pre-Phase-4 was 33 (factory dispatch was
1218
+ * inlined here). Phase 4 extracts the per-type tree into
1219
+ * `buildAnalysisProvider`, dropping this method to ≤ 5 branches
1220
+ * (under the 15 threshold).
1180
1221
  */
1181
1222
  private createAnalysisProvider;
1223
+ /**
1224
+ * Look up the routed BackendDef for an intelligence layer, falling
1225
+ * back through `routing.intelligence.<layer>` → `routing.default`
1226
+ * → null. Returns the resolved name alongside the def so callers can
1227
+ * key into the per-name resolver map.
1228
+ */
1229
+ private resolveRoutedBackendForIntelligence;
1182
1230
  private createProviderFromExplicitConfig;
1183
1231
  /**
1184
1232
  * Lazily initializes the ClaimManager if it hasn't been created yet.
@@ -1262,6 +1310,14 @@ declare class Orchestrator extends EventEmitter {
1262
1310
  * Used by the dashboard's "Dispatch Now" action.
1263
1311
  */
1264
1312
  dispatchAdHoc(issue: Issue): Promise<void>;
1313
+ /**
1314
+ * Initialize the LocalModelResolver and intelligence pipeline.
1315
+ *
1316
+ * Runs the initial probe (so resolver state reflects server availability)
1317
+ * before constructing the intelligence pipeline. Subscribes the dashboard
1318
+ * broadcast stub to status changes. Called exactly once from start().
1319
+ */
1320
+ private initLocalModelAndPipeline;
1265
1321
  /**
1266
1322
  * Starts the polling loop and the internal HTTP server.
1267
1323
  * Runs startup reconciliation to release orphaned claims before the first tick.
@@ -1292,4 +1348,169 @@ declare function launchTUI(orchestrator: Orchestrator): {
1292
1348
  waitUntilExit: () => Promise<void>;
1293
1349
  };
1294
1350
 
1295
- export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type TickEvent, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, triageIssue, validateWorkflowConfig };
1351
+ interface BackendRouterOptions {
1352
+ backends: Record<string, BackendDef>;
1353
+ routing: RoutingConfig;
1354
+ }
1355
+ /**
1356
+ * BackendRouter
1357
+ *
1358
+ * Owns the lookup from a `RoutingUseCase` (a discriminated query — tier,
1359
+ * intelligence layer, maintenance, chat) to a named backend.
1360
+ * Construction-time validation guarantees every name referenced by
1361
+ * `routing` is present in `backends` so runtime lookups are total and
1362
+ * never throw on unknown-name references (D6/D7).
1363
+ *
1364
+ * Lookups for tier/intelligence use cases that fall through to undefined
1365
+ * mappings return `routing.default` without throwing — this matches the
1366
+ * spec's "every use case inherits default unless explicitly routed"
1367
+ * semantics. The `maintenance` and `chat` kinds always resolve to
1368
+ * `routing.default` (SC19, SC20).
1369
+ */
1370
+ declare class BackendRouter {
1371
+ private readonly backends;
1372
+ private readonly routing;
1373
+ constructor(opts: BackendRouterOptions);
1374
+ /**
1375
+ * Returns the backend name for a given use case.
1376
+ *
1377
+ * - `tier`: per-tier override, falling back to `routing.default`.
1378
+ * - `intelligence`: per-layer override under `routing.intelligence`,
1379
+ * falling back to `routing.default`.
1380
+ * - `maintenance` / `chat`: always `routing.default`.
1381
+ */
1382
+ resolve(useCase: RoutingUseCase): string;
1383
+ /**
1384
+ * Returns the BackendDef reference for the resolved name. Returns the
1385
+ * exact reference held in `backends` (no copy) so identity comparisons
1386
+ * succeed (SC21).
1387
+ */
1388
+ resolveDefinition(useCase: RoutingUseCase): BackendDef;
1389
+ private validateReferences;
1390
+ }
1391
+
1392
+ /**
1393
+ * Options for `OrchestratorBackendFactory`.
1394
+ *
1395
+ * `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own
1396
+ * agent-config fields. `getResolverModelFor` is a registration hook the
1397
+ * orchestrator calls to bind each `local`/`pi` backend to its
1398
+ * `LocalModelResolver` (so multi-resolver array-fallback works without
1399
+ * leaking resolver lifetimes into the factory).
1400
+ */
1401
+ interface OrchestratorBackendFactoryOptions {
1402
+ backends: Record<string, BackendDef>;
1403
+ routing: RoutingConfig;
1404
+ sandboxPolicy: 'none' | 'docker';
1405
+ container?: ContainerConfig;
1406
+ secrets?: SecretConfig;
1407
+ /**
1408
+ * Hook for resolver injection. Invoked per `local`/`pi` backend at
1409
+ * `forUseCase()` time with the backend's name. When the hook returns a
1410
+ * function, the factory rebuilds the local/pi instance using that
1411
+ * function as `getModel` (overriding the head-of-array placeholder
1412
+ * baked into `createBackend`). Returning `undefined` means "no
1413
+ * resolver registered for this name" — the placeholder stays in place.
1414
+ *
1415
+ * This indirection keeps the factory ignorant of `LocalModelResolver`'s
1416
+ * existence and lifecycle while still letting it produce backends that
1417
+ * route through the resolver Map.
1418
+ */
1419
+ getResolverModelFor?: (backendName: string) => (() => string | null) | undefined;
1420
+ }
1421
+ /**
1422
+ * High-level factory wrapping `BackendRouter` + `createBackend` plus
1423
+ * orchestrator-side concerns (sandbox wrapping, resolver binding).
1424
+ *
1425
+ * Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh
1426
+ * `AgentBackend` whose class matches the routed `BackendDef.type`.
1427
+ * `local`/`pi` defs are bound to their per-name resolver before being
1428
+ * returned, and the result is wrapped in `ContainerBackend` when
1429
+ * sandboxPolicy is 'docker'.
1430
+ */
1431
+ declare class OrchestratorBackendFactory {
1432
+ private readonly router;
1433
+ private readonly opts;
1434
+ constructor(opts: OrchestratorBackendFactoryOptions);
1435
+ /**
1436
+ * Resolve `useCase` to a backend name, materialize a fresh
1437
+ * `AgentBackend`, optionally rebind its model resolver, and apply
1438
+ * sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
1439
+ * holds the per-dispatch reference and discards it when the run ends.
1440
+ */
1441
+ /**
1442
+ * Resolve `useCase` to its routed backend name, exposing the
1443
+ * router lookup without materializing a backend. Used by callers
1444
+ * (e.g., the orchestrator's dispatch site) that need to label
1445
+ * telemetry with the routed name BEFORE constructing the backend.
1446
+ *
1447
+ * Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
1448
+ * + `StreamRecorder` with the legacy `agent.backend` field, which
1449
+ * is `undefined` for pure-modern configs. Threading the routed name
1450
+ * through dispatch eliminates that gap.
1451
+ */
1452
+ resolveName(useCase: RoutingUseCase): string;
1453
+ forUseCase(useCase: RoutingUseCase): AgentBackend;
1454
+ /**
1455
+ * Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
1456
+ * mirroring `createBackend`'s local/pi branches but substituting the
1457
+ * head-of-array placeholder with the orchestrator-owned resolver.
1458
+ */
1459
+ private buildLocalLikeWithResolver;
1460
+ /**
1461
+ * Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
1462
+ * backend per call so each dispatch sees a fresh container handle map
1463
+ * (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
1464
+ */
1465
+ private wrapInContainer;
1466
+ }
1467
+
1468
+ /**
1469
+ * Result of running `migrateAgentConfig`.
1470
+ *
1471
+ * `config` is the *effective* AgentConfig — either the input unchanged
1472
+ * (when `agent.backends` is already set or no migration is needed) or
1473
+ * the input augmented with synthesized `backends` and `routing` fields.
1474
+ *
1475
+ * `warnings` is a list of human-readable deprecation messages, one per
1476
+ * legacy field encountered. Each message names the deprecated field
1477
+ * (dotted path) and includes a pointer to the migration guide. The
1478
+ * orchestrator emits these as `warn`-level log entries at startup.
1479
+ */
1480
+ interface MigrationResult {
1481
+ config: AgentConfig;
1482
+ warnings: string[];
1483
+ }
1484
+ /**
1485
+ * Translate legacy `agent.backend` / `agent.localBackend` /
1486
+ * `agent.escalation.autoExecute` into the new `agent.backends` +
1487
+ * `agent.routing` shape (Spec 2 D3, D4, D8, D11).
1488
+ *
1489
+ * Behavior matrix:
1490
+ * - `agent.backends` already set, no legacy fields:
1491
+ * no-op; returns input config unchanged, warnings = [].
1492
+ * - `agent.backends` already set + at least one legacy field:
1493
+ * no-op on config; warnings name each ignored legacy field (D4).
1494
+ * - No `agent.backends`, at least one of `agent.backend` /
1495
+ * `agent.localBackend` / `agent.localEndpoint` / etc. set:
1496
+ * synthesize `backends.primary` (always, from `agent.backend`)
1497
+ * and `backends.local` (when `localBackend` is set), plus a
1498
+ * `routing` map driven by `escalation.autoExecute` (D3).
1499
+ * - No `agent.backends`, no legacy fields:
1500
+ * no-op; the caller's downstream Zod validation surfaces the gap
1501
+ * as a missing-required-field error.
1502
+ *
1503
+ * Throws on internal inconsistencies (e.g., `agent.backend = 'pi'`
1504
+ * with no `localEndpoint`/`localModel`) — these are user-config bugs
1505
+ * that would have produced a runtime crash today.
1506
+ */
1507
+ declare function migrateAgentConfig(agent: AgentConfig): MigrationResult;
1508
+
1509
+ /**
1510
+ * Pure constructor: BackendDef -> concrete AgentBackend instance.
1511
+ * No side effects beyond the underlying class constructors.
1512
+ * Container wrapping (sandbox policy) is the orchestrator's job, not the factory's.
1513
+ */
1514
+ declare function createBackend(def: BackendDef): AgentBackend;
1515
+
1516
+ export { type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BackendRouter, type BackendRouterOptions, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileFn, type Highlight, type HighlightsInfo, InteractionQueue, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, type MigrationResult, MockBackend, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PendingInteraction, PromptRenderer, type PublishedIndex, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type ReleaseClaimEffect, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, type RunAttemptPhase, type RunningEntry, type ScheduleRetryEffect, type SideEffect, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type TickEvent, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, applyEvent, artifactPresenceFromIssue, calculateRetryDelay, canDispatch, computeRateLimitDelay, createBackend, createEmptyState, detectScopeTier, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, isEligible, launchTUI, loadPublishedIndex, migrateAgentConfig, reconcile, renderAnalysisComment, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, savePublishedIndex, selectCandidates, sortCandidates, triageIssue, validateWorkflowConfig };