@harness-engineering/orchestrator 0.12.0 → 0.13.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.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _harness_engineering_types from '@harness-engineering/types';
2
- import { Issue, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, IssueRoutingDecision, Result, WorkflowDefinition, BackendDef, RoutingConfig, WorkspaceConfig, HooksConfig, AgentBackend, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, CheckScriptDefinition, OutputRetentionConfig, RoutingDecision, RoutingUseCase, ContainerConfig, SecretConfig, AgentConfig, LocalModelStatus, CustomTaskDefinition, MaintenanceConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types';
3
- import { IssueTrackerClient, Issue as Issue$1, TrackerConfig, eventSourcing, CacheMetricsRecorder, ArchiveHooks, SkillProposal, ProposalGateFinding, SkillKind } from '@harness-engineering/core';
2
+ import { Issue, WorkflowExecutionPlan, StageRun, AgentEvent, WorkflowConfig, TokenUsage, ConcernSignal, ScopeTier, EscalationConfig, IssueRoutingDecision, Result, WorkflowDefinition, BackendDef, RoutingConfig, RoutingDecision, RoutingUseCase, ContainerConfig, SecretConfig, AgentBackend, RoutingRequest, CapabilityTier, WorkspaceConfig, HooksConfig, SessionStartParams, AgentSession, AgentError, TurnParams, TurnResult, CheckScriptDefinition, OutputRetentionConfig, BackendCapabilityRegistry, RoutingPolicy, ComplexityVerdict, RoutingTelemetry, RoutingStatus, RoutingError, PrivacyClass, BackendCapabilities, AgentConfig, LocalModelStatus, CustomTaskDefinition, MaintenanceConfig, TokenScope, AuthToken, AuthTokenPublic, IndexedFileKind, SessionSearchResult, ReindexStats, SessionSummarizationConfig, SessionSummary, SessionSummaryMeta, SessionsConfig, GatewayEvent, NotificationEnvelope, NotificationDeliveryResult, NotificationSinkConfig, NotificationsConfig } from '@harness-engineering/types';
3
+ import { IssueTrackerClient, Issue as Issue$1, CacheMetricsRecorder, TrackerConfig, eventSourcing, ArchiveHooks, SkillProposal, ProposalGateFinding, SkillKind } from '@harness-engineering/core';
4
4
  import { EnrichedSpec, ComplexityScore, SimulationResult, IntelligencePipeline, WeightedRecommendation, AnalysisProvider } from '@harness-engineering/intelligence';
5
5
  import { GraphStore } from '@harness-engineering/graph';
6
6
  import { execFile } from 'node:child_process';
@@ -45,6 +45,22 @@ interface RunningEntry {
45
45
  startedAt: string;
46
46
  phase: RunAttemptPhase;
47
47
  session: LiveSession | null;
48
+ /**
49
+ * AMR Phase 4 (D10/SC16): the tier the AdaptiveRouter resolved this dispatch
50
+ * at (`decision.tierRequired`), captured so a later quality outcome can feed
51
+ * `AdaptiveRouter.recordOutcome(issue.id, lastRoutedTier, ok)`. Absent when
52
+ * AMR is off (no policy) or the dispatch used an override.
53
+ */
54
+ lastRoutedTier?: _harness_engineering_types.CapabilityTier;
55
+ /** split-routing: the plan this unit is running staged, if any. Engine-owned. */
56
+ workflow?: WorkflowExecutionPlan;
57
+ /** split-routing: the stage index currently executing. Engine-owned. */
58
+ currentStageIndex?: number;
59
+ /**
60
+ * split-routing: per-stage runs. The per-stage session/abort live HERE
61
+ * (C1), never in the issue-level `session` field above.
62
+ */
63
+ stageRuns?: StageRun[];
48
64
  }
49
65
  /**
50
66
  * Entry in the retry queue.
@@ -1001,99 +1017,537 @@ declare function discoverSkillCatalog(projectRoot: string): SkillCatalogEntry[];
1001
1017
  declare function discoverSkillCatalogNames(projectRoot: string): string[];
1002
1018
 
1003
1019
  /**
1004
- * Adapter for using a markdown roadmap file as an issue tracker.
1020
+ * split-routing D5/D12/D13 the result of the doubly-opt-in match.
1005
1021
  *
1006
- * This adapter parses a standard Harness roadmap file, extracts features,
1007
- * and maps them to the internal Issue model using deterministic hashing
1008
- * for identifiers.
1022
+ * `workflowFor` is the SINGLE matching authority: it returns BOTH the execution
1023
+ * `plan` AND the matched decl's optional `stageDeadlineMs` (D12) so `dispatchIssue`
1024
+ * never re-runs the prefix+labels+`>= 2` predicate to recover the deadline. One
1025
+ * match, one source of truth — no drift between the predicate and a second matcher.
1009
1026
  */
1010
- declare class RoadmapTrackerAdapter implements IssueTrackerClient {
1011
- private config;
1027
+ interface WorkflowMatch {
1028
+ /** The execution plan for the matched >= 2-stage decl. */
1029
+ plan: WorkflowExecutionPlan;
1030
+ /** The matched decl's per-stage deadline override (D12), if declared. */
1031
+ stageDeadlineMs?: number;
1032
+ }
1033
+ /**
1034
+ * split-routing D5/D13 — the doubly-opt-in gate as a PURE matcher.
1035
+ *
1036
+ * Selects a `WorkflowMatch` for `issue` iff `config.workflows` declares a
1037
+ * matching decl with `stages.length >= 2`. Match grain (v1): an
1038
+ * `identifierPrefix` and/or `labels`; both, when declared, must hold. The FIRST
1039
+ * matching decl wins. The returned `WorkflowMatch` carries the matched decl's
1040
+ * `stageDeadlineMs` (D12) so the caller reads it from HERE rather than
1041
+ * re-matching — this function is the single match authority.
1042
+ *
1043
+ * D13: a matching 1-stage decl returns `undefined` — the unit takes the
1044
+ * single-agent path (a 1-stage "workflow" is not staged). A 0-stage decl never
1045
+ * reaches here (rejected at config validation).
1046
+ *
1047
+ * SC4 — this matcher has ZERO side effects: it never touches the workspace,
1048
+ * claims, routes, logs, or mutates state. When it returns `undefined`
1049
+ * `dispatchIssue` falls through to the byte-identical single-agent path. Keep it
1050
+ * cheap and pure so calling it on every dispatch cannot change non-workflow
1051
+ * behavior.
1052
+ */
1053
+ declare function workflowFor(issue: Issue, config: WorkflowConfig): WorkflowMatch | undefined;
1054
+
1055
+ interface RoutingDecisionBusFilter {
1056
+ skillName?: string;
1057
+ mode?: string;
1058
+ backendName?: string;
1059
+ limit?: number;
1060
+ }
1061
+ interface RoutingDecisionBusOptions {
1062
+ /** Default 500. Bound on the in-memory ring buffer. */
1063
+ capacity?: number;
1012
1064
  /**
1013
- * Creates a new RoadmapTrackerAdapter.
1014
- *
1015
- * @param config - The tracker configuration including the file path
1065
+ * Logger for the structured `routing-decision` line (O1) and for
1066
+ * one-off warn() when a subscriber throws (S6). When omitted, the
1067
+ * bus silently swallows subscriber errors (test-mode default).
1016
1068
  */
1017
- constructor(config: TrackerConfig);
1069
+ logger?: StructuredLogger;
1070
+ }
1071
+ /**
1072
+ * Spec B Phase 4 (D8): in-process bus + ring buffer for
1073
+ * {@link RoutingDecision} events. One emit() per
1074
+ * {@link BackendRouter.resolve} call; subscribers receive the
1075
+ * decision synchronously after the ring buffer is updated.
1076
+ *
1077
+ * Subscriber errors are isolated (caught + logged, never thrown
1078
+ * back to the emitter) so a misbehaving subscriber cannot block a
1079
+ * dispatch. (S6)
1080
+ *
1081
+ * Capacity-bound (default 500) via Array.shift() — acceptable for
1082
+ * v1 (see plan C4); switch to circular indexing if 24h dispatch
1083
+ * volume ever pushes 10K+ records/min.
1084
+ */
1085
+ declare class RoutingDecisionBus {
1086
+ private readonly ringBuffer;
1087
+ private readonly listeners;
1088
+ private readonly capacity;
1089
+ private readonly logger;
1090
+ constructor(opts?: RoutingDecisionBusOptions);
1091
+ emit(decision: RoutingDecision): void;
1092
+ recent(filter?: RoutingDecisionBusFilter): RoutingDecision[];
1093
+ subscribe(listener: (d: RoutingDecision) => void): () => void;
1018
1094
  /**
1019
- * Fetches all issues that are in an "active" state according to the config.
1095
+ * Spec B Phase 5 (review-S2 fix): release all subscriber references so
1096
+ * teardown can complete without anchoring closures. Called from
1097
+ * `Orchestrator.stop()` before nulling the bus reference. The bus
1098
+ * remains usable after clear — `subscribe()` works as normal.
1020
1099
  */
1021
- fetchCandidateIssues(): Promise<Result<Issue$1[], Error>>;
1100
+ clearListeners(): void;
1101
+ }
1102
+
1103
+ interface BackendRouterOptions {
1104
+ backends: Record<string, BackendDef>;
1105
+ routing: RoutingConfig;
1022
1106
  /**
1023
- * Fetches issues that match any of the given state names.
1024
- *
1025
- * @param stateNames - List of statuses to filter by
1107
+ * Spec B Phase 4 (D8): when present, every resolve() emits its
1108
+ * decision onto the bus. The bus owns the structured log line + ring
1109
+ * buffer; the router stays a pure resolution function.
1026
1110
  */
1027
- fetchIssuesByStates(stateNames: string[]): Promise<Result<Issue$1[], Error>>;
1111
+ decisionBus?: RoutingDecisionBus;
1112
+ }
1113
+ /**
1114
+ * BackendRouter (Spec B Phase 1)
1115
+ *
1116
+ * Owns the lookup from a {@link RoutingUseCase} (a discriminated query
1117
+ * — tier, intelligence layer, maintenance, chat, isolation, **skill**,
1118
+ * **mode**) to a {@link RoutingDecision} naming a chosen backend and
1119
+ * the full resolution path that produced it.
1120
+ *
1121
+ * Resolution order (D2): invocation override -> per-skill -> per-mode
1122
+ * -> existing per-tier/intelligence/isolation/maintenance/chat ->
1123
+ * `routing.default`. Within each source, fallback chain entries are
1124
+ * tried in declared order; first existing backend wins. Unknown
1125
+ * entries are recorded with `outcome: 'unknown-backend'` and the walk
1126
+ * continues.
1127
+ *
1128
+ * Construction-time validation guarantees every name referenced by
1129
+ * `routing` is present in `backends` so the static-config case can
1130
+ * never produce a runtime exhaustion throw. The runtime throw at the
1131
+ * end of `resolve()` is a safety net for future dynamic-backends
1132
+ * scenarios where a chain entry can become unknown post-construction.
1133
+ */
1134
+ declare class BackendRouter {
1135
+ private readonly backends;
1136
+ private readonly routing;
1137
+ private readonly decisionBus;
1138
+ constructor(opts: BackendRouterOptions);
1028
1139
  /**
1029
- * Transitions a roadmap feature into the first configured terminal state
1030
- * and rewrites the markdown file. Idempotent: if the feature is already
1031
- * in a terminal state, this is a no-op.
1140
+ * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
1032
1141
  *
1033
- * The orchestrator calls this after a successful agent exit so the feature
1034
- * is no longer returned by `fetchCandidateIssues` on the next tick — and,
1035
- * critically, after an orchestrator restart.
1142
+ * @param useCase the routing query
1143
+ * @param opts.invocationOverride if set and the named backend exists,
1144
+ * beats all other sources (D7 — the `--backend <name>` escape hatch)
1036
1145
  */
1037
- markIssueComplete(issueId: string): Promise<Result<void, Error>>;
1146
+ resolve(useCase: RoutingUseCase, opts?: {
1147
+ invocationOverride?: string;
1148
+ }): RoutingDecision;
1038
1149
  /**
1039
- * Claims an issue by transitioning its status to "in-progress" and
1040
- * writing the orchestratorId into the assignee field. Idempotent if
1041
- * already claimed by the same orchestratorId.
1150
+ * Returns the {@link BackendDef} reference for the resolved name.
1151
+ * Identity-equal to the entry in `backends` (no copy) so callers
1152
+ * relying on reference equality (SC21) continue to work.
1042
1153
  */
1043
- claimIssue(issueId: string, orchestratorId: string): Promise<Result<void, Error>>;
1154
+ resolveDefinition(useCase: RoutingUseCase, opts?: {
1155
+ invocationOverride?: string;
1156
+ }): BackendDef;
1044
1157
  /**
1045
- * Releases a claimed issue by transitioning back to the first active
1046
- * state and clearing the assignee field.
1158
+ * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
1159
+ * for callers that need both. Replaces the previous pattern of
1160
+ * `resolveDefinition(useCase) + resolve(useCase)` which produced two
1161
+ * RoutingDecision emissions per dispatch — doubling routing-decision
1162
+ * log volume now that Phase 4 emits.
1163
+ *
1164
+ * Identity-equal `BackendDef` (no copy) so callers relying on
1165
+ * reference equality (SC21) continue to work.
1047
1166
  */
1048
- releaseIssue(issueId: string): Promise<Result<void, Error>>;
1167
+ resolveDecisionAndDef(useCase: RoutingUseCase, opts?: {
1168
+ invocationOverride?: string;
1169
+ }): {
1170
+ decision: RoutingDecision;
1171
+ def: BackendDef;
1172
+ };
1049
1173
  /**
1050
- * Resolve the roadmap store anchored on the configured aggregate file path.
1051
- * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
1052
- * file; otherwise the monolith file is read/written whole. Callers guard
1053
- * `this.config.filePath` before invoking.
1174
+ * The pre-Spec-B resolution helper: returns the configured
1175
+ * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
1176
+ * use cases (or `undefined` for skill/mode use cases, which are owned
1177
+ * by the per-skill / per-mode steps in {@link resolve}). Returning
1178
+ * `undefined` lets the caller fall through to `routing.default`.
1054
1179
  */
1055
- private resolveStore;
1056
- /** Load the roadmap via the store (sharded or monolith), returning a Result. */
1057
- private loadRoadmap;
1058
- private findFeatureById;
1180
+ private resolveExistingUseCase;
1181
+ private validateReferences;
1182
+ }
1183
+
1184
+ /**
1185
+ * Options for `OrchestratorBackendFactory`.
1186
+ *
1187
+ * `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own
1188
+ * agent-config fields. `getResolverModelFor` is a registration hook the
1189
+ * orchestrator calls to bind each `local`/`pi` backend to its
1190
+ * `LocalModelResolver` (so multi-resolver array-fallback works without
1191
+ * leaking resolver lifetimes into the factory).
1192
+ */
1193
+ /**
1194
+ * Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete.
1195
+ * See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}.
1196
+ */
1197
+ interface LocalModelUsageHooks {
1198
+ /** Fired with the resolved model after a successful turn (LRU + breaker clear). */
1199
+ onModelUsed?: (model: string) => void;
1200
+ /** Fired with the resolved model after a failed turn (breaker increment). */
1201
+ onModelFailed?: (model: string) => void;
1202
+ }
1203
+ interface OrchestratorBackendFactoryOptions {
1204
+ backends: Record<string, BackendDef>;
1205
+ routing: RoutingConfig;
1206
+ sandboxPolicy: 'none' | 'docker';
1207
+ container?: ContainerConfig;
1208
+ secrets?: SecretConfig;
1059
1209
  /**
1060
- * Fetches full issue details for a list of identifiers.
1210
+ * Hook for resolver injection. Invoked per `local`/`pi` backend at
1211
+ * `forUseCase()` time with the backend's name. When the hook returns a
1212
+ * function, the factory rebuilds the local/pi instance using that
1213
+ * function as `getModel` (overriding the head-of-array placeholder
1214
+ * baked into `createBackend`). Returning `undefined` means "no
1215
+ * resolver registered for this name" — the placeholder stays in place.
1061
1216
  *
1062
- * @param issueIds - List of issue IDs to fetch
1217
+ * This indirection keeps the factory ignorant of `LocalModelResolver`'s
1218
+ * existence and lifecycle while still letting it produce backends that
1219
+ * route through the resolver Map.
1063
1220
  */
1064
- fetchIssueStatesByIds(issueIds: string[]): Promise<Result<Map<string, Issue$1>, Error>>;
1221
+ getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined;
1065
1222
  /**
1066
- * Maps a raw RoadmapFeature from the parser to the unified Issue model.
1223
+ * Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the
1224
+ * runtime-feedback callbacks bound to that backend's resolver + pool. The
1225
+ * factory forwards them to the constructed `LocalBackend` so a completed turn
1226
+ * stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn
1227
+ * feeds the breaker. Returning `undefined` means "no feedback wiring" (the
1228
+ * backend runs exactly as before). Kept parallel to `getResolverModelFor` so
1229
+ * the factory stays ignorant of resolver/pool lifecycles.
1067
1230
  */
1068
- private mapFeatureToIssue;
1231
+ getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined;
1069
1232
  /**
1070
- * Generates a deterministic, URL-safe identifier for a feature name.
1233
+ * Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
1234
+ * Other backends accept-but-ignore. Shared across dispatches so the
1235
+ * `/api/v1/telemetry/cache/stats` endpoint sees the full rolling window.
1071
1236
  */
1072
- private generateId;
1237
+ cacheMetrics?: CacheMetricsRecorder;
1238
+ /**
1239
+ * Spec B Phase 4 (D8): forwarded to the underlying BackendRouter so
1240
+ * every resolve() during forUseCase / resolveName emits.
1241
+ */
1242
+ decisionBus?: RoutingDecisionBus;
1073
1243
  }
1074
-
1075
1244
  /**
1076
- * Interface for a Linear GraphQL tool extension — a thin authenticated client
1077
- * over Linear's GraphQL API (https://linear.app/developers/graphql).
1245
+ * High-level factory wrapping `BackendRouter` + `createBackend` plus
1246
+ * orchestrator-side concerns (sandbox wrapping, resolver binding).
1247
+ *
1248
+ * Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh
1249
+ * `AgentBackend` whose class matches the routed `BackendDef.type`.
1250
+ * `local`/`pi` defs are bound to their per-name resolver before being
1251
+ * returned, and the result is wrapped in `ContainerBackend` when
1252
+ * sandboxPolicy is 'docker'.
1078
1253
  */
1079
- interface LinearGraphQLExtension {
1254
+ declare class OrchestratorBackendFactory {
1255
+ private readonly router;
1256
+ private readonly opts;
1257
+ constructor(opts: OrchestratorBackendFactoryOptions);
1080
1258
  /**
1081
- * Execute a GraphQL operation. Resolves to `Ok(data)` (the `data` object of
1082
- * the GraphQL envelope) on success, or `Err` on a transport failure, a non-2xx
1083
- * HTTP status, or a GraphQL `errors` array.
1259
+ * Resolve `useCase` to a backend name, materialize a fresh
1260
+ * `AgentBackend`, optionally rebind its model resolver, and apply
1261
+ * sandbox wrapping. Idempotent across calls (no caching) — the AgentRunner
1262
+ * holds the per-dispatch reference and discards it when the run ends.
1084
1263
  */
1085
- query(query: string, variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
1086
- }
1087
- interface LinearGraphQLClientOptions {
1088
1264
  /**
1089
- * Linear authentication token. A personal API key is sent verbatim in the
1090
- * `Authorization` header; an OAuth access token must be passed already prefixed
1091
- * with `Bearer ` (Linear's two supported schemes).
1092
- */
1093
- apiKey: string;
1094
- /** Override the GraphQL endpoint (e.g. a proxy). Defaults to Linear's API. */
1095
- endpoint?: string;
1096
- /** Injectable fetch for tests; defaults to the global `fetch`. */
1265
+ * Resolve `useCase` to its routed backend name, exposing the
1266
+ * router lookup without materializing a backend. Used by callers
1267
+ * (e.g., the orchestrator's dispatch site) that need to label
1268
+ * telemetry with the routed name BEFORE constructing the backend.
1269
+ *
1270
+ * Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
1271
+ * + `StreamRecorder` with the legacy `agent.backend` field, which
1272
+ * is `undefined` for pure-modern configs. Threading the routed name
1273
+ * through dispatch eliminates that gap.
1274
+ */
1275
+ resolveName(useCase: RoutingUseCase, opts?: {
1276
+ invocationOverride?: string;
1277
+ }): string;
1278
+ /**
1279
+ * Spec B Phase 1: expose the underlying router for callers that need
1280
+ * it directly (e.g., {@link buildIntelligencePipeline} for the
1281
+ * I1 SEL/PESL comparison fix). Read-only access; consumers must not
1282
+ * mutate router state.
1283
+ */
1284
+ getRouter(): BackendRouter;
1285
+ forUseCase(useCase: RoutingUseCase, opts?: {
1286
+ invocationOverride?: string;
1287
+ }): AgentBackend;
1288
+ /**
1289
+ * Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
1290
+ * mirroring `createBackend`'s local/pi branches but substituting the
1291
+ * head-of-array placeholder with the orchestrator-owned resolver.
1292
+ */
1293
+ private buildLocalLikeWithResolver;
1294
+ /**
1295
+ * Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
1296
+ * backend per call so each dispatch sees a fresh container handle map
1297
+ * (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
1298
+ */
1299
+ private wrapInContainer;
1300
+ }
1301
+
1302
+ /**
1303
+ * Narrow surface the stage-execution engine needs. The Orchestrator will
1304
+ * implement this in Phase 4 when dispatchIssue wires the branch; the engine
1305
+ * must NOT import orchestrator.ts (layer cycle). Phase 1 tests inject a fake.
1306
+ */
1307
+ interface WorkflowEngineContext {
1308
+ recorder: StreamRecorder;
1309
+ logger: StructuredLogger;
1310
+ /** issueId of the coherence unit — used for recorder keys + terminal transitions. */
1311
+ issueId: string;
1312
+ identifier: string;
1313
+ externalId: string | null;
1314
+ workspacePath: string;
1315
+ /**
1316
+ * D12 (SC7): per-stage wall-clock deadline in ms. If a stage does not finish
1317
+ * within it, the engine fires the per-stage abort (running the runner's
1318
+ * `finally { stopSession }` via `gen.return()`) and treats the stage as
1319
+ * `passed:false` → feeds the D8 retry/terminal path (never an unbounded hang).
1320
+ * Absent ⇒ the engine default (`DEFAULT_STAGE_DEADLINE_MS`). Per-stage/config
1321
+ * override is Phase 4.
1322
+ */
1323
+ stageDeadlineMs?: number;
1324
+ /** Build a runner for a stage's routed backend. Phase 1: single stubbed backend. */
1325
+ makeRunner(backend: AgentBackend): {
1326
+ runSession: (issue: unknown, ws: string, prompt: string) => AsyncGenerator<AgentEvent, {
1327
+ sessionId: string;
1328
+ success: boolean;
1329
+ usage: {
1330
+ inputTokens: number;
1331
+ outputTokens: number;
1332
+ totalTokens: number;
1333
+ };
1334
+ }, void>;
1335
+ };
1336
+ /** Phase 1 stub: resolve the single backend for a stage (Phase 2 replaces with route()). */
1337
+ resolveStageBackend(step: WorkflowExecutionPlan['stages'][number]): AgentBackend;
1338
+ /**
1339
+ * split-routing 4b: render the REAL per-stage prompt (issue + stage role +
1340
+ * prior-stage outputs), replacing the Phase-1 bare skill-name stub. Bound by
1341
+ * `buildWorkflowContext` via a `PromptRenderer` (never imports orchestrator).
1342
+ * `priorOutputs` maps each prior stage's `produces` label → its captured output.
1343
+ * Optional so fake contexts that don't exercise prompt content fall back to the
1344
+ * skill name (byte-identical to the old stub).
1345
+ *
1346
+ * CONTRACT: must be non-blocking (CPU-bound). It runs BEFORE the per-stage
1347
+ * deadline timer is armed, so an I/O-bound renderer that hangs would stall the
1348
+ * stage with no wall-clock bound. The shipped renderer is synchronous LiquidJS.
1349
+ */
1350
+ renderStagePrompt?(step: WorkflowExecutionPlan['stages'][number], index: number, priorOutputs: Record<string, string>): string | Promise<string>;
1351
+ /**
1352
+ * split-routing Phase 2: per-stage adaptive router. Present ⇒ each stage is
1353
+ * routed via `route(buildStageRequest(...))`; ABSENT ⇒ identity fallback via
1354
+ * `resolveStageBackend` (no `routing.policy`). Narrow surface only (route +
1355
+ * recordOutcome) so the engine stays off the orchestrator import cycle.
1356
+ *
1357
+ * `route()` returns `{ decision }` only — the engine derives the backend name
1358
+ * from `decision.backendName` (a `BackendDef` carries no `name`; the name is
1359
+ * the router map key), so `def` is deliberately not part of this surface.
1360
+ */
1361
+ adaptiveRouter?: {
1362
+ route(req: RoutingRequest): Promise<{
1363
+ decision: RoutingDecision;
1364
+ }>;
1365
+ recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
1366
+ };
1367
+ /** Terminal success — exactly one running/claimed delete + one lane persist (D6). */
1368
+ emitWorkflowSuccess(unit: string, runs: StageRun[]): Promise<void>;
1369
+ /** Terminal failure/safety-net — exactly one running/claimed delete + one lane persist (D6/I1). */
1370
+ finalizeWorkflowTerminal(unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown): Promise<void>;
1371
+ }
1372
+
1373
+ /**
1374
+ * split-routing Phase 4 — the narrow adaptive-router surface the workflow engine
1375
+ * consumes (`route` + `recordOutcome`). Structurally identical to
1376
+ * `WorkflowEngineContext.adaptiveRouter` and to the real `AdaptiveRouter`'s public
1377
+ * methods; typed here so the dep bag never depends on the concrete router class
1378
+ * (and never on `orchestrator.ts`).
1379
+ */
1380
+ interface WorkflowRouterDep {
1381
+ route(req: RoutingRequest): Promise<{
1382
+ decision: RoutingDecision;
1383
+ }>;
1384
+ recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
1385
+ }
1386
+ /**
1387
+ * The dependency bag `buildWorkflowContext` composes the real
1388
+ * `WorkflowEngineContext` from. It is passed IN by `dispatchIssue` (Task 8) rather
1389
+ * than read off `this`, so this module imports only same-layer siblings
1390
+ * (`agent/runner`, `agent/orchestrator-backend-factory` types, `core/stream-recorder`)
1391
+ * and NEVER `orchestrator.ts` — the layer-cycle guard the context surface exists
1392
+ * for (concern #3). The two terminal seams are supplied as `settleSuccess` /
1393
+ * `settleTerminal` callbacks the orchestrator binds to its private methods (Task 7).
1394
+ */
1395
+ interface BuildWorkflowContextDeps {
1396
+ recorder: StreamRecorder;
1397
+ logger: StructuredLogger;
1398
+ /** The dispatch unit; issueId/identifier/externalId flow from here. */
1399
+ issue: Issue;
1400
+ /** The unit's ONE worktree (from the single `ensureWorkspace` in dispatchIssue). */
1401
+ workspacePath: string;
1402
+ /** `this.config.agent.maxTurns` — the per-stage runner turn cap. */
1403
+ maxTurns: number;
1404
+ /**
1405
+ * The orchestrator's backend factory. Used to materialize the real routed
1406
+ * backend for a stage: `forUseCase(useCase, { invocationOverride: name })`
1407
+ * mirrors the AMR dispatch swap (orchestrator.ts:2062-2064). `null` when
1408
+ * migration failed (legacy-only config) — then the identity fallback uses the
1409
+ * name-only `routingDefault` backend.
1410
+ */
1411
+ backendFactory: OrchestratorBackendFactory | null;
1412
+ /**
1413
+ * The real `AdaptiveRouter` (narrowed) when `routing.policy` is set
1414
+ * (orchestrator.ts:698), else `null` ⇒ the engine takes the identity fallback
1415
+ * (D5). Only `route`/`recordOutcome` are consumed.
1416
+ */
1417
+ adaptiveRouter: WorkflowRouterDep | null;
1418
+ /** `this.config.agent.routing?.default` — the identity-fallback backend name. */
1419
+ routingDefault: string | undefined;
1420
+ /** D12 override; absent ⇒ engine default DEFAULT_STAGE_DEADLINE_MS. */
1421
+ stageDeadlineMs?: number;
1422
+ /** Terminal-success seam (Task 6/7): bound to `settleWorkflowSuccess`. */
1423
+ settleSuccess: (unit: string, runs: StageRun[]) => Promise<void>;
1424
+ /** Terminal-failure/safety-net seam (Task 6/7): bound to `settleWorkflowTerminal`. */
1425
+ settleTerminal: (unit: string, runs: StageRun[], failingStep?: WorkflowExecutionPlan['stages'][number], err?: unknown) => Promise<void>;
1426
+ }
1427
+ /**
1428
+ * split-routing Phase 4 — compose the REAL `WorkflowEngineContext` (minus the two
1429
+ * terminal seams, which Task 6 completes) from orchestrator machinery.
1430
+ *
1431
+ * Seam → real code:
1432
+ * - `recorder`/`logger`/`issueId`/`identifier`/`externalId`/`workspacePath` → the deps.
1433
+ * - `stageDeadlineMs` → `deps.stageDeadlineMs` (conditional; engine defaults when absent).
1434
+ * - `makeRunner(backend)` → `new AgentRunner(realBackend, { maxTurns })`. The engine
1435
+ * passes a name-only backend for the routed path (execute-workflow.ts:320); we
1436
+ * re-materialize the real backend from that name via
1437
+ * `backendFactory.forUseCase(useCase, { invocationOverride: backend.name })`
1438
+ * (same swap as orchestrator.ts:2062-2064). Its `runSession` returns a
1439
+ * `TurnResult` (runner.ts:112) — the seam is already typed against it
1440
+ * (carry-forward #9), confirmed self-documenting by the explicit annotation below.
1441
+ * - `resolveStageBackend(step)` → identity fallback: `backendFactory.forUseCase(useCase)`
1442
+ * when the factory exists, else a name-only backend from `routingDefault`.
1443
+ * - `adaptiveRouter` → present iff `deps.adaptiveRouter !== null` (D5).
1444
+ *
1445
+ * The two TERMINAL seams (`emitWorkflowSuccess` / `finalizeWorkflowTerminal`,
1446
+ * SC5) are thin forwarders to the orchestrator's `settleSuccess` / `settleTerminal`
1447
+ * callbacks. They are NOT routed through `emitWorkerExit`/`handleWorkerExit`
1448
+ * (which fire the ISSUE-keyed `finishRecording(attempt)` + `recordAmrOutcome` — a
1449
+ * DOUBLE-fire, since the engine already ran PER-STAGE recorders + per-stage
1450
+ * `recordOutcome`). The reducer-reproduction (running.delete → completed.set →
1451
+ * claimed.delete → cleanWorkspace → persist → emit for success; and the
1452
+ * finalizeRoutingTerminal + needs-human + cleanWorkspace sequence for terminal)
1453
+ * lives in the orchestrator's private methods (Task 7) the callbacks bind to.
1454
+ */
1455
+ declare function buildWorkflowContext(deps: BuildWorkflowContextDeps): WorkflowEngineContext;
1456
+
1457
+ /**
1458
+ * Adapter for using a markdown roadmap file as an issue tracker.
1459
+ *
1460
+ * This adapter parses a standard Harness roadmap file, extracts features,
1461
+ * and maps them to the internal Issue model using deterministic hashing
1462
+ * for identifiers.
1463
+ */
1464
+ declare class RoadmapTrackerAdapter implements IssueTrackerClient {
1465
+ private config;
1466
+ /**
1467
+ * Creates a new RoadmapTrackerAdapter.
1468
+ *
1469
+ * @param config - The tracker configuration including the file path
1470
+ */
1471
+ constructor(config: TrackerConfig);
1472
+ /**
1473
+ * Fetches all issues that are in an "active" state according to the config.
1474
+ */
1475
+ fetchCandidateIssues(): Promise<Result<Issue$1[], Error>>;
1476
+ /**
1477
+ * Fetches issues that match any of the given state names.
1478
+ *
1479
+ * @param stateNames - List of statuses to filter by
1480
+ */
1481
+ fetchIssuesByStates(stateNames: string[]): Promise<Result<Issue$1[], Error>>;
1482
+ /**
1483
+ * Transitions a roadmap feature into the first configured terminal state
1484
+ * and rewrites the markdown file. Idempotent: if the feature is already
1485
+ * in a terminal state, this is a no-op.
1486
+ *
1487
+ * The orchestrator calls this after a successful agent exit so the feature
1488
+ * is no longer returned by `fetchCandidateIssues` on the next tick — and,
1489
+ * critically, after an orchestrator restart.
1490
+ */
1491
+ markIssueComplete(issueId: string): Promise<Result<void, Error>>;
1492
+ /**
1493
+ * Claims an issue by transitioning its status to "in-progress" and
1494
+ * writing the orchestratorId into the assignee field. Idempotent if
1495
+ * already claimed by the same orchestratorId.
1496
+ */
1497
+ claimIssue(issueId: string, orchestratorId: string): Promise<Result<void, Error>>;
1498
+ /**
1499
+ * Releases a claimed issue by transitioning back to the first active
1500
+ * state and clearing the assignee field.
1501
+ */
1502
+ releaseIssue(issueId: string): Promise<Result<void, Error>>;
1503
+ /**
1504
+ * Resolve the roadmap store anchored on the configured aggregate file path.
1505
+ * The shard backend is chosen when a sibling `roadmap.d/` exists next to the
1506
+ * file; otherwise the monolith file is read/written whole. Callers guard
1507
+ * `this.config.filePath` before invoking.
1508
+ */
1509
+ private resolveStore;
1510
+ /** Load the roadmap via the store (sharded or monolith), returning a Result. */
1511
+ private loadRoadmap;
1512
+ private findFeatureById;
1513
+ /**
1514
+ * Fetches full issue details for a list of identifiers.
1515
+ *
1516
+ * @param issueIds - List of issue IDs to fetch
1517
+ */
1518
+ fetchIssueStatesByIds(issueIds: string[]): Promise<Result<Map<string, Issue$1>, Error>>;
1519
+ /**
1520
+ * Maps a raw RoadmapFeature from the parser to the unified Issue model.
1521
+ */
1522
+ private mapFeatureToIssue;
1523
+ /**
1524
+ * Generates a deterministic, URL-safe identifier for a feature name.
1525
+ */
1526
+ private generateId;
1527
+ }
1528
+
1529
+ /**
1530
+ * Interface for a Linear GraphQL tool extension — a thin authenticated client
1531
+ * over Linear's GraphQL API (https://linear.app/developers/graphql).
1532
+ */
1533
+ interface LinearGraphQLExtension {
1534
+ /**
1535
+ * Execute a GraphQL operation. Resolves to `Ok(data)` (the `data` object of
1536
+ * the GraphQL envelope) on success, or `Err` on a transport failure, a non-2xx
1537
+ * HTTP status, or a GraphQL `errors` array.
1538
+ */
1539
+ query(query: string, variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
1540
+ }
1541
+ interface LinearGraphQLClientOptions {
1542
+ /**
1543
+ * Linear authentication token. A personal API key is sent verbatim in the
1544
+ * `Authorization` header; an OAuth access token must be passed already prefixed
1545
+ * with `Bearer ` (Linear's two supported schemes).
1546
+ */
1547
+ apiKey: string;
1548
+ /** Override the GraphQL endpoint (e.g. a proxy). Defaults to Linear's API. */
1549
+ endpoint?: string;
1550
+ /** Injectable fetch for tests; defaults to the global `fetch`. */
1097
1551
  fetchFn?: typeof fetch;
1098
1552
  }
1099
1553
  /**
@@ -1124,6 +1578,24 @@ declare class LinearGraphQLStub implements LinearGraphQLExtension {
1124
1578
  query(query: string, _variables?: Record<string, unknown>): Promise<Result<unknown, Error>>;
1125
1579
  }
1126
1580
 
1581
+ /**
1582
+ * AMR 4c: a single-agent quality-verdict source (ADR 0069). The escalation
1583
+ * mechanism + seam are complete; this supplies the *sound* signal that was
1584
+ * missing — a BASELINE-RELATIVE scan of the diff the agent introduced. Only the
1585
+ * ADDED lines are scanned, so a pre-existing pattern never counts against the
1586
+ * agent. This is sound (not approximate) because every security rule is
1587
+ * single-line, so per-added-line matching yields exactly the introduced findings.
1588
+ */
1589
+ /** One hunk of added lines from a unified diff, with its true start line. */
1590
+ interface IntroducedHunk {
1591
+ /** Repo-relative path of the changed file. */
1592
+ file: string;
1593
+ /** The added lines (leading `+` stripped), joined with `\n`. */
1594
+ addedContent: string;
1595
+ /** New-file line number the first added line sits at (for accurate findings). */
1596
+ startLine: number;
1597
+ }
1598
+
1127
1599
  /**
1128
1600
  * Structured event emitted when {@link WorkspaceManager.resolveBaseRef}
1129
1601
  * falls back past `origin/HEAD` and `origin/main`/`origin/master` to a
@@ -1163,6 +1635,27 @@ declare class WorkspaceManager {
1163
1635
  * Resolves the full path for an issue's workspace.
1164
1636
  */
1165
1637
  resolvePath(identifier: string): string;
1638
+ /**
1639
+ * AMR 4c: the lines the dispatched agent INTRODUCED in its worktree, as
1640
+ * per-hunk added-line blocks — the sound input for a baseline-relative quality
1641
+ * scan (pre-existing content is excluded by construction). Diffs the working
1642
+ * tree (so both committed AND uncommitted agent work is captured) against the
1643
+ * MERGE-BASE of the worktree HEAD and the base ref — merge-base, not the base
1644
+ * ref itself, so a base branch that advanced mid-dispatch never attributes
1645
+ * other merges to this agent. The seeded handoff overlay (`seedPaths`) is
1646
+ * excluded — those files are not the agent's work.
1647
+ */
1648
+ getIntroducedDiff(identifier: string): Promise<IntroducedHunk[]>;
1649
+ /**
1650
+ * AMR 4c v2: the SAME introduced change as {@link getIntroducedDiff}, but as the
1651
+ * RAW unified diff text (default context, NOT `--unified=0`) — the input an LLM
1652
+ * spec-satisfaction eval needs to judge whether the diff satisfies the spec.
1653
+ * Merge-base relative for the same reason (a base branch that advanced
1654
+ * mid-dispatch never attributes other merges here), and the seeded handoff
1655
+ * overlay is excluded via git `:(exclude)` pathspecs so the judge never reads
1656
+ * pre-seeded proposal/roadmap content as the agent's work.
1657
+ */
1658
+ getIntroducedDiffText(identifier: string): Promise<string>;
1166
1659
  /**
1167
1660
  * Discovers the git repository root from the workspace root directory.
1168
1661
  */
@@ -1474,133 +1967,235 @@ interface MaintenanceStatus {
1474
1967
  history: RunResult[];
1475
1968
  }
1476
1969
 
1477
- interface RoutingDecisionBusFilter {
1478
- skillName?: string;
1479
- mode?: string;
1480
- backendName?: string;
1481
- limit?: number;
1482
- }
1483
- interface RoutingDecisionBusOptions {
1484
- /** Default 500. Bound on the in-memory ring buffer. */
1485
- capacity?: number;
1486
- /**
1487
- * Logger for the structured `routing-decision` line (O1) and for
1488
- * one-off warn() when a subscriber throws (S6). When omitted, the
1489
- * bus silently swallows subscriber errors (test-mode default).
1490
- */
1491
- logger?: StructuredLogger;
1492
- }
1493
1970
  /**
1494
- * Spec B Phase 4 (D8): in-process bus + ring buffer for
1495
- * {@link RoutingDecision} events. One emit() per
1496
- * {@link BackendRouter.resolve} call; subscribers receive the
1497
- * decision synchronously after the ring buffer is updated.
1498
- *
1499
- * Subscriber errors are isolated (caught + logged, never thrown
1500
- * back to the emitter) so a misbehaving subscriber cannot block a
1501
- * dispatch. (S6)
1502
- *
1503
- * Capacity-bound (default 500) via Array.shift() — acceptable for
1504
- * v1 (see plan C4); switch to circular indexing if 24h dispatch
1505
- * volume ever pushes 10K+ records/min.
1971
+ * D10 vertical escalation. Per-`coherenceUnit` quality-failure counter that
1972
+ * raises the unit's floor tier one step on the Nth consecutive QUALITY failure
1973
+ * (never transport that is the shipped per-model breaker). Monotonic +
1974
+ * `strong`-capped cannot loop or thrash. Naming mirrors
1975
+ * `LocalModelResolver.recordSuccess/recordFailure` (local-model-resolver.ts:460/474).
1506
1976
  */
1507
- declare class RoutingDecisionBus {
1508
- private readonly ringBuffer;
1509
- private readonly listeners;
1510
- private readonly capacity;
1511
- private readonly logger;
1512
- constructor(opts?: RoutingDecisionBusOptions);
1513
- emit(decision: RoutingDecision): void;
1514
- recent(filter?: RoutingDecisionBusFilter): RoutingDecision[];
1515
- subscribe(listener: (d: RoutingDecision) => void): () => void;
1977
+ declare class EscalationState {
1978
+ private readonly threshold;
1979
+ private readonly units;
1980
+ constructor(threshold?: number);
1981
+ /**
1982
+ * The current escalation floor for a unit — the minimum tier `route()` must
1983
+ * resolve at. Defaults to `'fast'` (no-op floor) for an unknown/absent unit,
1984
+ * so an un-escalated request derives its tier normally.
1985
+ */
1986
+ floorFor(coherenceUnit?: string): CapabilityTier;
1987
+ /** D10 mechanism flag: has this unit ever climbed a tier? (Phase 6 reads this for Tier-A disqualification.) */
1988
+ isEscalated(coherenceUnit?: string): boolean;
1989
+ /**
1990
+ * AMR observability: the coherence units that have climbed ABOVE the `fast`
1991
+ * floor, with their current floor tier. Operator status only (read-only) — an
1992
+ * empty list means nothing has escalated. Units still at `fast` are omitted.
1993
+ */
1994
+ climbedUnits(): Array<{
1995
+ coherenceUnit: string;
1996
+ floor: CapabilityTier;
1997
+ }>;
1516
1998
  /**
1517
- * Spec B Phase 5 (review-S2 fix): release all subscriber references so
1518
- * teardown can complete without anchoring closures. Called from
1519
- * `Orchestrator.stop()` before nulling the bus reference. The bus
1520
- * remains usable after clear `subscribe()` works as normal.
1999
+ * SC16: a QUALITY failure at `tier` increments this unit's counter; on the
2000
+ * Nth (threshold) consecutive failure the floor climbs one step (fast→standard
2001
+ * →strong), the count resets, and `escalated` latches true. `strong` is the
2002
+ * ceiling: a threshold-crossing failure already at `strong` returns
2003
+ * 'exhausted' (router emits routing:escalation-exhausted). `ok` clears the
2004
+ * in-progress count but leaves the raised floor (monotonic per D10).
2005
+ *
2006
+ * `_tier` (the tier the outcome occurred at) is ADVISORY/UNUSED today: the climb
2007
+ * is driven entirely off the unit's own `state.floorTier`, not off this argument.
2008
+ * It is kept in the signature so the call-site contract stays stable for a future
2009
+ * refinement that guards against out-of-order (stale-tier) reports. (Note: this is
2010
+ * NOT positionally mirroring `LocalModelResolver.recordFailure`, which keys on a
2011
+ * model identifier and takes no tier — the earlier comment claiming that shape was
2012
+ * misleading.)
1521
2013
  */
1522
- clearListeners(): void;
2014
+ recordOutcome(coherenceUnit: string, _tier: CapabilityTier, ok: boolean): 'ok' | 'escalated' | 'exhausted';
1523
2015
  }
1524
2016
 
1525
- interface BackendRouterOptions {
1526
- backends: Record<string, BackendDef>;
1527
- routing: RoutingConfig;
2017
+ interface AdaptiveRouterDeps {
2018
+ router: BackendRouter;
2019
+ registry: BackendCapabilityRegistry;
2020
+ policy: RoutingPolicy;
2021
+ /**
2022
+ * Complexity classifier. Accepts a sync verdict-provider (Phase-3 conservative
2023
+ * stub) OR an async classifier (D4 live cascade); `route()` awaits either shape.
2024
+ * A rejection/throw is caught and degraded to `{moderate, low}` (never blocks
2025
+ * dispatch — Failure modes / D4).
2026
+ */
2027
+ classify: (req: RoutingRequest) => ComplexityVerdict | Promise<ComplexityVerdict>;
2028
+ /** name → provider type, derived from agent.backends (Task 6). REQUIRED when policy.allowedProviders is set. */
2029
+ providerOf?: (name: string) => BackendDef['type'] | undefined;
2030
+ /** Injected spend snapshot (D8/S1-001). Defaults to 0-spend. */
2031
+ budgetState?: () => {
2032
+ spentUsd: number;
2033
+ };
1528
2034
  /**
1529
- * Spec B Phase 4 (D8): when present, every resolve() emits its
1530
- * decision onto the bus. The bus owns the structured log line + ring
1531
- * buffer; the router stays a pure resolution function.
2035
+ * D10 vertical escalation state. When present, `route()` uses
2036
+ * `escalation.floorFor(req.coherenceUnit)` as the tier floor (a climbed unit
2037
+ * resolves higher); when absent, the floor is the no-op `'fast'`.
2038
+ */
2039
+ escalation?: EscalationState;
2040
+ /**
2041
+ * D10 steward-escalation seam. Invoked with the `coherenceUnit` when
2042
+ * `recordOutcome` returns `'exhausted'` (floor already `strong`, re-crossed the
2043
+ * threshold). The orchestrator binds this to `routingDecisionBus`/logger to emit
2044
+ * `routing:escalation-exhausted`.
2045
+ */
2046
+ onExhausted?: (coherenceUnit: string) => void;
2047
+ /**
2048
+ * SC9: enriched-decision bus. The shipped `resolveDecisionAndDef` emits the
2049
+ * BASE decision inside the D2-frozen router (no `complexity`/`tierRequired`/
2050
+ * `estCostUsd`). When `AdaptiveRouter` owns live dispatch it emits the
2051
+ * ENRICHED decision here so a subscriber actually receives the AMR telemetry.
2052
+ * Absent ⇒ no enriched emit (and when AMR is off the router isn't constructed,
2053
+ * so nothing changes on the bus — SC8/SC17 preserved).
1532
2054
  */
1533
2055
  decisionBus?: RoutingDecisionBus;
1534
2056
  }
1535
2057
  /**
1536
- * BackendRouter (Spec B Phase 1)
2058
+ * AMR Phase 3 (D2): wraps never modifies — the shipped {@link BackendRouter}.
1537
2059
  *
1538
- * Owns the lookup from a {@link RoutingUseCase} (a discriminated query
1539
- * tier, intelligence layer, maintenance, chat, isolation, **skill**,
1540
- * **mode**) to a {@link RoutingDecision} naming a chosen backend and
1541
- * the full resolution path that produced it.
1542
- *
1543
- * Resolution order (D2): invocation override -> per-skill -> per-mode
1544
- * -> existing per-tier/intelligence/isolation/maintenance/chat ->
1545
- * `routing.default`. Within each source, fallback chain entries are
1546
- * tried in declared order; first existing backend wins. Unknown
1547
- * entries are recorded with `outcome: 'unknown-backend'` and the walk
1548
- * continues.
1549
- *
1550
- * Construction-time validation guarantees every name referenced by
1551
- * `routing` is present in `backends` so the static-config case can
1552
- * never produce a runtime exhaustion throw. The runtime throw at the
1553
- * end of `resolve()` is a safety net for future dynamic-backends
1554
- * scenarios where a chain entry can become unknown post-construction.
2060
+ * `route()` classifies the request, derives the required {@link CapabilityTier}
2061
+ * via `deriveRequiredTier`, picks the cheapest qualifying backend
2062
+ * (`selectCheapestQualifying`), and delegates the actual resolution to
2063
+ * `router.resolveDecisionAndDef` enriching the returned {@link RoutingDecision}
2064
+ * with `complexity`/`tierRequired`/`estCostUsd` (SC9). The shipped router's
2065
+ * identity/default chain still owns the final backend lookup, so when tier
2066
+ * selection abstains (`undefined`) dispatch falls through unchanged.
1555
2067
  */
1556
- declare class BackendRouter {
1557
- private readonly backends;
1558
- private readonly routing;
1559
- private readonly decisionBus;
1560
- constructor(opts: BackendRouterOptions);
2068
+ declare class AdaptiveRouter {
2069
+ private readonly deps;
2070
+ /**
2071
+ * D8 live spend accumulator. Monotonic sum of `estCostUsd` over every decision
2072
+ * this router has made. `route()` reads it (via the `budgetState` default)
2073
+ * BEFORE deriving a tier, so `deriveRequiredTier`'s budget clamp actually fires
2074
+ * as spend accrues — previously `budgetState` was an un-wired `{ spentUsd: 0 }`
2075
+ * stub, so the clamp was dead.
2076
+ *
2077
+ * Semantics (deliberately modest — do NOT read this as a hard ceiling):
2078
+ * - **Soft, lagging cap.** The clamp reads spend accrued from PRIOR dispatches;
2079
+ * a burst of concurrent dispatches all read a stale-low total before any of
2080
+ * them accrues, so a burst can overshoot `capUsd` before the clamp engages.
2081
+ * This is a degrade *signal*, not an admission gate.
2082
+ * - **Single-step degrade.** `deriveRequiredTier` lowers the tier by exactly ONE
2083
+ * step under budget pressure and never below the D5 privacy veto floor — it
2084
+ * does not throttle progressively toward `fast`.
2085
+ * - **Monotonic** (NOT the bounded `projectTelemetry` ring-sum): a long run must
2086
+ * not evict early spend and un-clamp. Preserved across `setPolicy` (instance
2087
+ * persists) — note a *lowered* `capUsd` then clamps immediately and, since the
2088
+ * total never decreases, irreversibly for the rest of the run.
2089
+ * An injected `deps.budgetState` overrides this for the clamp (DI/tests).
2090
+ */
2091
+ private spentUsd;
2092
+ constructor(deps: AdaptiveRouterDeps);
2093
+ /**
2094
+ * Construct an `AdaptiveRouter` from an orchestrator's `agent.backends`
2095
+ * (plus optional LMLM pool). Builds the capability registry via
2096
+ * `buildCapabilityRegistry` and — crucially — derives `providerOf`
2097
+ * UNCONDITIONALLY from `agent.backends` (name → `def.type`).
2098
+ *
2099
+ * Phase-1 finding (capability-registry.ts:60-66): passing an allowlist to
2100
+ * `selectCheapestQualifying` WITHOUT a `providerOf` fail-closes EVERY request
2101
+ * (silent DoS), because every candidate's provider reads back as `undefined`.
2102
+ * Deriving `providerOf` here means enabling the (Phase-5) allowlist branch can
2103
+ * never accidentally deny-all.
2104
+ */
2105
+ static fromConfig(args: {
2106
+ router: BackendRouter;
2107
+ backends: Record<string, BackendDef>;
2108
+ pool?: PoolStateProvider;
2109
+ policy: RoutingPolicy;
2110
+ classify: (req: RoutingRequest) => ComplexityVerdict | Promise<ComplexityVerdict>;
2111
+ budgetState?: () => {
2112
+ spentUsd: number;
2113
+ };
2114
+ /** D10: injected escalation state; defaults to a fresh one seeded from `policy.escalationThreshold`. */
2115
+ escalation?: EscalationState;
2116
+ /** D10: steward-escalation seam bound by the orchestrator to the decision bus/logger. */
2117
+ onExhausted?: (coherenceUnit: string) => void;
2118
+ /** SC9: enriched-decision bus (the same instance dispatch emits base decisions onto). */
2119
+ decisionBus?: RoutingDecisionBus;
2120
+ }): AdaptiveRouter;
2121
+ /**
2122
+ * AMR Phase 5 (D1): hot-swap the routing policy in place. Every policy field
2123
+ * takes effect on the NEXT dispatch EXCEPT `escalationThreshold` (see note) —
2124
+ * the capability registry and `providerOf` are derived from `agent.backends`,
2125
+ * NOT from `policy`, so a policy edit leaves them untouched; `route()` /
2126
+ * `buildConstraints` / `deriveRequiredTier` all read `this.deps.policy` fresh,
2127
+ * so the swapped-in tier matrix / privacy floor / allowlist / budget all apply.
2128
+ * The live {@link EscalationState} is preserved by reference: a unit's climbed
2129
+ * floor survives the swap (a policy edit must not reset accumulated escalation).
2130
+ * The monotonic spend accumulator also persists — so a swap that LOWERS `capUsd`
2131
+ * clamps immediately and irreversibly for the rest of the run (see `spentUsd`).
2132
+ *
2133
+ * Threshold note: the `EscalationState` was seeded with the ORIGINAL policy's
2134
+ * `escalationThreshold` and is intentionally NOT re-seeded here — re-seeding
2135
+ * would conflate "raise the bar" with "reset progress". Preserving climbed
2136
+ * floors is the invariant (SC1); a live threshold change is out of scope (D1).
2137
+ */
2138
+ setPolicy(policy: RoutingPolicy): void;
1561
2139
  /**
1562
- * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
2140
+ * AMR Phase 5 (D2): project the ENRICHED routing decisions in the bus ring
2141
+ * buffer into the Shuttle telemetry wire shape ({@link RoutingTelemetry}).
1563
2142
  *
1564
- * @param useCase the routing query
1565
- * @param opts.invocationOverride if set and the named backend exists,
1566
- * beats all other sources (D7 the `--backend <name>` escape hatch)
2143
+ * Non-destructive this backs the idempotent `GET /api/v1/routing/telemetry`,
2144
+ * so it reads (never clears) the ring; the ring self-evicts FIFO at capacity
2145
+ * and Shuttle dedups by `decisionTs`. `spentUsd` is therefore a sum over the
2146
+ * RETAINED ring (telemetry, not billing-of-record).
2147
+ *
2148
+ * Filters to ENRICHED decisions only — those `AdaptiveRouter.route()` emitted,
2149
+ * carrying `estCostUsd`+`tierRequired`. The SAME bus also carries the BASE emit
2150
+ * from `BackendRouter.resolveDecisionAndDef` (neither field), so an unfiltered
2151
+ * projection would double-count rows and under-fill `spentUsd`. Returns an
2152
+ * empty payload when no bus is wired or no enriched decisions exist.
1567
2153
  */
1568
- resolve(useCase: RoutingUseCase, opts?: {
1569
- invocationOverride?: string;
1570
- }): RoutingDecision;
2154
+ projectTelemetry(): RoutingTelemetry;
1571
2155
  /**
1572
- * Returns the {@link BackendDef} reference for the resolved name.
1573
- * Identity-equal to the entry in `backends` (no copy) so callers
1574
- * relying on reference equality (SC21) continue to work.
2156
+ * D8: the EFFECTIVE spend total the budget clamp reads — the injected
2157
+ * `budgetState` when present, otherwise the router's own monotonic accumulator.
2158
+ * Returning the effective value (not blindly the internal tally) means an
2159
+ * operator reading this always sees the number that actually drove routing.
1575
2160
  */
1576
- resolveDefinition(useCase: RoutingUseCase, opts?: {
1577
- invocationOverride?: string;
1578
- }): BackendDef;
2161
+ getSpentUsd(): number;
1579
2162
  /**
1580
- * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
1581
- * for callers that need both. Replaces the previous pattern of
1582
- * `resolveDefinition(useCase) + resolve(useCase)` which produced two
1583
- * RoutingDecision emissions per dispatch doubling routing-decision
1584
- * log volume now that Phase 4 emits.
1585
- *
1586
- * Identity-equal `BackendDef` (no copy) so callers relying on
1587
- * reference equality (SC21) continue to work.
2163
+ * AMR observability: the live operator status — budget spend-vs-cap (using the
2164
+ * monotonic accumulator that drives the clamp, NOT the telemetry ring sum),
2165
+ * the coherence units that have climbed their escalation floor, and the active
2166
+ * provider allowlist. Called only on a live router, so `active` is always true.
1588
2167
  */
1589
- resolveDecisionAndDef(useCase: RoutingUseCase, opts?: {
1590
- invocationOverride?: string;
1591
- }): {
2168
+ getStatus(): RoutingStatus;
2169
+ route(req: RoutingRequest): Promise<{
1592
2170
  decision: RoutingDecision;
1593
2171
  def: BackendDef;
1594
- };
2172
+ }>;
1595
2173
  /**
1596
- * The pre-Spec-B resolution helper: returns the configured
1597
- * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
1598
- * use cases (or `undefined` for skill/mode use cases, which are owned
1599
- * by the per-skill / per-mode steps in {@link resolve}). Returning
1600
- * `undefined` lets the caller fall through to `routing.default`.
2174
+ * D4 fail-safe: await the (possibly async) classifier; if it rejects or throws,
2175
+ * degrade to a conservative `{ level:'moderate', confidence:'low' }` verdict.
2176
+ * Classification NEVER blocks dispatch a classifier failure/timeout must not
2177
+ * propagate out of `route()`.
1601
2178
  */
1602
- private resolveExistingUseCase;
1603
- private validateReferences;
2179
+ private classifySafe;
2180
+ /**
2181
+ * D10 outcome feedback (mirrors LocalModelResolver.recordSuccess/recordFailure).
2182
+ * Only QUALITY failures are reported here; transport/inference errors go to the
2183
+ * shipped per-model breaker and must NOT reach this path (they never
2184
+ * double-count). A quality failure that re-crosses the threshold while the floor
2185
+ * is already `strong` returns `'exhausted'` from EscalationState, at which point
2186
+ * the injected `onExhausted` seam emits `routing:escalation-exhausted` for
2187
+ * steward escalation. No-op when no EscalationState is injected.
2188
+ */
2189
+ recordOutcome(coherenceUnit: string, tier: CapabilityTier, ok: boolean): void;
2190
+ private selectTarget;
2191
+ /**
2192
+ * Translate the request's declared constraints into {@link SelectConstraints}.
2193
+ * `policy.privacyFloor`, the provider allowlist (AMR Phase 5, D3), and the
2194
+ * per-request capability needs are threaded in. `providerOf` is derived
2195
+ * unconditionally in `fromConfig`, so the allowlist can never silently
2196
+ * fail-close every request (Phase-1 finding, capability-registry.ts:60-66).
2197
+ */
2198
+ private buildConstraints;
1604
2199
  }
1605
2200
 
1606
2201
  /**
@@ -1669,6 +2264,13 @@ declare class Orchestrator extends EventEmitter {
1669
2264
  * construction time. Eliminating this fallback is autopilot Phase 4+.
1670
2265
  */
1671
2266
  private backendFactory;
2267
+ /**
2268
+ * AMR Phase 3 (D11): opt-in adaptive router. Constructed ONLY when
2269
+ * `agent.routing.policy` is present and non-empty; `null` otherwise so
2270
+ * dispatch stays byte-identical on the shipped `BackendRouter`
2271
+ * (SC8/SC17/SC19). Exposed for tests via {@link getAdaptiveRouter}.
2272
+ */
2273
+ private adaptiveRouter;
1672
2274
  /**
1673
2275
  * Spec B Phase 4 (D8): per-orchestrator in-process bus for
1674
2276
  * `RoutingDecision` events. Constructed alongside backendFactory when
@@ -1768,6 +2370,15 @@ declare class Orchestrator extends EventEmitter {
1768
2370
  */
1769
2371
  private localModelStatusUnsubscribes;
1770
2372
  private pipeline;
2373
+ /**
2374
+ * AMR live-classifier provider (final-review finding #2). The complexity
2375
+ * cascade may spend a fast-tier LLM tie-break; it borrows the SEL-layer
2376
+ * AnalysisProvider. Built lazily on first classify (the AdaptiveRouter is
2377
+ * constructed before start(), so this cannot be eager); `null` means "no
2378
+ * provider — cascade stays fully offline / static-only". `undefined` means
2379
+ * "not yet resolved".
2380
+ */
2381
+ private complexityProvider;
1771
2382
  private analysisArchive;
1772
2383
  private graphStore;
1773
2384
  private claimManager;
@@ -1882,6 +2493,19 @@ declare class Orchestrator extends EventEmitter {
1882
2493
  */
1883
2494
  private initMaintenance;
1884
2495
  private createIntelligencePipeline;
2496
+ /**
2497
+ * AMR live-classifier provider resolution (final-review finding #2). The
2498
+ * complexity cascade's OPTIONAL fast-tier tie-break borrows the SEL-layer
2499
+ * AnalysisProvider (the same one intelligence enrichment uses). Resolved and
2500
+ * memoized on first classify because the AdaptiveRouter is constructed BEFORE
2501
+ * start(), so the provider cannot be resolved eagerly.
2502
+ *
2503
+ * Returns `undefined` when no provider is available (intelligence disabled, no
2504
+ * backendFactory, or the layer resolves to nothing) — the cascade then stays
2505
+ * fully offline and returns the static verdict (never throws). A build failure
2506
+ * degrades the same way: static-only, never blocks dispatch (D4).
2507
+ */
2508
+ private resolveComplexityProvider;
1885
2509
  /**
1886
2510
  * Lazily initializes the ClaimManager if it hasn't been created yet.
1887
2511
  * Called from both start() and asyncTick() to avoid duplicating the init block.
@@ -1968,10 +2592,136 @@ declare class Orchestrator extends EventEmitter {
1968
2592
  private processAgentEvent;
1969
2593
  private awaitRateLimitClearance;
1970
2594
  private runAgentInBackgroundTask;
2595
+ /**
2596
+ * AMR 4c (ADR 0069): the sound single-agent quality-verdict feeder. On a normal
2597
+ * exit, when AMR is active, run a BASELINE-RELATIVE security scan of the lines
2598
+ * the agent INTRODUCED (only added lines — a pre-existing pattern never counts);
2599
+ * a NEW error-severity finding → `'quality-fail'`, which climbs the coherence
2600
+ * unit's escalation floor. Success stays `neutral` (returns `undefined`) — a
2601
+ * premature `'quality-pass'` would clear accumulating failures (ADR 0069).
2602
+ *
2603
+ * Fully guarded: any error (git/scan/parse) → `undefined` → `neutral`, so this
2604
+ * NEVER breaks completion. No-op (zero cost) when AMR is off, keeping the
2605
+ * dispatch path byte-identical. Staged workflows use their own per-stage gate
2606
+ * feeder; this is the single-agent equivalent.
2607
+ */
2608
+ private deriveSingleAgentQualityVerdict;
2609
+ /**
2610
+ * AMR 4c v2: opt-in LLM spec-satisfaction verdict. Gated on
2611
+ * `routing.policy.acceptanceEval.enabled` (HEAVY — one model call + latency per
2612
+ * exit, so separate from the always-on cheap security scan), a present spec, and
2613
+ * an available analysis provider. Runs the shared `OutcomeEvaluator` over the
2614
+ * introduced diff vs the spec's judgment section, reusing the SEL-layer provider
2615
+ * the live classifier already builds; a BLOCKING verdict (high-confidence
2616
+ * NOT_SATISFIED) → `'quality-fail'`. Conservative + fully guarded: no
2617
+ * spec / no provider / empty diff / any error → `undefined` (neutral). The mapper
2618
+ * only ever emits the negative, so success can never become a premature
2619
+ * `'quality-pass'`. An absent GraphStore falls back to an ephemeral one — the
2620
+ * evaluator's `execution_outcome` persistence is best-effort and never blocks.
2621
+ */
2622
+ private deriveAcceptanceEvalVerdict;
1971
2623
  /**
1972
2624
  * Informs the state machine that an agent worker has exited.
1973
2625
  */
1974
2626
  private emitWorkerExit;
2627
+ /**
2628
+ * AMR Phase 4 (D10/SC16): feed a dispatch outcome into vertical escalation.
2629
+ * No-op unless an AdaptiveRouter is live (policy present) AND this dispatch was
2630
+ * AMR-routed (a `lastRoutedTier` was captured). Transport outcomes never reach
2631
+ * `recordOutcome` — the shipped per-model breaker owns those, so the two signals
2632
+ * never double-count. The coherence unit is the issue id (D6 issue-grain pinning).
2633
+ */
2634
+ private recordAmrOutcome;
2635
+ /**
2636
+ * AMR steward-escalation seam (D10, findings item 1 + 2). Queues a `needs-human`
2637
+ * interaction for a coherence unit whose routing hard-failed — either the vertical
2638
+ * escalation exhausted the `strong` ceiling (`escalation-exhausted`) or the
2639
+ * fail-closed selector left no compliant backend (`privacy-no-match`). Both ride
2640
+ * the SAME `needs-human` mechanism as every other escalation. The `RoutingError`
2641
+ * code disambiguates the two on the steward's channel. The coherence unit is the
2642
+ * issue id (D6 issue-grain pinning); title/description are recovered from running
2643
+ * state when still present. Fire-and-forget + `.catch` — a queue write must never
2644
+ * block or throw out of the dispatch/outcome path.
2645
+ */
2646
+ private escalateRoutingToHuman;
2647
+ /**
2648
+ * AMR dispatch-boundary routing-failure handler (finding #3 + live-wiring
2649
+ * review blocker). When `AdaptiveRouter.route()` throws a fail-closed
2650
+ * `PrivacyNoMatch` (`RoutingError` code `'privacy-no-match'`) at dispatch, that
2651
+ * distinct signal MUST NOT be swallowed by the generic transport/dispatch-error
2652
+ * path (S4-001): it is not a runner/transport failure, so it must never be
2653
+ * recorded as one or feed the vertical escalation breaker. Instead it emits a
2654
+ * DISTINCT `routing:no-tier-match` steward escalation (needs-human, same
2655
+ * mechanism as `onExhausted`) carrying the coherence unit + reason.
2656
+ *
2657
+ * It is ALSO deterministic — the `privacyFloor`/allowlist that emptied the
2658
+ * candidate set is config-driven, so re-dispatch would throw the SAME
2659
+ * `PrivacyNoMatch`. Therefore this path is TERMINAL: it drives the unit to the
2660
+ * `canceled` lane and removes it from `running`/`claimed` directly, rather than
2661
+ * routing through `emitWorkerExit('error')` (whose state-machine error branch
2662
+ * enqueues a retry whenever the retry budget is not yet exhausted — which would
2663
+ * re-dispatch, re-fail closed, and re-escalate up to `maxRetries` times). No
2664
+ * retry is scheduled, no transport outcome is recorded, and exactly one
2665
+ * needs-human escalation is queued. Fail-closed is preserved — `route()` already
2666
+ * refused to pick a non-compliant backend, and returning `true` here stops the
2667
+ * caller from falling through to any further routing.
2668
+ *
2669
+ * Returns `true` when the boundary CLAIMED the error (`privacy-no-match` or the
2670
+ * hard-cap `budget-exhausted`), so the caller returns without ANY
2671
+ * `emitWorkerExit`. Returns `false` for any other error (including
2672
+ * `escalation-exhausted`, which the `onExhausted` seam owns) so the generic
2673
+ * dispatch-error path runs unchanged.
2674
+ */
2675
+ private handleRoutingFailure;
2676
+ /**
2677
+ * AMR live-wiring review blocker: terminally retire a unit whose dispatch failed
2678
+ * closed (`privacy-no-match`). Mirrors the terminal side of a worker exit —
2679
+ * remove the unit from `running` and release its `claimed` slot — then persist
2680
+ * the terminal `canceled` lane (`abandon`), matching how retry-exhausted
2681
+ * escalations settle. Crucially it does NOT run the state-machine `worker_exit`
2682
+ * reducer, so no `scheduleRetry` effect is emitted. Best-effort lane persistence
2683
+ * (`persistLaneSafe` never throws). No transport/escalation outcome is recorded —
2684
+ * that stays the sole job of the single `routing:no-tier-match` escalation already
2685
+ * queued by `escalateRoutingToHuman`.
2686
+ */
2687
+ private finalizeRoutingTerminal;
2688
+ /**
2689
+ * split-routing Phase 4 (D6/SC5) — terminal SUCCESS settle for a workflow unit.
2690
+ * The real `WorkflowEngineContext.emitWorkflowSuccess` forwards here (bound via
2691
+ * the context's `settleSuccess` dep in `dispatchIssue`). It reproduces the
2692
+ * `worker_exit`/`reason==='normal'` reducer BY HAND (state-machine.ts:457,467-474):
2693
+ * `running.delete` → `completed.set(now)` → `claimed.delete` → `cleanWorkspace`
2694
+ * effect → then persists the terminal `success` lane and emits one state change.
2695
+ *
2696
+ * It deliberately does NOT route through `emitWorkerExit`/`handleWorkerExit`
2697
+ * (completion/handler.ts): that fires the ISSUE-keyed `finishRecording(issueId,
2698
+ * attempt)` + `recordAmrOutcome`, but the engine already ran PER-STAGE recorders
2699
+ * (`stageAttemptKey(index, attempt)`) and per-stage `recordOutcome`. Going through
2700
+ * the worker-exit path would (a) finish a recording never started at the
2701
+ * issue-attempt key and (b) double-feed the escalation state. This is the ONE
2702
+ * hand-reproduced reducer sequence in Phase 4; the `worker_exit` reducer itself
2703
+ * stays untouched (Task 12 pins it) so the two remain in sync.
2704
+ *
2705
+ * `runs` are the per-stage records (best-effort telemetry; the per-stage cost is
2706
+ * already attributable via the recorders). Never throws — a success settle must
2707
+ * complete the single terminal transition (D6).
2708
+ */
2709
+ private settleWorkflowSuccess;
2710
+ /**
2711
+ * split-routing Phase 4 (D6/I1/SC5) — terminal FAILURE/safety-net settle for a
2712
+ * workflow unit. The real context's `finalizeWorkflowTerminal` forwards here
2713
+ * (bound via `settleTerminal`). Composed from the `finalizeRoutingTerminal`
2714
+ * pattern (`running.delete` + `claimed.delete` + `persistLaneSafe('abandon')`,
2715
+ * orchestrator.ts:2388-2394) PLUS a single `needs-human` escalation
2716
+ * (escalateRoutingToHuman-style queue push, :2301-2316) PLUS `cleanWorkspace`
2717
+ * (S5). It must NEVER rethrow — the engine's `catch` calls it on the I1 safety
2718
+ * net, so a throw here would defeat the single-exit guarantee.
2719
+ *
2720
+ * It is NOT a verbatim `finalizeRoutingTerminal` call (that lacks the needs-human
2721
+ * + cleanWorkspace the Phase-3 terminal contract pinned). Exactly one needs-human
2722
+ * per terminal transition.
2723
+ */
2724
+ private settleWorkflowTerminal;
1975
2725
  /**
1976
2726
  * Hermes Phase 3: wire in-process notification sinks against the
1977
2727
  * orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
@@ -2074,6 +2824,51 @@ declare class Orchestrator extends EventEmitter {
2074
2824
  * single-backend config bypassed agent.backends synthesis.
2075
2825
  */
2076
2826
  getRoutingDecisionBus(): RoutingDecisionBus | null;
2827
+ /**
2828
+ * AMR Phase 3 (D11): the opt-in adaptive router, or `null` when no
2829
+ * `routing.policy` is configured (the default-off path). Exposed for the
2830
+ * SC8/SC17/SC19 default-off proof: `null` here means dispatch stays on the
2831
+ * shipped `BackendRouter`, byte-identical, with no classify()/telemetry.
2832
+ */
2833
+ getAdaptiveRouter(): AdaptiveRouter | null;
2834
+ /**
2835
+ * AMR Phase 3 (D11) / Phase 5 (D1): construct the opt-in AdaptiveRouter for a
2836
+ * policy. Extracted from the constructor so runtime ingestion
2837
+ * (`ingestRoutingPolicy`) builds a router IDENTICAL to the constructor's —
2838
+ * same live classify seam, strong-cap escalation-exhaustion hard-fail-to-human
2839
+ * (D10), and enriched-decision bus (SC9). Precondition: the routing subsystem
2840
+ * exists (`backendFactory` + `agent.backends` present); callers guard.
2841
+ */
2842
+ private buildAdaptiveRouter;
2843
+ /**
2844
+ * AMR Phase 5 (D1/D5): ingest a routing policy pushed at runtime by the
2845
+ * Shuttle control plane (`PUT /api/v1/routing/policy`). Hot-swaps the live
2846
+ * router:
2847
+ * - empty policy (`{}` / no activating fields) → `adaptiveRouter = null`
2848
+ * (default-off restored, D5 — byte-identical dispatch resumes);
2849
+ * - an existing router → `setPolicy` (preserves the accumulated
2850
+ * `EscalationState` climbed floors — a policy edit must not reset them);
2851
+ * - no router yet → construct one from the pushed policy.
2852
+ *
2853
+ * The field-swap is atomic between `await`s (single-threaded): a dispatch that
2854
+ * already captured the router finishes on it; the next dispatch sees the new
2855
+ * policy. No-op-safe when the routing subsystem is absent (`backendFactory`
2856
+ * null) — the caller (`PUT` handler) reports 503 in that case, so this path is
2857
+ * reached only when routing is available.
2858
+ */
2859
+ ingestRoutingPolicy(policy: RoutingPolicy): void;
2860
+ /**
2861
+ * AMR Phase 5 (D2): project the live router's enriched decision ring into the
2862
+ * Shuttle telemetry wire shape (`GET /api/v1/routing/telemetry`). Returns an
2863
+ * empty payload when routing is off (no router) — a safe, idempotent read.
2864
+ */
2865
+ getRoutingTelemetry(): RoutingTelemetry;
2866
+ /**
2867
+ * AMR observability: the live operator status (budget spend-vs-cap, escalated
2868
+ * units, allowlist), or an inactive payload when AMR is off. Backs
2869
+ * `GET /api/v1/routing/status`.
2870
+ */
2871
+ getRoutingStatus(): RoutingStatus;
2077
2872
  /**
2078
2873
  * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
2079
2874
  * dispatch path uses the factory-owned router directly; observability
@@ -2112,122 +2907,70 @@ declare function launchTUI(orchestrator: Orchestrator): {
2112
2907
  };
2113
2908
 
2114
2909
  /**
2115
- * Options for `OrchestratorBackendFactory`.
2910
+ * Fail-closed signal: privacy floor / allowlist emptied the candidate set (S4-001).
2911
+ * Distinguishable from a tier/cost-only exclusion, which returns `undefined`.
2116
2912
  *
2117
- * `sandboxPolicy` and `container`/`secrets` mirror the orchestrator's own
2118
- * agent-config fields. `getResolverModelFor` is a registration hook the
2119
- * orchestrator calls to bind each `local`/`pi` backend to its
2120
- * `LocalModelResolver` (so multi-resolver array-fallback works without
2121
- * leaking resolver lifetimes into the factory).
2122
- */
2123
- /**
2124
- * Runtime-feedback callbacks a `local`/`pi` backend fires as turns complete.
2125
- * See {@link OrchestratorBackendFactoryOptions.getModelUsageHooksFor}.
2913
+ * Finding #4 — ONE typed error family: `PrivacyNoMatch` stays orchestrator-local
2914
+ * (it is thrown deep in the selector, a layer below the exported types package) but
2915
+ * EXTENDS the exported `RoutingError`, carrying its canonical `'privacy-no-match'`
2916
+ * code. The dispatch boundary can therefore catch it as either `PrivacyNoMatch`
2917
+ * (ergonomic `instanceof`) or `RoutingError` (code-narrowed), and the sibling
2918
+ * `escalation-exhausted` code is emitted via `RoutingError` too — no divergent
2919
+ * surfaces, no dead exported error type. `code` narrows to the literal here.
2126
2920
  */
2127
- interface LocalModelUsageHooks {
2128
- /** Fired with the resolved model after a successful turn (LRU + breaker clear). */
2129
- onModelUsed?: (model: string) => void;
2130
- /** Fired with the resolved model after a failed turn (breaker increment). */
2131
- onModelFailed?: (model: string) => void;
2921
+ declare class PrivacyNoMatch extends RoutingError {
2922
+ readonly code: "privacy-no-match";
2923
+ constructor(message: string);
2132
2924
  }
2133
- interface OrchestratorBackendFactoryOptions {
2134
- backends: Record<string, BackendDef>;
2135
- routing: RoutingConfig;
2136
- sandboxPolicy: 'none' | 'docker';
2137
- container?: ContainerConfig;
2138
- secrets?: SecretConfig;
2139
- /**
2140
- * Hook for resolver injection. Invoked per `local`/`pi` backend at
2141
- * `forUseCase()` time with the backend's name. When the hook returns a
2142
- * function, the factory rebuilds the local/pi instance using that
2143
- * function as `getModel` (overriding the head-of-array placeholder
2144
- * baked into `createBackend`). Returning `undefined` means "no
2145
- * resolver registered for this name" — the placeholder stays in place.
2146
- *
2147
- * This indirection keeps the factory ignorant of `LocalModelResolver`'s
2148
- * existence and lifecycle while still letting it produce backends that
2149
- * route through the resolver Map.
2150
- */
2151
- getResolverModelFor?: (backendName: string, useCase: RoutingUseCase) => (() => string | null) | undefined;
2152
- /**
2153
- * Consumption Phase 3 (T11): per-`local`/`pi` backend hook returning the
2154
- * runtime-feedback callbacks bound to that backend's resolver + pool. The
2155
- * factory forwards them to the constructed `LocalBackend` so a completed turn
2156
- * stamps `lastUsedAt` (LRU) + clears the circuit breaker, and a failed turn
2157
- * feeds the breaker. Returning `undefined` means "no feedback wiring" (the
2158
- * backend runs exactly as before). Kept parallel to `getResolverModelFor` so
2159
- * the factory stays ignorant of resolver/pool lifecycles.
2160
- */
2161
- getModelUsageHooksFor?: (backendName: string) => LocalModelUsageHooks | undefined;
2162
- /**
2163
- * Phase 5: prompt-cache recorder forwarded to Anthropic-capable backends.
2164
- * Other backends accept-but-ignore. Shared across dispatches so the
2165
- * `/api/v1/telemetry/cache/stats` endpoint sees the full rolling window.
2166
- */
2167
- cacheMetrics?: CacheMetricsRecorder;
2168
- /**
2169
- * Spec B Phase 4 (D8): forwarded to the underlying BackendRouter so
2170
- * every resolve() during forUseCase / resolveName emits.
2171
- */
2172
- decisionBus?: RoutingDecisionBus;
2925
+ interface SelectConstraints {
2926
+ privacyFloor?: PrivacyClass;
2927
+ /** Present ⇒ only these providers allowed. Absent ⇒ all allowed. Empty array ⇒ none. */
2928
+ allowed?: BackendDef['type'][];
2929
+ needsVision?: boolean;
2930
+ needsToolUse?: boolean;
2931
+ minContextTokens?: number;
2173
2932
  }
2174
2933
  /**
2175
- * High-level factory wrapping `BackendRouter` + `createBackend` plus
2176
- * orchestrator-side concerns (sandbox wrapping, resolver binding).
2934
+ * D1 core: filter the registry to backends with tier ≥ requiredTier, privacyClass
2935
+ * at least as strong as the floor, provider in the allowlist, and capabilities ⊇
2936
+ * required (vision/toolUse/minContextTokens); sort by costPer1kTokens ascending;
2937
+ * return the cheapest.
2177
2938
  *
2178
- * Spec 2 SC22-SC25: every `forUseCase(useCase)` call returns a fresh
2179
- * `AgentBackend` whose class matches the routed `BackendDef.type`.
2180
- * `local`/`pi` defs are bound to their per-name resolver before being
2181
- * returned, and the result is wrapped in `ContainerBackend` when
2182
- * sandboxPolicy is 'docker'.
2939
+ * Fail-closed (S4-001): if a privacy-floor OR allowlist constraint empties the set,
2940
+ * throw PrivacyNoMatch (the item must surface to the steward — never fall through
2941
+ * to identity routing at a non-compliant backend). A tier/cost-only exclusion is
2942
+ * best-effort: return undefined so the caller can fall back to the shipped router's
2943
+ * identity/default chain. No `if (local)` anywhere.
2183
2944
  */
2184
- declare class OrchestratorBackendFactory {
2185
- private readonly router;
2186
- private readonly opts;
2187
- constructor(opts: OrchestratorBackendFactoryOptions);
2188
- /**
2189
- * Resolve `useCase` to a backend name, materialize a fresh
2190
- * `AgentBackend`, optionally rebind its model resolver, and apply
2191
- * sandbox wrapping. Idempotent across calls (no caching) the AgentRunner
2192
- * holds the per-dispatch reference and discards it when the run ends.
2193
- */
2194
- /**
2195
- * Resolve `useCase` to its routed backend name, exposing the
2196
- * router lookup without materializing a backend. Used by callers
2197
- * (e.g., the orchestrator's dispatch site) that need to label
2198
- * telemetry with the routed name BEFORE constructing the backend.
2199
- *
2200
- * Spec 2 P2-I2: previously the orchestrator labelled `LiveSession`
2201
- * + `StreamRecorder` with the legacy `agent.backend` field, which
2202
- * is `undefined` for pure-modern configs. Threading the routed name
2203
- * through dispatch eliminates that gap.
2204
- */
2205
- resolveName(useCase: RoutingUseCase, opts?: {
2206
- invocationOverride?: string;
2207
- }): string;
2208
- /**
2209
- * Spec B Phase 1: expose the underlying router for callers that need
2210
- * it directly (e.g., {@link buildIntelligencePipeline} for the
2211
- * I1 SEL/PESL comparison fix). Read-only access; consumers must not
2212
- * mutate router state.
2213
- */
2214
- getRouter(): BackendRouter;
2215
- forUseCase(useCase: RoutingUseCase, opts?: {
2216
- invocationOverride?: string;
2217
- }): AgentBackend;
2218
- /**
2219
- * Rebuild a `local`/`pi` backend with a resolver-bound `getModel`,
2220
- * mirroring `createBackend`'s local/pi branches but substituting the
2221
- * head-of-array placeholder with the orchestrator-owned resolver.
2222
- */
2223
- private buildLocalLikeWithResolver;
2224
- /**
2225
- * Apply ContainerBackend wrapping (PFC-3). Pulls the runtime + secret
2226
- * backend per call so each dispatch sees a fresh container handle map
2227
- * (ContainerBackend keeps its own per-instance Map<sessionId, handle>).
2228
- */
2229
- private wrapInContainer;
2230
- }
2945
+ declare function selectCheapestQualifying(registry: BackendCapabilityRegistry, requiredTier: CapabilityTier, constraints: SelectConstraints,
2946
+ /**
2947
+ * Provider lookup by name, required to enforce a `constraints.allowed` allowlist.
2948
+ * If `allowed` is set but `providerOf` is absent (or returns `undefined` for an
2949
+ * entry), that entry cannot be admitted → excluded; an allowlist with no
2950
+ * `providerOf` therefore excludes ALL candidates and fails closed
2951
+ * (`PrivacyNoMatch`). Phase 3 (`AdaptiveRouter`) MUST derive this from
2952
+ * `agent.backends` whenever it sets an allowlist. Absent `allowed` ⇒ unused.
2953
+ */
2954
+ providerOf?: (name: string) => BackendDef['type'] | undefined): {
2955
+ name: string;
2956
+ capabilities: BackendCapabilities;
2957
+ } | undefined;
2958
+ /** Default capability block derived for an LMLM pool candidate that carries no
2959
+ * explicit capabilities. On-device strongest privacy, zero marginal cost.
2960
+ * Seed values (tunable in later phases); a candidate is thus visible to tier
2961
+ * selection (spec "Failure modes": a backend with NO capabilities is invisible,
2962
+ * but a pool candidate is always given a derived block so it can win on cost). */
2963
+ declare function defaultPoolCapabilities(): BackendCapabilities;
2964
+ /**
2965
+ * Build the tier-selection registry (name → capabilities) from configured
2966
+ * `agent.backends` (their `capabilities` blocks, when present) merged with LMLM
2967
+ * pool candidates (each given a derived on-device/zero-cost block). A configured
2968
+ * backend WITHOUT a `capabilities` block is omitted — invisible to tier selection,
2969
+ * reachable only via identity routing (spec "Failure modes"). No LMLM code changes.
2970
+ */
2971
+ declare function buildCapabilityRegistry(backends: Record<string, BackendDef>, pool?: PoolStateProvider): BackendCapabilityRegistry;
2972
+
2973
+ declare function estimateCost(def: BackendDef, _req: RoutingRequest): number;
2231
2974
 
2232
2975
  /**
2233
2976
  * Result of running `migrateAgentConfig`.
@@ -3792,4 +4535,4 @@ declare function emitProposalCreated(bus: EventEmitter, proposal: SkillProposal)
3792
4535
  declare function emitProposalApproved(bus: EventEmitter, proposal: SkillProposal): void;
3793
4536
  declare function emitProposalRejected(bus: EventEmitter, proposal: SkillProposal): void;
3794
4537
 
3795
- export { type AgentDispatchResult, type AgentDispatcher, type AgentDispatcherDeps, type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, type BackendResolver, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, type CheckCommandResult, type CheckCommandRunner, type CheckFailureClassification, type CheckFailureKind, CheckScriptRunner, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CommandExecResult, type CommandExecutor, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, type ExecFileAsyncFn, type ExecFileError, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type HarnessSpawn, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, LinearGraphQLClient, type LinearGraphQLClientOptions, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAINTENANCE_CHECK_MAX_BUFFER, MAINTENANCE_CHECK_TIMEOUT_MS, MAX_ATTEMPTS, MaintenanceReporter, type MaintenanceReporterOptions, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PRLifecycleManager, type PendingInteraction, type PersistedOutputEntry, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunHarnessCheckOptions, type RunMode, type RunOrigin, type RunResult, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, TaskRunner, type TaskRunnerOptions, type TaskSelectionFilter, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, calculateRetryDelay, canDispatch, classifyCheckExecutionFailure, computeRateLimitDelay, createAgentDispatcher, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, explicitFindingsCount, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isCheckTimeoutError, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, makeBackendResolver, migrateAgentConfig, normalizeFts5Query, normalizeHarnessCommand, normalizeLocalModel, openSearchIndex, promote, reconcile, recoverFindingsCount, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, runHarnessCheck, savePublishedIndex, searchIndexPath, selectCandidates, selectTasks, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, wrapAsEnvelope };
4538
+ export { AdaptiveRouter, type AdaptiveRouterDeps, type AgentDispatchResult, type AgentDispatcher, type AgentDispatcherDeps, type AgentUpdateEvent, AnalysisArchive, type AnalysisRecord, type ApplyEventResult, type ArtifactPresence, type AttemptStats, BUILT_IN_TASKS, BackendDefSchema, type BackendResolver, BackendRouter, type BackendRouterOptions, type BaseRefFallbackEvent, type BuildArchiveHooksOptions, type BuildWorkflowContextDeps, type CheckCommandResult, type CheckCommandRunner, type CheckFailureClassification, type CheckFailureKind, CheckScriptRunner, ClaimManager, type ClaimManagerConfig, type CleanWorkspaceEffect, type CommandExecResult, type CommandExecutor, type CreateTokenInput, type CreateTokenResult, type CustomTaskValidationError, type DispatchEffect, type EmitLogEffect, type EscalateEffect, EscalationState, type ExecFileAsyncFn, type ExecFileError, type ExecFileFn$1 as ExecFileFn, type FromConfigOptions, GateNotReadyError, type GateResult, GateRunError, type HarnessSpawn, type Highlight, type HighlightsInfo, type IndexedDoc, InteractionQueue, LinearGraphQLClient, type LinearGraphQLClientOptions, type LinearGraphQLExtension, LinearGraphQLStub, type LiveSession, LocalModelResolver, type LocalModelResolverOptions, MAINTENANCE_CHECK_MAX_BUFFER, MAINTENANCE_CHECK_TIMEOUT_MS, MAX_ATTEMPTS, MaintenanceReporter, type MaintenanceReporterOptions, type MigrationResult, MockBackend, type NotificationSink, type NotificationSinkDeliverInput, ORCHESTRATOR_IDENTITY_FILE, Orchestrator, OrchestratorBackendFactory, type OrchestratorBackendFactoryOptions, type OrchestratorContext, type OrchestratorEvent, type OrchestratorState, PRDetector, type PRDetectorLogger, type PRLifecycleManager, type PendingInteraction, type PersistedOutputEntry, PrivacyNoMatch, PromotionError, type PromotionResult, PromptRenderer, type ProposalApprovedData, type ProposalCreatedData, type ProposalRejectedData, type PublishedIndex, type QueueInsertInput, type QueueRow, type QueueStats, RETRY_DELAYS_MS, type RateLimitSnapshot as RateLimitComputeSnapshot, type RateLimitConfig, type RateLimitSnapshot$1 as RateLimitSnapshot, type RegistryEntry, type ReleaseClaimEffect, type ResolverLogger, type RetryEntry, type RetryFiredEvent, RoadmapTrackerAdapter, RoutingConfigSchema, RoutingValueSchema, type RunAttemptPhase, type RunHarnessCheckOptions, type RunMode, type RunOrigin, type RunResult, type RunningEntry, type ScheduleRetryEffect, type SearchOptions, type SelectConstraints, type SideEffect, SinkConfigError, SinkRegistry, type SkillCatalogEntry, SlackSink, type SlackSinkOptions, SqliteSearchIndex, type StallDetectedEvent, type StopEffect, type StreamManifest, StreamRecorder, type SummarizeContext, type SummarizeResult, type SyncMainOptions, type SyncMainResult, type SyncSkipReason, type TaskDefinition, TaskOutputStore, TaskRunner, type TaskRunnerOptions, type TaskSelectionFilter, type TaskType, type TickEvent, TokenStore, type TokenTotals, type TriageConfig, type TriageDecision, type TriageSignals, type TriageSkill, type UpdateTokensEffect, type ValidateWorkflowConfigOptions, type ValidatedWorkflowConfig, WebhookQueue, type WorkerExitEvent, WorkflowLoader, type WorkflowRouterDep, WorkspaceHooks, WorkspaceManager, type WorkspaceManagerOptions, applyEvent, artifactPresenceFromIssue, buildArchiveHooks, buildCapabilityRegistry, buildWorkflowContext, calculateRetryDelay, canDispatch, classifyCheckExecutionFailure, computeRateLimitDelay, createAgentDispatcher, createBackend, createEmptyState, crossFieldRoutingIssues, defaultFetchModels, defaultPoolCapabilities, deriveSeedPaths, detectScopeTier, discoverSkillCatalog, discoverSkillCatalogNames, emitProposalApproved, emitProposalCreated, emitProposalRejected, estimateCost, explicitFindingsCount, extractHighlights, extractTitlePrefix, getAvailableSlots, getDefaultConfig, getPerStateCount, indexSessionDirectory, isCheckTimeoutError, isEligible, isSummaryEnabled, launchTUI, loadPublishedIndex, makeBackendResolver, migrateAgentConfig, normalizeFts5Query, normalizeHarnessCommand, normalizeLocalModel, openSearchIndex, promote, reconcile, recoverFindingsCount, reindexFromArchive, renderAnalysisComment, renderLlmSummaryMarkdown, renderPRComment, resolveEscalationConfig, resolveOrchestratorId, routeIssue, routingWarnings, runGate, runHarnessCheck, savePublishedIndex, searchIndexPath, selectCandidates, selectCheapestQualifying, selectTasks, sortCandidates, summarizeArchivedSession, syncMain, triageIssue, truncateForBudget, validateCustomTasks, validateWorkflowConfig, wireNotificationSinks, workflowFor, wrapAsEnvelope };