@harness-engineering/orchestrator 0.6.0 → 0.7.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.mjs CHANGED
@@ -27,7 +27,7 @@ function sortCandidates(issues) {
27
27
  return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
28
28
  });
29
29
  }
30
- function isEligible(issue, state, activeStates, terminalStates) {
30
+ function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
31
31
  if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
32
32
  return false;
33
33
  }
@@ -49,6 +49,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
49
49
  if (state.completed.has(issue.id)) {
50
50
  return false;
51
51
  }
52
+ if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
53
+ return false;
54
+ }
52
55
  if (normalizedState === "todo" && issue.blockedBy.length > 0) {
53
56
  const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
54
57
  if (blocker.state === null) return true;
@@ -60,9 +63,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
60
63
  }
61
64
  return true;
62
65
  }
63
- function selectCandidates(issues, state, activeStates, terminalStates) {
66
+ function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
64
67
  const sorted = sortCandidates(issues);
65
- return sorted.filter((issue) => isEligible(issue, state, activeStates, terminalStates));
68
+ return sorted.filter(
69
+ (issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
70
+ );
66
71
  }
67
72
 
68
73
  // src/core/concurrency.ts
@@ -658,7 +663,8 @@ function handleTick(state, event, config) {
658
663
  candidates,
659
664
  next,
660
665
  config.tracker.activeStates,
661
- config.tracker.terminalStates
666
+ config.tracker.terminalStates,
667
+ event.selfAssignee
662
668
  );
663
669
  const escalationConfig = resolveEscalationConfig(config);
664
670
  for (const issue of eligible) {
@@ -1144,7 +1150,7 @@ var ClaimManager = class {
1144
1150
  const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
1145
1151
  if (!claimResult.ok) return claimResult;
1146
1152
  if (this.verifyDelayMs > 0) {
1147
- await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
1153
+ await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
1148
1154
  }
1149
1155
  const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
1150
1156
  if (!statesResult.ok) return statesResult;
@@ -1796,7 +1802,8 @@ function formatFilesList(files) {
1796
1802
  }
1797
1803
 
1798
1804
  // src/workflow/loader.ts
1799
- import * as fs6 from "fs/promises";
1805
+ import * as fs7 from "fs/promises";
1806
+ import * as path7 from "path";
1800
1807
  import { parse } from "yaml";
1801
1808
  import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
1802
1809
 
@@ -1804,7 +1811,8 @@ import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
1804
1811
  import { z as z2 } from "zod";
1805
1812
  import {
1806
1813
  Ok as Ok2,
1807
- Err
1814
+ Err,
1815
+ STANDARD_COGNITIVE_MODES
1808
1816
  } from "@harness-engineering/types";
1809
1817
 
1810
1818
  // src/workflow/schema.ts
@@ -1852,16 +1860,29 @@ var BackendDefSchema = z.discriminatedUnion("type", [
1852
1860
  probeIntervalMs: z.number().int().min(1e3).optional()
1853
1861
  }).strict()
1854
1862
  ]);
1863
+ var RoutingValueSchema = z.union([
1864
+ z.string().min(1),
1865
+ z.array(z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
1866
+ ]);
1855
1867
  var RoutingConfigSchema = z.object({
1856
- default: z.string().min(1),
1857
- "quick-fix": z.string().optional(),
1858
- "guided-change": z.string().optional(),
1859
- "full-exploration": z.string().optional(),
1860
- diagnostic: z.string().optional(),
1868
+ default: RoutingValueSchema,
1869
+ "quick-fix": RoutingValueSchema.optional(),
1870
+ "guided-change": RoutingValueSchema.optional(),
1871
+ "full-exploration": RoutingValueSchema.optional(),
1872
+ diagnostic: RoutingValueSchema.optional(),
1861
1873
  intelligence: z.object({
1862
- sel: z.string().optional(),
1863
- pesl: z.string().optional()
1864
- }).strict().optional()
1874
+ sel: RoutingValueSchema.optional(),
1875
+ pesl: RoutingValueSchema.optional()
1876
+ }).strict().optional(),
1877
+ // --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
1878
+ isolation: z.object({
1879
+ none: RoutingValueSchema.optional(),
1880
+ container: RoutingValueSchema.optional(),
1881
+ "remote-sandbox": RoutingValueSchema.optional()
1882
+ }).strict().optional(),
1883
+ // --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
1884
+ skills: z.record(z.string().min(1), RoutingValueSchema).optional(),
1885
+ modes: z.record(z.string().min(1), RoutingValueSchema).optional()
1865
1886
  }).strict();
1866
1887
 
1867
1888
  // src/workflow/config.ts
@@ -1870,13 +1891,17 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
1870
1891
  function crossFieldRoutingIssues(backends, routing) {
1871
1892
  const issues = [];
1872
1893
  const names = new Set(Object.keys(backends));
1873
- const checkRef = (path22, name) => {
1874
- if (name !== void 0 && !names.has(name)) {
1894
+ const checkRef = (path24, value) => {
1895
+ if (value === void 0) return;
1896
+ const entries = Array.isArray(value) ? value : [value];
1897
+ entries.forEach((name, idx) => {
1898
+ if (names.has(name)) return;
1899
+ const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
1875
1900
  issues.push({
1876
- path: path22,
1877
- message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1901
+ path: pathWithIdx,
1902
+ message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1878
1903
  });
1879
- }
1904
+ });
1880
1905
  };
1881
1906
  checkRef(["default"], routing.default);
1882
1907
  checkRef(["quick-fix"], routing["quick-fix"]);
@@ -1885,9 +1910,44 @@ function crossFieldRoutingIssues(backends, routing) {
1885
1910
  checkRef(["diagnostic"], routing.diagnostic);
1886
1911
  checkRef(["intelligence", "sel"], routing.intelligence?.sel);
1887
1912
  checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
1913
+ checkRef(["isolation", "none"], routing.isolation?.none);
1914
+ checkRef(["isolation", "container"], routing.isolation?.container);
1915
+ checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
1916
+ if (routing.skills) {
1917
+ for (const [skill, value] of Object.entries(routing.skills)) {
1918
+ checkRef(["skills", skill], value);
1919
+ }
1920
+ }
1921
+ if (routing.modes) {
1922
+ for (const [mode, value] of Object.entries(routing.modes)) {
1923
+ checkRef(["modes", mode], value);
1924
+ }
1925
+ }
1888
1926
  return issues;
1889
1927
  }
1890
- function validateWorkflowConfig(config) {
1928
+ function routingWarnings(routing, knownSkillNames) {
1929
+ const warnings = [];
1930
+ if (knownSkillNames.length > 0 && routing.skills) {
1931
+ const known = new Set(knownSkillNames);
1932
+ for (const name of Object.keys(routing.skills)) {
1933
+ if (known.has(name)) continue;
1934
+ warnings.push(
1935
+ `routing.skills.${name} references a skill that is not present in the local skill catalog. If this is intentional (e.g., a skill installed by a downstream consumer), this warning can be ignored.`
1936
+ );
1937
+ }
1938
+ }
1939
+ if (routing.modes) {
1940
+ const standardModes = new Set(STANDARD_COGNITIVE_MODES);
1941
+ for (const mode of Object.keys(routing.modes)) {
1942
+ if (standardModes.has(mode)) continue;
1943
+ warnings.push(
1944
+ `routing.modes.${mode} is not in STANDARD_COGNITIVE_MODES (${[...STANDARD_COGNITIVE_MODES].join(", ")}). Custom cognitive modes are allowed but uncommon; verify this is not a typo.`
1945
+ );
1946
+ }
1947
+ }
1948
+ return warnings;
1949
+ }
1950
+ function validateWorkflowConfig(config, options = {}) {
1891
1951
  if (!config || typeof config !== "object")
1892
1952
  return Err(new Error("Config is missing or not an object"));
1893
1953
  const c = config;
@@ -1903,6 +1963,7 @@ function validateWorkflowConfig(config) {
1903
1963
  if (!hasLegacyBackend && !hasModernBackends) {
1904
1964
  return Err(new Error("Config must define agent.backend or agent.backends."));
1905
1965
  }
1966
+ const warnings = [];
1906
1967
  if (hasModernBackends) {
1907
1968
  const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
1908
1969
  if (!backendsParsed.success) {
@@ -1913,9 +1974,10 @@ function validateWorkflowConfig(config) {
1913
1974
  return Err(new Error(`agent.routing: ${routingParsed.error.message}`));
1914
1975
  }
1915
1976
  if (routingParsed.data) {
1977
+ const routingData = routingParsed.data;
1916
1978
  const cross = crossFieldRoutingIssues(
1917
1979
  backendsParsed.data,
1918
- routingParsed.data
1980
+ routingData
1919
1981
  );
1920
1982
  if (cross.length > 0) {
1921
1983
  return Err(
@@ -1924,9 +1986,10 @@ function validateWorkflowConfig(config) {
1924
1986
  )
1925
1987
  );
1926
1988
  }
1989
+ warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
1927
1990
  }
1928
1991
  }
1929
- return Ok2(config);
1992
+ return Ok2({ config, warnings });
1930
1993
  }
1931
1994
  function getDefaultConfig() {
1932
1995
  return {
@@ -1979,11 +2042,55 @@ function getDefaultConfig() {
1979
2042
  };
1980
2043
  }
1981
2044
 
2045
+ // src/workflow/skill-catalog.ts
2046
+ import * as fs6 from "fs";
2047
+ import * as path6 from "path";
2048
+ import { parse as parseYaml } from "yaml";
2049
+ function discoverSkillCatalog(projectRoot) {
2050
+ const skillsRoot = path6.join(projectRoot, "agents", "skills");
2051
+ if (!fs6.existsSync(skillsRoot)) return [];
2052
+ const byName = /* @__PURE__ */ new Map();
2053
+ let hosts;
2054
+ try {
2055
+ hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
2056
+ } catch {
2057
+ return [];
2058
+ }
2059
+ for (const host of hosts) {
2060
+ if (!host.isDirectory()) continue;
2061
+ const hostDir = path6.join(skillsRoot, host.name);
2062
+ let skills;
2063
+ try {
2064
+ skills = fs6.readdirSync(hostDir, { withFileTypes: true });
2065
+ } catch {
2066
+ continue;
2067
+ }
2068
+ for (const skill of skills) {
2069
+ if (!skill.isDirectory()) continue;
2070
+ const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
2071
+ if (!fs6.existsSync(skillYamlPath)) continue;
2072
+ try {
2073
+ const content = fs6.readFileSync(skillYamlPath, "utf-8");
2074
+ const parsed = parseYaml(content);
2075
+ if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
2076
+ const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
2077
+ byName.set(parsed.name, entry);
2078
+ }
2079
+ } catch {
2080
+ }
2081
+ }
2082
+ }
2083
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2084
+ }
2085
+ function discoverSkillCatalogNames(projectRoot) {
2086
+ return discoverSkillCatalog(projectRoot).map((e) => e.name);
2087
+ }
2088
+
1982
2089
  // src/workflow/loader.ts
1983
2090
  var WorkflowLoader = class {
1984
2091
  async loadWorkflow(filePath) {
1985
2092
  try {
1986
- const content = await fs6.readFile(filePath, "utf-8");
2093
+ const content = await fs7.readFile(filePath, "utf-8");
1987
2094
  const parts = content.split("---");
1988
2095
  if (parts.length < 3) {
1989
2096
  return Err2(
@@ -1995,13 +2102,16 @@ var WorkflowLoader = class {
1995
2102
  const yamlContent = parts[1].trim();
1996
2103
  const promptTemplate = parts.slice(2).join("---").trim();
1997
2104
  const configData = parse(yamlContent);
1998
- const configResult = validateWorkflowConfig(configData);
2105
+ const projectRoot = path7.dirname(path7.resolve(filePath));
2106
+ const knownSkillNames = discoverSkillCatalogNames(projectRoot);
2107
+ const configResult = validateWorkflowConfig(configData, { knownSkillNames });
1999
2108
  if (!configResult.ok) {
2000
2109
  return Err2(configResult.error);
2001
2110
  }
2002
2111
  return Ok3({
2003
- config: configResult.value,
2004
- promptTemplate
2112
+ config: configResult.value.config,
2113
+ promptTemplate,
2114
+ warnings: configResult.value.warnings
2005
2115
  });
2006
2116
  } catch (error) {
2007
2117
  return Err2(error instanceof Error ? error : new Error(String(error)));
@@ -2010,7 +2120,7 @@ var WorkflowLoader = class {
2010
2120
  };
2011
2121
 
2012
2122
  // src/tracker/adapters/roadmap.ts
2013
- import * as fs7 from "fs/promises";
2123
+ import * as fs8 from "fs/promises";
2014
2124
  import { createHash as createHash2 } from "crypto";
2015
2125
  import {
2016
2126
  parseRoadmap,
@@ -2047,7 +2157,7 @@ var RoadmapTrackerAdapter = class {
2047
2157
  async fetchIssuesByStates(stateNames) {
2048
2158
  try {
2049
2159
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2050
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2160
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2051
2161
  const roadmapResult = parseRoadmap(content);
2052
2162
  if (!roadmapResult.ok) return roadmapResult;
2053
2163
  const issues = [];
@@ -2079,7 +2189,7 @@ var RoadmapTrackerAdapter = class {
2079
2189
  if (!terminal) {
2080
2190
  return Err3(new Error("Tracker config has no terminalStates; cannot mark complete"));
2081
2191
  }
2082
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2192
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2083
2193
  const roadmapResult = parseRoadmap(content);
2084
2194
  if (!roadmapResult.ok) return roadmapResult;
2085
2195
  const roadmap = roadmapResult.value;
@@ -2088,7 +2198,7 @@ var RoadmapTrackerAdapter = class {
2088
2198
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2089
2199
  if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
2090
2200
  target.status = terminal;
2091
- await fs7.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2201
+ await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2092
2202
  return Ok4(void 0);
2093
2203
  } catch (error) {
2094
2204
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2102,19 +2212,22 @@ var RoadmapTrackerAdapter = class {
2102
2212
  async claimIssue(issueId, orchestratorId) {
2103
2213
  try {
2104
2214
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2105
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2215
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2106
2216
  const roadmapResult = parseRoadmap(content);
2107
2217
  if (!roadmapResult.ok) return roadmapResult;
2108
2218
  const roadmap = roadmapResult.value;
2109
2219
  const target = this.findFeatureById(roadmap.milestones, issueId);
2110
2220
  if (!target) return Ok4(void 0);
2221
+ if (target.assignee != null && target.assignee !== orchestratorId) {
2222
+ return Ok4(void 0);
2223
+ }
2111
2224
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2112
2225
  return Ok4(void 0);
2113
2226
  }
2114
2227
  target.status = "in-progress";
2115
2228
  target.assignee = orchestratorId;
2116
2229
  target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2117
- await fs7.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2230
+ await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2118
2231
  return Ok4(void 0);
2119
2232
  } catch (error) {
2120
2233
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2131,7 +2244,7 @@ var RoadmapTrackerAdapter = class {
2131
2244
  if (!activeState) {
2132
2245
  return Err3(new Error("Tracker config has no activeStates; cannot release"));
2133
2246
  }
2134
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2247
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2135
2248
  const roadmapResult = parseRoadmap(content);
2136
2249
  if (!roadmapResult.ok) return roadmapResult;
2137
2250
  const roadmap = roadmapResult.value;
@@ -2143,7 +2256,7 @@ var RoadmapTrackerAdapter = class {
2143
2256
  target.status = activeState;
2144
2257
  target.assignee = null;
2145
2258
  target.updatedAt = null;
2146
- await fs7.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2259
+ await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
2147
2260
  return Ok4(void 0);
2148
2261
  } catch (error) {
2149
2262
  return Err3(error instanceof Error ? error : new Error(String(error)));
@@ -2165,7 +2278,7 @@ var RoadmapTrackerAdapter = class {
2165
2278
  async fetchIssueStatesByIds(issueIds) {
2166
2279
  try {
2167
2280
  if (!this.config.filePath) return Err3(new Error("Missing filePath"));
2168
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2281
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2169
2282
  const roadmapResult = parseRoadmap(content);
2170
2283
  if (!roadmapResult.ok) return roadmapResult;
2171
2284
  const issueMap = /* @__PURE__ */ new Map();
@@ -2230,8 +2343,8 @@ var LinearGraphQLStub = class {
2230
2343
  };
2231
2344
 
2232
2345
  // src/workspace/manager.ts
2233
- import * as fs8 from "fs/promises";
2234
- import * as path6 from "path";
2346
+ import * as fs9 from "fs/promises";
2347
+ import * as path8 from "path";
2235
2348
  import { execFile as execFile2 } from "child_process";
2236
2349
  import { promisify as promisify2 } from "util";
2237
2350
  import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
@@ -2262,15 +2375,15 @@ var WorkspaceManager = class {
2262
2375
  */
2263
2376
  resolvePath(identifier) {
2264
2377
  const sanitized = this.sanitizeIdentifier(identifier);
2265
- return path6.join(this.config.root, sanitized);
2378
+ return path8.join(this.config.root, sanitized);
2266
2379
  }
2267
2380
  /**
2268
2381
  * Discovers the git repository root from the workspace root directory.
2269
2382
  */
2270
2383
  async getRepoRoot() {
2271
2384
  if (this.repoRoot) return this.repoRoot;
2272
- const root = path6.resolve(this.config.root);
2273
- await fs8.mkdir(root, { recursive: true });
2385
+ const root = path8.resolve(this.config.root);
2386
+ await fs9.mkdir(root, { recursive: true });
2274
2387
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2275
2388
  this.repoRoot = stdout.trim();
2276
2389
  return this.repoRoot;
@@ -2281,23 +2394,23 @@ var WorkspaceManager = class {
2281
2394
  */
2282
2395
  async ensureWorkspace(identifier) {
2283
2396
  try {
2284
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2397
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2285
2398
  try {
2286
- await fs8.access(path6.join(workspacePath, ".git"));
2399
+ await fs9.access(path8.join(workspacePath, ".git"));
2287
2400
  const repoRoot2 = await this.getRepoRoot();
2288
2401
  try {
2289
2402
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2290
2403
  } catch {
2291
- await fs8.rm(workspacePath, { recursive: true, force: true });
2404
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2292
2405
  }
2293
2406
  } catch {
2294
2407
  try {
2295
- await fs8.access(workspacePath);
2408
+ await fs9.access(workspacePath);
2296
2409
  const repoRoot2 = await this.getRepoRoot();
2297
2410
  try {
2298
2411
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2299
2412
  } catch {
2300
- await fs8.rm(workspacePath, { recursive: true, force: true });
2413
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2301
2414
  }
2302
2415
  } catch {
2303
2416
  }
@@ -2393,7 +2506,7 @@ var WorkspaceManager = class {
2393
2506
  async exists(identifier) {
2394
2507
  try {
2395
2508
  const workspacePath = this.resolvePath(identifier);
2396
- await fs8.access(workspacePath);
2509
+ await fs9.access(workspacePath);
2397
2510
  return true;
2398
2511
  } catch {
2399
2512
  return false;
@@ -2406,9 +2519,9 @@ var WorkspaceManager = class {
2406
2519
  */
2407
2520
  async findPushedBranch(identifier) {
2408
2521
  try {
2409
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2522
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2410
2523
  try {
2411
- await fs8.access(path6.join(workspacePath, ".git"));
2524
+ await fs9.access(path8.join(workspacePath, ".git"));
2412
2525
  } catch {
2413
2526
  return null;
2414
2527
  }
@@ -2514,12 +2627,12 @@ var WorkspaceManager = class {
2514
2627
  */
2515
2628
  async removeWorkspace(identifier) {
2516
2629
  try {
2517
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2630
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2518
2631
  try {
2519
2632
  const repoRoot = await this.getRepoRoot();
2520
2633
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2521
2634
  } catch {
2522
- await fs8.rm(workspacePath, { recursive: true, force: true });
2635
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2523
2636
  }
2524
2637
  return Ok6(void 0);
2525
2638
  } catch (error) {
@@ -2544,7 +2657,7 @@ var WorkspaceHooks = class {
2544
2657
  if (!command) {
2545
2658
  return Ok7(void 0);
2546
2659
  }
2547
- return new Promise((resolve7) => {
2660
+ return new Promise((resolve8) => {
2548
2661
  const filteredEnv = {};
2549
2662
  for (const [k, v] of Object.entries(process.env)) {
2550
2663
  if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
@@ -2557,19 +2670,19 @@ var WorkspaceHooks = class {
2557
2670
  });
2558
2671
  const timeout = setTimeout(() => {
2559
2672
  child.kill();
2560
- resolve7(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2673
+ resolve8(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2561
2674
  }, this.config.timeoutMs);
2562
2675
  child.on("exit", (code) => {
2563
2676
  clearTimeout(timeout);
2564
2677
  if (code === 0) {
2565
- resolve7(Ok7(void 0));
2678
+ resolve8(Ok7(void 0));
2566
2679
  } else {
2567
- resolve7(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2680
+ resolve8(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2568
2681
  }
2569
2682
  });
2570
2683
  child.on("error", (error) => {
2571
2684
  clearTimeout(timeout);
2572
- resolve7(Err5(error));
2685
+ resolve8(Err5(error));
2573
2686
  });
2574
2687
  });
2575
2688
  }
@@ -2609,7 +2722,7 @@ var MockBackend = class {
2609
2722
  content: "Thinking...",
2610
2723
  sessionId: session.sessionId
2611
2724
  };
2612
- await new Promise((resolve7) => setTimeout(resolve7, 100));
2725
+ await new Promise((resolve8) => setTimeout(resolve8, 100));
2613
2726
  yield {
2614
2727
  type: "thought",
2615
2728
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2661,12 +2774,12 @@ var PromptRenderer = class {
2661
2774
 
2662
2775
  // src/orchestrator.ts
2663
2776
  import { EventEmitter } from "events";
2664
- import * as path19 from "path";
2777
+ import * as path21 from "path";
2665
2778
  import { randomUUID as randomUUID5 } from "crypto";
2666
2779
  import { writeTaint } from "@harness-engineering/core";
2667
2780
 
2668
2781
  // src/intelligence/pipeline-runner.ts
2669
- import * as path7 from "path";
2782
+ import * as path9 from "path";
2670
2783
  import { weightedRecommendPersona, refreshProfiles } from "@harness-engineering/intelligence";
2671
2784
  import {
2672
2785
  GitHubIssuesSyncAdapter,
@@ -2788,7 +2901,7 @@ var IntelligencePipelineRunner = class {
2788
2901
  }
2789
2902
  async loadGraphStore() {
2790
2903
  try {
2791
- const graphDir = path7.join(this.ctx.config.workspace.root, "..", "graph");
2904
+ const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
2792
2905
  const loaded = await this.ctx.graphStore.load(graphDir);
2793
2906
  if (loaded) {
2794
2907
  this.ctx.logger.info("Graph store loaded from disk");
@@ -3066,7 +3179,7 @@ var IntelligencePipelineRunner = class {
3066
3179
  };
3067
3180
 
3068
3181
  // src/completion/handler.ts
3069
- import * as path8 from "path";
3182
+ import * as path10 from "path";
3070
3183
  import { GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter2, loadTrackerSyncConfig as loadTrackerSyncConfig2 } from "@harness-engineering/core";
3071
3184
  var CompletionHandler = class {
3072
3185
  ctx;
@@ -3149,7 +3262,7 @@ var CompletionHandler = class {
3149
3262
  result: outcome.result
3150
3263
  });
3151
3264
  if (this.ctx.graphStore) {
3152
- const graphDir = path8.join(this.ctx.config.workspace.root, "..", "graph");
3265
+ const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
3153
3266
  await this.ctx.graphStore.save(graphDir);
3154
3267
  }
3155
3268
  } catch (err) {
@@ -3635,11 +3748,11 @@ function detectLegacyFields(agent) {
3635
3748
  }
3636
3749
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3637
3750
  const warnings = [];
3638
- for (const path22 of presentLegacy) {
3639
- if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
3640
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
3751
+ for (const path24 of presentLegacy) {
3752
+ if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
3753
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
3641
3754
  warnings.push(
3642
- `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3755
+ `Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3643
3756
  );
3644
3757
  }
3645
3758
  return warnings;
@@ -3667,7 +3780,7 @@ function migrateAgentConfig(agent) {
3667
3780
  }
3668
3781
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3669
3782
  const warnings = presentLegacy.map(
3670
- (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3783
+ (path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3671
3784
  );
3672
3785
  return {
3673
3786
  config: { ...agent, backends, routing },
@@ -3730,61 +3843,160 @@ function synthesizeLocal(agent) {
3730
3843
  }
3731
3844
 
3732
3845
  // src/agent/backend-router.ts
3846
+ function toArray(value) {
3847
+ return Array.isArray(value) ? value : [value];
3848
+ }
3733
3849
  var BackendRouter = class {
3734
3850
  backends;
3735
3851
  routing;
3852
+ decisionBus;
3736
3853
  constructor(opts) {
3737
3854
  this.backends = opts.backends;
3738
3855
  this.routing = opts.routing;
3856
+ this.decisionBus = opts.decisionBus;
3739
3857
  this.validateReferences();
3740
3858
  }
3741
3859
  /**
3742
- * Returns the backend name for a given use case.
3860
+ * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
3861
+ *
3862
+ * @param useCase the routing query
3863
+ * @param opts.invocationOverride if set and the named backend exists,
3864
+ * beats all other sources (D7 — the `--backend <name>` escape hatch)
3865
+ */
3866
+ resolve(useCase, opts) {
3867
+ const startedAt = performance.now();
3868
+ const path24 = [];
3869
+ const tryChain = (source, value) => {
3870
+ if (value === void 0) return void 0;
3871
+ for (const name of toArray(value)) {
3872
+ const step = { source, candidate: name, outcome: "considered" };
3873
+ path24.push(step);
3874
+ if (this.backends[name]) {
3875
+ step.outcome = "chosen";
3876
+ return name;
3877
+ }
3878
+ step.outcome = "unknown-backend";
3879
+ }
3880
+ return void 0;
3881
+ };
3882
+ const decide = (backendName) => {
3883
+ const def = this.backends[backendName];
3884
+ if (!def) {
3885
+ throw new Error(
3886
+ `BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
3887
+ );
3888
+ }
3889
+ return {
3890
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3891
+ useCase,
3892
+ resolutionPath: path24,
3893
+ backendName,
3894
+ backendType: def.type,
3895
+ durationMs: performance.now() - startedAt
3896
+ };
3897
+ };
3898
+ const emitAndReturn = (decision) => {
3899
+ this.decisionBus?.emit(decision);
3900
+ return decision;
3901
+ };
3902
+ const fromInvocation = tryChain(
3903
+ "invocation",
3904
+ opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
3905
+ );
3906
+ if (fromInvocation) return emitAndReturn(decide(fromInvocation));
3907
+ if (useCase.kind === "skill") {
3908
+ const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
3909
+ if (fromSkill) return emitAndReturn(decide(fromSkill));
3910
+ }
3911
+ const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
3912
+ if (mode !== void 0) {
3913
+ const fromMode = tryChain("mode", this.routing.modes?.[mode]);
3914
+ if (fromMode) return emitAndReturn(decide(fromMode));
3915
+ }
3916
+ const fromExisting = this.resolveExistingUseCase(useCase);
3917
+ if (fromExisting !== void 0) {
3918
+ const chained = tryChain("tier", fromExisting);
3919
+ if (chained) return emitAndReturn(decide(chained));
3920
+ }
3921
+ const fromDefault = tryChain("default", this.routing.default);
3922
+ if (fromDefault) return emitAndReturn(decide(fromDefault));
3923
+ const knownList = Object.keys(this.backends).join(", ") || "(none)";
3924
+ throw new Error(
3925
+ `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
3926
+ );
3927
+ }
3928
+ /**
3929
+ * Returns the {@link BackendDef} reference for the resolved name.
3930
+ * Identity-equal to the entry in `backends` (no copy) so callers
3931
+ * relying on reference equality (SC21) continue to work.
3932
+ */
3933
+ resolveDefinition(useCase, opts) {
3934
+ const decision = this.resolve(useCase, opts);
3935
+ const def = this.backends[decision.backendName];
3936
+ if (!def) {
3937
+ throw new Error(
3938
+ `BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3939
+ );
3940
+ }
3941
+ return def;
3942
+ }
3943
+ /**
3944
+ * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
3945
+ * for callers that need both. Replaces the previous pattern of
3946
+ * `resolveDefinition(useCase) + resolve(useCase)` which produced two
3947
+ * RoutingDecision emissions per dispatch — doubling routing-decision
3948
+ * log volume now that Phase 4 emits.
3743
3949
  *
3744
- * - `tier`: per-tier override, falling back to `routing.default`.
3745
- * - `intelligence`: per-layer override under `routing.intelligence`,
3746
- * falling back to `routing.default`.
3747
- * - `maintenance` / `chat`: always `routing.default`.
3950
+ * Identity-equal `BackendDef` (no copy) so callers relying on
3951
+ * reference equality (SC21) continue to work.
3748
3952
  */
3749
- resolve(useCase) {
3953
+ resolveDecisionAndDef(useCase, opts) {
3954
+ const decision = this.resolve(useCase, opts);
3955
+ const def = this.backends[decision.backendName];
3956
+ if (!def) {
3957
+ throw new Error(
3958
+ `BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3959
+ );
3960
+ }
3961
+ return { decision, def };
3962
+ }
3963
+ /**
3964
+ * The pre-Spec-B resolution helper: returns the configured
3965
+ * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
3966
+ * use cases (or `undefined` for skill/mode use cases, which are owned
3967
+ * by the per-skill / per-mode steps in {@link resolve}). Returning
3968
+ * `undefined` lets the caller fall through to `routing.default`.
3969
+ */
3970
+ resolveExistingUseCase(useCase) {
3750
3971
  switch (useCase.kind) {
3751
3972
  case "tier": {
3752
- const named = this.routing[useCase.tier];
3753
- return named ?? this.routing.default;
3973
+ const tierMap = this.routing;
3974
+ return tierMap[useCase.tier];
3754
3975
  }
3755
3976
  case "intelligence": {
3756
3977
  const intel = this.routing.intelligence;
3757
- return intel?.[useCase.layer] ?? this.routing.default;
3978
+ return intel?.[useCase.layer];
3758
3979
  }
3759
3980
  case "isolation": {
3760
3981
  const iso = this.routing.isolation;
3761
- return iso?.[useCase.tier] ?? this.routing.default;
3982
+ return iso?.[useCase.tier];
3762
3983
  }
3763
3984
  case "maintenance":
3764
3985
  case "chat":
3765
- return this.routing.default;
3766
- }
3767
- }
3768
- /**
3769
- * Returns the BackendDef reference for the resolved name. Returns the
3770
- * exact reference held in `backends` (no copy) so identity comparisons
3771
- * succeed (SC21).
3772
- */
3773
- resolveDefinition(useCase) {
3774
- const name = this.resolve(useCase);
3775
- const def = this.backends[name];
3776
- if (!def) {
3777
- throw new Error(
3778
- `BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3779
- );
3986
+ return void 0;
3987
+ case "skill":
3988
+ case "mode":
3989
+ return void 0;
3780
3990
  }
3781
- return def;
3782
3991
  }
3783
3992
  validateReferences() {
3784
3993
  const known = new Set(Object.keys(this.backends));
3785
3994
  const missing = [];
3786
- const check = (path22, name) => {
3787
- if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
3995
+ const check = (label, value) => {
3996
+ if (value === void 0) return;
3997
+ for (const name of toArray(value)) {
3998
+ if (!known.has(name)) missing.push({ path: label, name });
3999
+ }
3788
4000
  };
3789
4001
  check("default", this.routing.default);
3790
4002
  check("quick-fix", this.routing["quick-fix"]);
@@ -3796,8 +4008,14 @@ var BackendRouter = class {
3796
4008
  check("isolation.none", this.routing.isolation?.none);
3797
4009
  check("isolation.container", this.routing.isolation?.container);
3798
4010
  check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
4011
+ for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
4012
+ check(`skills.${skill}`, value);
4013
+ }
4014
+ for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
4015
+ check(`modes.${mode}`, value);
4016
+ }
3799
4017
  if (missing.length > 0) {
3800
- const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
4018
+ const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
3801
4019
  const known_ = [...known].join(", ") || "(none)";
3802
4020
  throw new Error(
3803
4021
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3814,11 +4032,11 @@ import {
3814
4032
  Ok as Ok10,
3815
4033
  Err as Err7
3816
4034
  } from "@harness-engineering/types";
3817
- function resolveExitCode(code, command, resolve7) {
4035
+ function resolveExitCode(code, command, resolve8) {
3818
4036
  if (code === 0) {
3819
- resolve7(Ok10(void 0));
4037
+ resolve8(Ok10(void 0));
3820
4038
  } else {
3821
- resolve7(
4039
+ resolve8(
3822
4040
  Err7({
3823
4041
  category: "agent_not_found",
3824
4042
  message: `Claude command '${command}' not found or failed`
@@ -3826,8 +4044,8 @@ function resolveExitCode(code, command, resolve7) {
3826
4044
  );
3827
4045
  }
3828
4046
  }
3829
- function resolveSpawnError(command, resolve7) {
3830
- resolve7(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4047
+ function resolveSpawnError(command, resolve8) {
4048
+ resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3831
4049
  }
3832
4050
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3833
4051
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4140,10 +4358,10 @@ var ClaudeBackend = class {
4140
4358
  errRl.close();
4141
4359
  }
4142
4360
  if (exitCode === null) {
4143
- await new Promise((resolve7) => {
4361
+ await new Promise((resolve8) => {
4144
4362
  child.on("exit", (code) => {
4145
4363
  exitCode = code;
4146
- resolve7(null);
4364
+ resolve8(null);
4147
4365
  });
4148
4366
  });
4149
4367
  }
@@ -4165,10 +4383,10 @@ var ClaudeBackend = class {
4165
4383
  return Ok10(void 0);
4166
4384
  }
4167
4385
  async healthCheck() {
4168
- return new Promise((resolve7) => {
4386
+ return new Promise((resolve8) => {
4169
4387
  const child = spawn2(this.command, ["--version"]);
4170
- child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
4171
- child.on("error", () => resolveSpawnError(this.command, resolve7));
4388
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
4389
+ child.on("error", () => resolveSpawnError(this.command, resolve8));
4172
4390
  });
4173
4391
  }
4174
4392
  };
@@ -5100,14 +5318,14 @@ var SshBackend = class {
5100
5318
  async healthCheck() {
5101
5319
  const args = [...this.buildSshArgs()];
5102
5320
  args[args.length - 1] = "true";
5103
- return new Promise((resolve7) => {
5321
+ return new Promise((resolve8) => {
5104
5322
  let child;
5105
5323
  try {
5106
5324
  child = this.spawnImpl(this.config.sshBinary, args, {
5107
5325
  stdio: ["ignore", "ignore", "pipe"]
5108
5326
  });
5109
5327
  } catch (err) {
5110
- resolve7(
5328
+ resolve8(
5111
5329
  Err13({
5112
5330
  category: "agent_not_found",
5113
5331
  message: err instanceof Error ? err.message : "failed to spawn ssh"
@@ -5128,9 +5346,9 @@ var SshBackend = class {
5128
5346
  child.on("close", (code) => {
5129
5347
  clearTimeout(timer);
5130
5348
  if (code === 0) {
5131
- resolve7(Ok16(void 0));
5349
+ resolve8(Ok16(void 0));
5132
5350
  } else {
5133
- resolve7(
5351
+ resolve8(
5134
5352
  Err13({
5135
5353
  category: "agent_not_found",
5136
5354
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
@@ -5140,7 +5358,7 @@ var SshBackend = class {
5140
5358
  });
5141
5359
  child.on("error", (err) => {
5142
5360
  clearTimeout(timer);
5143
- resolve7(Err13({ category: "agent_not_found", message: err.message }));
5361
+ resolve8(Err13({ category: "agent_not_found", message: err.message }));
5144
5362
  });
5145
5363
  });
5146
5364
  }
@@ -5188,13 +5406,13 @@ async function* readLines(stream) {
5188
5406
  if (buffer.length > 0) yield buffer;
5189
5407
  }
5190
5408
  function waitForExit(child) {
5191
- return new Promise((resolve7) => {
5409
+ return new Promise((resolve8) => {
5192
5410
  if (child.exitCode !== null) {
5193
- resolve7(child.exitCode);
5411
+ resolve8(child.exitCode);
5194
5412
  return;
5195
5413
  }
5196
- child.once("close", (code) => resolve7(code));
5197
- child.once("error", () => resolve7(null));
5414
+ child.once("close", (code) => resolve8(code));
5415
+ child.once("error", () => resolve8(null));
5198
5416
  });
5199
5417
  }
5200
5418
 
@@ -5384,14 +5602,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
5384
5602
  return out;
5385
5603
  }
5386
5604
  runOneShot(binary, args) {
5387
- return new Promise((resolve7) => {
5605
+ return new Promise((resolve8) => {
5388
5606
  let child;
5389
5607
  try {
5390
5608
  child = this.spawnImpl(binary, args, {
5391
5609
  stdio: ["ignore", "pipe", "pipe"]
5392
5610
  });
5393
5611
  } catch (err) {
5394
- resolve7(
5612
+ resolve8(
5395
5613
  Err14({
5396
5614
  category: "agent_not_found",
5397
5615
  message: err instanceof Error ? err.message : "failed to spawn runtime"
@@ -5416,9 +5634,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
5416
5634
  child.on("close", (code) => {
5417
5635
  clearTimeout(timer);
5418
5636
  if (code === 0) {
5419
- resolve7(Ok17(stdout));
5637
+ resolve8(Ok17(stdout));
5420
5638
  } else {
5421
- resolve7(
5639
+ resolve8(
5422
5640
  Err14({
5423
5641
  category: "response_error",
5424
5642
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
@@ -5428,7 +5646,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5428
5646
  });
5429
5647
  child.on("error", (err) => {
5430
5648
  clearTimeout(timer);
5431
- resolve7(Err14({ category: "agent_not_found", message: err.message }));
5649
+ resolve8(Err14({ category: "agent_not_found", message: err.message }));
5432
5650
  });
5433
5651
  });
5434
5652
  }
@@ -5488,13 +5706,13 @@ async function* readLines2(stream) {
5488
5706
  if (buffer.length > 0) yield buffer;
5489
5707
  }
5490
5708
  function waitForExit2(child) {
5491
- return new Promise((resolve7) => {
5709
+ return new Promise((resolve8) => {
5492
5710
  if (child.exitCode !== null) {
5493
- resolve7(child.exitCode);
5711
+ resolve8(child.exitCode);
5494
5712
  return;
5495
5713
  }
5496
- child.once("close", (code) => resolve7(code));
5497
- child.once("error", () => resolve7(null));
5714
+ child.once("close", (code) => resolve8(code));
5715
+ child.once("error", () => resolve8(null));
5498
5716
  });
5499
5717
  }
5500
5718
 
@@ -5694,13 +5912,13 @@ var ContainerBackend = class {
5694
5912
  import { execFile as execFile3, spawn as spawn5 } from "child_process";
5695
5913
  import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
5696
5914
  function dockerExec(args) {
5697
- return new Promise((resolve7, reject) => {
5915
+ return new Promise((resolve8, reject) => {
5698
5916
  execFile3("docker", args, (error, stdout) => {
5699
5917
  if (error) {
5700
5918
  reject(error);
5701
5919
  return;
5702
5920
  }
5703
- resolve7(stdout.trim());
5921
+ resolve8(stdout.trim());
5704
5922
  });
5705
5923
  });
5706
5924
  }
@@ -5759,11 +5977,11 @@ var DockerRuntime = class {
5759
5977
  } finally {
5760
5978
  rl.close();
5761
5979
  }
5762
- const exitCode = await new Promise((resolve7) => {
5980
+ const exitCode = await new Promise((resolve8) => {
5763
5981
  if (child.exitCode !== null) {
5764
- resolve7(child.exitCode);
5982
+ resolve8(child.exitCode);
5765
5983
  } else {
5766
- child.on("exit", (code) => resolve7(code ?? 1));
5984
+ child.on("exit", (code) => resolve8(code ?? 1));
5767
5985
  }
5768
5986
  });
5769
5987
  return exitCode;
@@ -5822,13 +6040,13 @@ var EnvSecretBackend = class {
5822
6040
  import { execFile as execFile4 } from "child_process";
5823
6041
  import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
5824
6042
  function opExec(args) {
5825
- return new Promise((resolve7, reject) => {
6043
+ return new Promise((resolve8, reject) => {
5826
6044
  execFile4("op", args, (error, stdout) => {
5827
6045
  if (error) {
5828
6046
  reject(error);
5829
6047
  return;
5830
6048
  }
5831
- resolve7(stdout.trim());
6049
+ resolve8(stdout.trim());
5832
6050
  });
5833
6051
  });
5834
6052
  }
@@ -5871,13 +6089,13 @@ var OnePasswordSecretBackend = class {
5871
6089
  import { execFile as execFile5 } from "child_process";
5872
6090
  import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
5873
6091
  function vaultExec(args, env) {
5874
- return new Promise((resolve7, reject) => {
6092
+ return new Promise((resolve8, reject) => {
5875
6093
  execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5876
6094
  if (error) {
5877
6095
  reject(error);
5878
6096
  return;
5879
6097
  }
5880
- resolve7(stdout.trim());
6098
+ resolve8(stdout.trim());
5881
6099
  });
5882
6100
  });
5883
6101
  }
@@ -5952,7 +6170,11 @@ var OrchestratorBackendFactory = class {
5952
6170
  opts;
5953
6171
  constructor(opts) {
5954
6172
  this.opts = opts;
5955
- this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
6173
+ this.router = new BackendRouter({
6174
+ backends: opts.backends,
6175
+ routing: opts.routing,
6176
+ ...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
6177
+ });
5956
6178
  }
5957
6179
  /**
5958
6180
  * Resolve `useCase` to a backend name, materialize a fresh
@@ -5971,12 +6193,21 @@ var OrchestratorBackendFactory = class {
5971
6193
  * is `undefined` for pure-modern configs. Threading the routed name
5972
6194
  * through dispatch eliminates that gap.
5973
6195
  */
5974
- resolveName(useCase) {
5975
- return this.router.resolve(useCase);
6196
+ resolveName(useCase, opts) {
6197
+ return this.router.resolve(useCase, opts).backendName;
6198
+ }
6199
+ /**
6200
+ * Spec B Phase 1: expose the underlying router for callers that need
6201
+ * it directly (e.g., {@link buildIntelligencePipeline} for the
6202
+ * I1 SEL/PESL comparison fix). Read-only access; consumers must not
6203
+ * mutate router state.
6204
+ */
6205
+ getRouter() {
6206
+ return this.router;
5976
6207
  }
5977
- forUseCase(useCase) {
5978
- const def = this.router.resolveDefinition(useCase);
5979
- const name = this.router.resolve(useCase);
6208
+ forUseCase(useCase, opts) {
6209
+ const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
6210
+ const name = decision.backendName;
5980
6211
  let backend;
5981
6212
  const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
5982
6213
  if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
@@ -6149,15 +6380,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
6149
6380
 
6150
6381
  // src/agent/intelligence-factory.ts
6151
6382
  function buildIntelligencePipeline(deps) {
6152
- const { config } = deps;
6383
+ const { config, router } = deps;
6153
6384
  const intel = config.intelligence;
6154
6385
  if (!intel?.enabled) return null;
6155
6386
  const selProvider = buildAnalysisProviderForLayer("sel", deps);
6156
6387
  if (!selProvider) return null;
6157
- const routing = config.agent.routing;
6158
- const peslName = routing?.intelligence?.pesl;
6159
- const selName = routing?.intelligence?.sel ?? routing?.default;
6160
- const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6388
+ const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
6389
+ const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
6390
+ const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6161
6391
  const peslModel = intel.models?.pesl ?? config.agent.model;
6162
6392
  const graphStore = new GraphStore();
6163
6393
  const pipeline = new IntelligencePipeline(selProvider, graphStore, {
@@ -6174,7 +6404,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
6174
6404
  const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
6175
6405
  return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
6176
6406
  }
6177
- const routed = resolveRoutedBackend(layer, config, logger);
6407
+ const routed = resolveRoutedBackend(layer, deps);
6178
6408
  if (!routed) return null;
6179
6409
  const { name, def } = routed;
6180
6410
  const resolver = localResolvers.get(name);
@@ -6199,20 +6429,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
6199
6429
  logger
6200
6430
  });
6201
6431
  }
6202
- function resolveRoutedBackend(layer, config, logger) {
6203
- const routing = config.agent.routing;
6432
+ function resolveRoutedBackend(layer, deps) {
6433
+ const { config, router, logger } = deps;
6204
6434
  const backends = config.agent.backends;
6205
- if (!routing || !backends) return null;
6206
- const layerName = routing.intelligence?.[layer];
6207
- const name = layerName ?? routing.default;
6208
- const def = backends[name];
6209
- if (!def) {
6435
+ if (!backends || !router) return null;
6436
+ try {
6437
+ const decision = router.resolve({ kind: "intelligence", layer });
6438
+ const def = backends[decision.backendName];
6439
+ if (!def) {
6440
+ logger.warn(
6441
+ `Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
6442
+ );
6443
+ return null;
6444
+ }
6445
+ return { name: decision.backendName, def };
6446
+ } catch (err) {
6210
6447
  logger.warn(
6211
- `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
6448
+ `Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
6212
6449
  );
6213
6450
  return null;
6214
6451
  }
6215
- return { name, def };
6216
6452
  }
6217
6453
  function buildExplicitProvider(provider, selModel, config) {
6218
6454
  if (provider.kind === "anthropic") {
@@ -6247,9 +6483,104 @@ function buildExplicitProvider(provider, selModel, config) {
6247
6483
  });
6248
6484
  }
6249
6485
 
6486
+ // src/routing/decision-bus.ts
6487
+ var RoutingDecisionBus = class {
6488
+ ringBuffer = [];
6489
+ listeners = /* @__PURE__ */ new Set();
6490
+ capacity;
6491
+ logger;
6492
+ constructor(opts) {
6493
+ this.capacity = opts?.capacity ?? 500;
6494
+ this.logger = opts?.logger;
6495
+ }
6496
+ emit(decision) {
6497
+ this.ringBuffer.push(decision);
6498
+ if (this.ringBuffer.length > this.capacity) {
6499
+ this.ringBuffer.shift();
6500
+ }
6501
+ if (this.logger) {
6502
+ this.logger.info("routing-decision", {
6503
+ useCase: decision.useCase,
6504
+ backendName: decision.backendName,
6505
+ resolutionPathLength: decision.resolutionPath.length,
6506
+ durationMs: decision.durationMs
6507
+ });
6508
+ }
6509
+ for (const listener of this.listeners) {
6510
+ try {
6511
+ listener(decision);
6512
+ } catch (err) {
6513
+ if (this.logger) {
6514
+ this.logger.warn("RoutingDecisionBus subscriber threw", {
6515
+ error: String(err)
6516
+ });
6517
+ }
6518
+ }
6519
+ }
6520
+ }
6521
+ recent(filter) {
6522
+ let out = this.ringBuffer.slice();
6523
+ if (filter?.skillName !== void 0) {
6524
+ out = out.filter(
6525
+ (d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
6526
+ );
6527
+ }
6528
+ if (filter?.mode !== void 0) {
6529
+ const m = filter.mode;
6530
+ out = out.filter(
6531
+ (d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
6532
+ );
6533
+ }
6534
+ if (filter?.backendName !== void 0) {
6535
+ out = out.filter((d) => d.backendName === filter.backendName);
6536
+ }
6537
+ if (filter?.limit !== void 0) {
6538
+ out = out.slice(-filter.limit).reverse();
6539
+ } else {
6540
+ out = out.reverse();
6541
+ }
6542
+ return out;
6543
+ }
6544
+ subscribe(listener) {
6545
+ this.listeners.add(listener);
6546
+ return () => {
6547
+ this.listeners.delete(listener);
6548
+ };
6549
+ }
6550
+ /**
6551
+ * Spec B Phase 5 (review-S2 fix): release all subscriber references so
6552
+ * teardown can complete without anchoring closures. Called from
6553
+ * `Orchestrator.stop()` before nulling the bus reference. The bus
6554
+ * remains usable after clear — `subscribe()` works as normal.
6555
+ */
6556
+ clearListeners() {
6557
+ this.listeners.clear();
6558
+ }
6559
+ };
6560
+
6561
+ // src/agent/triage-skill-mapping.ts
6562
+ function resolveSkillForTriage(triageSkill, catalog) {
6563
+ const expected = `harness-${triageSkill}`;
6564
+ const match = catalog.find((e) => e.name === expected);
6565
+ if (!match) return void 0;
6566
+ return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
6567
+ }
6568
+
6569
+ // src/agent/use-case-builder.ts
6570
+ function buildRoutingUseCase(issue, backendParam, catalog) {
6571
+ if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
6572
+ const decision = triageIssue(issue, {});
6573
+ const resolved = resolveSkillForTriage(decision.skill, catalog);
6574
+ if (resolved) {
6575
+ return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
6576
+ }
6577
+ const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
6578
+ return { kind: "tier", tier };
6579
+ }
6580
+
6250
6581
  // src/server/http.ts
6251
6582
  import * as http from "http";
6252
- import * as path15 from "path";
6583
+ import * as path17 from "path";
6253
6584
  import { assertPortUsable } from "@harness-engineering/core";
6254
6585
 
6255
6586
  // src/server/websocket.ts
@@ -6312,7 +6643,7 @@ import { z as z3 } from "zod";
6312
6643
  // src/server/utils.ts
6313
6644
  var DEFAULT_MAX_BYTES = 1048576;
6314
6645
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6315
- return new Promise((resolve7, reject) => {
6646
+ return new Promise((resolve8, reject) => {
6316
6647
  let body = "";
6317
6648
  let bytes = 0;
6318
6649
  req.on("data", (chunk) => {
@@ -6324,7 +6655,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6324
6655
  }
6325
6656
  body += String(chunk);
6326
6657
  });
6327
- req.on("end", () => resolve7(body));
6658
+ req.on("end", () => resolve8(body));
6328
6659
  req.on("error", reject);
6329
6660
  });
6330
6661
  }
@@ -6445,8 +6776,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6445
6776
 
6446
6777
  // src/server/routes/plans.ts
6447
6778
  import { z as z5 } from "zod";
6448
- import * as fs9 from "fs/promises";
6449
- import * as path9 from "path";
6779
+ import * as fs10 from "fs/promises";
6780
+ import * as path11 from "path";
6450
6781
  var PlanWriteSchema = z5.object({
6451
6782
  filename: z5.string().min(1),
6452
6783
  content: z5.string().min(1)
@@ -6466,7 +6797,7 @@ function handlePlansRoute(req, res, plansDir) {
6466
6797
  return;
6467
6798
  }
6468
6799
  const parsed = result.data;
6469
- const basename3 = path9.basename(parsed.filename);
6800
+ const basename3 = path11.basename(parsed.filename);
6470
6801
  if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
6471
6802
  res.writeHead(400, { "Content-Type": "application/json" });
6472
6803
  res.end(
@@ -6474,9 +6805,9 @@ function handlePlansRoute(req, res, plansDir) {
6474
6805
  );
6475
6806
  return;
6476
6807
  }
6477
- await fs9.mkdir(plansDir, { recursive: true });
6478
- const filePath = path9.join(plansDir, basename3);
6479
- await fs9.writeFile(filePath, parsed.content, "utf-8");
6808
+ await fs10.mkdir(plansDir, { recursive: true });
6809
+ const filePath = path11.join(plansDir, basename3);
6810
+ await fs10.writeFile(filePath, parsed.content, "utf-8");
6480
6811
  res.writeHead(201, { "Content-Type": "application/json" });
6481
6812
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6482
6813
  } catch {
@@ -6851,8 +7182,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
6851
7182
  }
6852
7183
 
6853
7184
  // src/server/routes/roadmap-actions.ts
6854
- import * as fs10 from "fs/promises";
6855
- import * as path10 from "path";
7185
+ import * as fs11 from "fs/promises";
7186
+ import * as path12 from "path";
6856
7187
  import {
6857
7188
  parseRoadmap as parseRoadmap2,
6858
7189
  serializeRoadmap as serializeRoadmap2,
@@ -6888,7 +7219,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6888
7219
  sendJSON2(res, 503, { error: "Roadmap path not configured" });
6889
7220
  return;
6890
7221
  }
6891
- const projectRoot = path10.dirname(path10.dirname(roadmapPath));
7222
+ const projectRoot = path12.dirname(path12.dirname(roadmapPath));
6892
7223
  const mode = loadProjectRoadmapMode(projectRoot);
6893
7224
  if (mode === "file-less") {
6894
7225
  const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
@@ -6941,7 +7272,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6941
7272
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
6942
7273
  return;
6943
7274
  }
6944
- const content = await fs10.readFile(roadmapPath, "utf-8");
7275
+ const content = await fs11.readFile(roadmapPath, "utf-8");
6945
7276
  const roadmapResult = parseRoadmap2(content);
6946
7277
  if (!roadmapResult.ok) {
6947
7278
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
@@ -6972,8 +7303,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6972
7303
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
6973
7304
  const tmpPath = roadmapPath + ".tmp";
6974
7305
  const serialized = serializeRoadmap2(roadmap);
6975
- await fs10.writeFile(tmpPath, serialized, "utf-8");
6976
- await fs10.rename(tmpPath, roadmapPath);
7306
+ await fs11.writeFile(tmpPath, serialized, "utf-8");
7307
+ await fs11.rename(tmpPath, roadmapPath);
6977
7308
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
6978
7309
  } catch (err) {
6979
7310
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7473,7 +7804,7 @@ import {
7473
7804
  } from "@harness-engineering/types";
7474
7805
 
7475
7806
  // src/proposals/gate.ts
7476
- import { parse as parseYaml } from "yaml";
7807
+ import { parse as parseYaml2 } from "yaml";
7477
7808
  import {
7478
7809
  getProposal,
7479
7810
  updateProposal,
@@ -7490,7 +7821,7 @@ function checkSkillYaml(yaml) {
7490
7821
  const findings = [];
7491
7822
  let doc;
7492
7823
  try {
7493
- doc = parseYaml(yaml);
7824
+ doc = parseYaml2(yaml);
7494
7825
  } catch (err) {
7495
7826
  findings.push({
7496
7827
  severity: "error",
@@ -7613,9 +7944,9 @@ async function runGate(projectPath, proposalId) {
7613
7944
  }
7614
7945
 
7615
7946
  // src/proposals/promote.ts
7616
- import * as fs11 from "fs";
7617
- import * as path11 from "path";
7618
- import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
7947
+ import * as fs12 from "fs";
7948
+ import * as path13 from "path";
7949
+ import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
7619
7950
  import {
7620
7951
  getProposal as getProposal2,
7621
7952
  updateProposal as updateProposal2,
@@ -7635,11 +7966,11 @@ var PromotionError = class extends Error {
7635
7966
  };
7636
7967
  var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7637
7968
  function skillDir(projectPath, name) {
7638
- return path11.join(projectPath, "agents", "skills", "claude-code", name);
7969
+ return path13.join(projectPath, "agents", "skills", "claude-code", name);
7639
7970
  }
7640
7971
  function readIfExists(p) {
7641
7972
  try {
7642
- return fs11.readFileSync(p, "utf-8");
7973
+ return fs12.readFileSync(p, "utf-8");
7643
7974
  } catch {
7644
7975
  return null;
7645
7976
  }
@@ -7647,7 +7978,7 @@ function readIfExists(p) {
7647
7978
  function injectProvenanceIntoYaml(yamlText, proposalId) {
7648
7979
  let doc;
7649
7980
  try {
7650
- doc = parseYaml2(yamlText);
7981
+ doc = parseYaml3(yamlText);
7651
7982
  } catch (err) {
7652
7983
  throw new PromotionError(
7653
7984
  `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
@@ -7685,15 +8016,15 @@ function assertGateReady(proposal) {
7685
8016
  }
7686
8017
  async function promoteNewSkill(projectPath, proposal) {
7687
8018
  const target = skillDir(projectPath, proposal.content.name);
7688
- if (fs11.existsSync(target)) {
8019
+ if (fs12.existsSync(target)) {
7689
8020
  throw new PromotionError(
7690
8021
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7691
8022
  );
7692
8023
  }
7693
- fs11.mkdirSync(target, { recursive: true });
8024
+ fs12.mkdirSync(target, { recursive: true });
7694
8025
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
7695
- fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
7696
- fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8026
+ fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8027
+ fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7697
8028
  return { skillPath: target };
7698
8029
  }
7699
8030
  async function promoteRefinement(projectPath, proposal) {
@@ -7701,12 +8032,12 @@ async function promoteRefinement(projectPath, proposal) {
7701
8032
  throw new PromotionError("refinement proposal is missing targetSkill");
7702
8033
  }
7703
8034
  const target = skillDir(projectPath, proposal.targetSkill);
7704
- if (!fs11.existsSync(target)) {
8035
+ if (!fs12.existsSync(target)) {
7705
8036
  throw new PromotionError(
7706
8037
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7707
8038
  );
7708
8039
  }
7709
- const yamlPath = path11.join(target, "skill.yaml");
8040
+ const yamlPath = path13.join(target, "skill.yaml");
7710
8041
  const before = readIfExists(yamlPath) ?? "";
7711
8042
  const after = injectProvenanceIntoYaml(before, proposal.id);
7712
8043
  if (after === before) {
@@ -7714,7 +8045,7 @@ async function promoteRefinement(projectPath, proposal) {
7714
8045
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7715
8046
  );
7716
8047
  }
7717
- fs11.writeFileSync(yamlPath, after);
8048
+ fs12.writeFileSync(yamlPath, after);
7718
8049
  return { skillPath: target };
7719
8050
  }
7720
8051
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8001,35 +8332,185 @@ function handleV1ProposalsRoute(req, res, deps) {
8001
8332
  return false;
8002
8333
  }
8003
8334
 
8004
- // src/server/routes/sessions.ts
8005
- import * as fs12 from "fs/promises";
8006
- import * as path12 from "path";
8335
+ // src/server/routes/v1/routing.ts
8007
8336
  import { z as z14 } from "zod";
8008
- var SessionCreateSchema = z14.object({
8009
- sessionId: z14.string().min(1)
8337
+ var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
8338
+ var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
8339
+ var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
8340
+ function sendJSON9(res, status, body) {
8341
+ res.writeHead(status, { "Content-Type": "application/json" });
8342
+ res.end(JSON.stringify(body));
8343
+ }
8344
+ function unavailable(res) {
8345
+ sendJSON9(res, 503, { error: "BackendRouter not available" });
8346
+ return true;
8347
+ }
8348
+ function resolveChain(value, backends) {
8349
+ return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
8350
+ }
8351
+ function buildResolvedChains(routing, backends) {
8352
+ const out = {};
8353
+ out["default"] = resolveChain(routing.default, backends);
8354
+ for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
8355
+ const v = routing[tier];
8356
+ if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
8357
+ }
8358
+ if (routing.intelligence) {
8359
+ for (const [layer, v] of Object.entries(routing.intelligence)) {
8360
+ if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
8361
+ }
8362
+ }
8363
+ if (routing.isolation) {
8364
+ for (const [tier, v] of Object.entries(routing.isolation)) {
8365
+ if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
8366
+ }
8367
+ }
8368
+ if (routing.skills) {
8369
+ for (const [name, v] of Object.entries(routing.skills)) {
8370
+ if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
8371
+ }
8372
+ }
8373
+ if (routing.modes) {
8374
+ for (const [mode, v] of Object.entries(routing.modes)) {
8375
+ if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
8376
+ }
8377
+ }
8378
+ return out;
8379
+ }
8380
+ function handleConfig(res, deps) {
8381
+ if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
8382
+ sendJSON9(res, 200, {
8383
+ routing: deps.routing,
8384
+ resolvedChains: buildResolvedChains(deps.routing, deps.backends),
8385
+ backends: Object.keys(deps.backends)
8386
+ });
8387
+ return true;
8388
+ }
8389
+ function parseDecisionsQuery(url) {
8390
+ const qIdx = url.indexOf("?");
8391
+ if (qIdx === -1) return {};
8392
+ const p = new URLSearchParams(url.slice(qIdx + 1));
8393
+ const filter = {};
8394
+ const skill = p.get("skill");
8395
+ const mode = p.get("mode");
8396
+ const backend = p.get("backend");
8397
+ const limit = p.get("limit");
8398
+ if (skill) filter.skillName = skill;
8399
+ if (mode) filter.mode = mode;
8400
+ if (backend) filter.backendName = backend;
8401
+ if (limit) {
8402
+ const n = Number(limit);
8403
+ if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
8404
+ }
8405
+ return filter;
8406
+ }
8407
+ function handleDecisions(req, res, deps) {
8408
+ if (!deps.bus) return unavailable(res);
8409
+ const filter = parseDecisionsQuery(req.url ?? "");
8410
+ sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
8411
+ return true;
8412
+ }
8413
+ var UseCaseSchema = z14.discriminatedUnion("kind", [
8414
+ z14.object({
8415
+ kind: z14.literal("tier"),
8416
+ tier: z14.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
8417
+ }),
8418
+ z14.object({ kind: z14.literal("intelligence"), layer: z14.enum(["sel", "pesl"]) }),
8419
+ z14.object({ kind: z14.literal("isolation"), tier: z14.string() }),
8420
+ z14.object({ kind: z14.literal("maintenance") }),
8421
+ z14.object({ kind: z14.literal("chat") }),
8422
+ z14.object({
8423
+ kind: z14.literal("skill"),
8424
+ skillName: z14.string().min(1),
8425
+ cognitiveMode: z14.string().optional()
8426
+ }),
8427
+ z14.object({ kind: z14.literal("mode"), cognitiveMode: z14.string().min(1) })
8428
+ ]);
8429
+ var TraceBodySchema = z14.object({
8430
+ useCase: UseCaseSchema,
8431
+ invocationOverride: z14.string().min(1).optional()
8432
+ });
8433
+ async function handleTrace(req, res, deps) {
8434
+ if (!deps.routing || !deps.backends) {
8435
+ unavailable(res);
8436
+ return true;
8437
+ }
8438
+ let raw;
8439
+ try {
8440
+ raw = await readBody(req);
8441
+ } catch {
8442
+ sendJSON9(res, 400, { error: "body read failed" });
8443
+ return true;
8444
+ }
8445
+ let parsed;
8446
+ try {
8447
+ parsed = JSON.parse(raw);
8448
+ } catch {
8449
+ sendJSON9(res, 400, { error: "invalid JSON body" });
8450
+ return true;
8451
+ }
8452
+ const r = TraceBodySchema.safeParse(parsed);
8453
+ if (!r.success) {
8454
+ sendJSON9(res, 400, { error: r.error.message });
8455
+ return true;
8456
+ }
8457
+ const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
8458
+ try {
8459
+ const dryRunRouter = new BackendRouter({
8460
+ backends: deps.backends,
8461
+ routing: deps.routing
8462
+ });
8463
+ const { decision, def } = dryRunRouter.resolveDecisionAndDef(
8464
+ r.data.useCase,
8465
+ opts
8466
+ );
8467
+ sendJSON9(res, 200, { decision, def: { type: def.type } });
8468
+ } catch (err) {
8469
+ sendJSON9(res, 500, { error: String(err) });
8470
+ }
8471
+ return true;
8472
+ }
8473
+ function handleV1RoutingRoute(req, res, deps) {
8474
+ const url = req.url ?? "";
8475
+ const method = req.method ?? "GET";
8476
+ if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
8477
+ if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
8478
+ if (method === "POST" && TRACE_RE.test(url)) {
8479
+ void handleTrace(req, res, deps);
8480
+ return true;
8481
+ }
8482
+ return false;
8483
+ }
8484
+
8485
+ // src/server/routes/sessions.ts
8486
+ import * as fs13 from "fs/promises";
8487
+ import * as path14 from "path";
8488
+ import { z as z15 } from "zod";
8489
+ var SessionCreateSchema = z15.object({
8490
+ sessionId: z15.string().min(1)
8010
8491
  }).passthrough();
8011
8492
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8012
8493
  function isSafeId(id) {
8013
- return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
8494
+ return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
8014
8495
  }
8015
8496
  function jsonResponse(res, status, data) {
8016
8497
  res.writeHead(status, { "Content-Type": "application/json" });
8017
8498
  res.end(JSON.stringify(data));
8018
8499
  }
8019
8500
  function extractSessionId(url) {
8020
- const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
8501
+ const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
8021
8502
  const id = segments.pop();
8022
8503
  return id && id !== "sessions" ? id : null;
8023
8504
  }
8024
8505
  async function handleList2(res, sessionsDir) {
8025
8506
  try {
8026
- const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
8507
+ const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8027
8508
  const sessions = [];
8028
8509
  for (const entry of entries) {
8029
8510
  if (!entry.isDirectory()) continue;
8030
8511
  try {
8031
- const content = await fs12.readFile(
8032
- path12.join(sessionsDir, entry.name, "session.json"),
8512
+ const content = await fs13.readFile(
8513
+ path14.join(sessionsDir, entry.name, "session.json"),
8033
8514
  "utf-8"
8034
8515
  );
8035
8516
  sessions.push(JSON.parse(content));
@@ -8054,7 +8535,7 @@ async function handleGet2(res, id, sessionsDir) {
8054
8535
  return;
8055
8536
  }
8056
8537
  try {
8057
- const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
8538
+ const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8058
8539
  jsonResponse(res, 200, JSON.parse(content));
8059
8540
  } catch (err) {
8060
8541
  if (err.code === "ENOENT") {
@@ -8077,9 +8558,9 @@ async function handleCreate(req, res, sessionsDir) {
8077
8558
  jsonResponse(res, 400, { error: "Invalid sessionId" });
8078
8559
  return;
8079
8560
  }
8080
- const sessionDir = path12.join(sessionsDir, session.sessionId);
8081
- await fs12.mkdir(sessionDir, { recursive: true });
8082
- await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8561
+ const sessionDir = path14.join(sessionsDir, session.sessionId);
8562
+ await fs13.mkdir(sessionDir, { recursive: true });
8563
+ await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8083
8564
  jsonResponse(res, 200, { ok: true });
8084
8565
  } catch {
8085
8566
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8093,10 +8574,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
8093
8574
  return;
8094
8575
  }
8095
8576
  const body = await readBody(req);
8096
- const updates = z14.record(z14.unknown()).parse(JSON.parse(body));
8097
- const sessionFilePath = path12.join(sessionsDir, id, "session.json");
8098
- const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
8099
- await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8577
+ const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
8578
+ const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8579
+ const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8580
+ await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8100
8581
  jsonResponse(res, 200, { ok: true });
8101
8582
  } catch {
8102
8583
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8109,7 +8590,7 @@ async function handleDelete(res, url, sessionsDir) {
8109
8590
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8110
8591
  return;
8111
8592
  }
8112
- await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
8593
+ await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8113
8594
  jsonResponse(res, 200, { ok: true });
8114
8595
  } catch {
8115
8596
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8213,20 +8694,20 @@ function handleStreamsRoute(req, res, recorder) {
8213
8694
  }
8214
8695
 
8215
8696
  // src/server/routes/auth.ts
8216
- import { z as z15 } from "zod";
8697
+ import { z as z16 } from "zod";
8217
8698
  import {
8218
8699
  TokenScopeSchema,
8219
8700
  BridgeKindSchema,
8220
8701
  AuthTokenPublicSchema
8221
8702
  } from "@harness-engineering/types";
8222
- var CreateBodySchema = z15.object({
8223
- name: z15.string().min(1).max(100),
8224
- scopes: z15.array(TokenScopeSchema).min(1),
8703
+ var CreateBodySchema = z16.object({
8704
+ name: z16.string().min(1).max(100),
8705
+ scopes: z16.array(TokenScopeSchema).min(1),
8225
8706
  bridgeKind: BridgeKindSchema.optional(),
8226
- tenantId: z15.string().optional(),
8227
- expiresAt: z15.string().datetime().optional()
8707
+ tenantId: z16.string().optional(),
8708
+ expiresAt: z16.string().datetime().optional()
8228
8709
  });
8229
- function sendJSON9(res, status, body) {
8710
+ function sendJSON10(res, status, body) {
8230
8711
  res.writeHead(status, { "Content-Type": "application/json" });
8231
8712
  res.end(JSON.stringify(body));
8232
8713
  }
@@ -8236,19 +8717,19 @@ async function handlePost(req, res, store) {
8236
8717
  raw = await readBody(req);
8237
8718
  } catch (err) {
8238
8719
  const msg = err instanceof Error ? err.message : "Failed to read body";
8239
- sendJSON9(res, 413, { error: msg });
8720
+ sendJSON10(res, 413, { error: msg });
8240
8721
  return;
8241
8722
  }
8242
8723
  let json;
8243
8724
  try {
8244
8725
  json = JSON.parse(raw);
8245
8726
  } catch {
8246
- sendJSON9(res, 400, { error: "Invalid JSON body" });
8727
+ sendJSON10(res, 400, { error: "Invalid JSON body" });
8247
8728
  return;
8248
8729
  }
8249
8730
  const parsed = CreateBodySchema.safeParse(json);
8250
8731
  if (!parsed.success) {
8251
- sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8732
+ sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8252
8733
  return;
8253
8734
  }
8254
8735
  try {
@@ -8261,37 +8742,37 @@ async function handlePost(req, res, store) {
8261
8742
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
8262
8743
  const result = await store.create(input);
8263
8744
  const publicRecord = AuthTokenPublicSchema.parse(result.record);
8264
- sendJSON9(res, 200, {
8745
+ sendJSON10(res, 200, {
8265
8746
  ...publicRecord,
8266
8747
  token: result.token
8267
8748
  });
8268
8749
  } catch (err) {
8269
8750
  const msg = err instanceof Error ? err.message : "Failed to create token";
8270
8751
  if (msg.includes("already exists")) {
8271
- sendJSON9(res, 409, { error: msg });
8752
+ sendJSON10(res, 409, { error: msg });
8272
8753
  return;
8273
8754
  }
8274
- sendJSON9(res, 500, { error: "Internal error creating token" });
8755
+ sendJSON10(res, 500, { error: "Internal error creating token" });
8275
8756
  }
8276
8757
  }
8277
8758
  async function handleList3(res, store) {
8278
8759
  try {
8279
8760
  const list = await store.list();
8280
- sendJSON9(res, 200, list);
8761
+ sendJSON10(res, 200, list);
8281
8762
  } catch {
8282
- sendJSON9(res, 500, { error: "Internal error listing tokens" });
8763
+ sendJSON10(res, 500, { error: "Internal error listing tokens" });
8283
8764
  }
8284
8765
  }
8285
8766
  async function handleDelete2(res, store, id) {
8286
8767
  try {
8287
8768
  const ok = await store.revoke(id);
8288
8769
  if (!ok) {
8289
- sendJSON9(res, 404, { error: "Token not found" });
8770
+ sendJSON10(res, 404, { error: "Token not found" });
8290
8771
  return;
8291
8772
  }
8292
- sendJSON9(res, 200, { deleted: true });
8773
+ sendJSON10(res, 200, { deleted: true });
8293
8774
  } catch {
8294
- sendJSON9(res, 500, { error: "Internal error revoking token" });
8775
+ sendJSON10(res, 500, { error: "Internal error revoking token" });
8295
8776
  }
8296
8777
  }
8297
8778
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -8316,12 +8797,12 @@ function handleAuthRoute(req, res, store) {
8316
8797
  return true;
8317
8798
  }
8318
8799
  }
8319
- sendJSON9(res, 405, { error: "Method not allowed" });
8800
+ sendJSON10(res, 405, { error: "Method not allowed" });
8320
8801
  return true;
8321
8802
  }
8322
8803
 
8323
8804
  // src/server/routes/local-model.ts
8324
- function sendJSON10(res, status, body) {
8805
+ function sendJSON11(res, status, body) {
8325
8806
  res.writeHead(status, { "Content-Type": "application/json" });
8326
8807
  res.end(JSON.stringify(body));
8327
8808
  }
@@ -8329,36 +8810,36 @@ function handleLocalModelRoute(req, res, getStatus) {
8329
8810
  const { method, url } = req;
8330
8811
  if (url !== "/api/v1/local-model/status") return false;
8331
8812
  if (method !== "GET") {
8332
- sendJSON10(res, 405, { error: "Method not allowed" });
8813
+ sendJSON11(res, 405, { error: "Method not allowed" });
8333
8814
  return true;
8334
8815
  }
8335
8816
  if (!getStatus) {
8336
- sendJSON10(res, 503, { error: "Local backend not configured" });
8817
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8337
8818
  return true;
8338
8819
  }
8339
8820
  const status = getStatus();
8340
8821
  if (!status) {
8341
- sendJSON10(res, 503, { error: "Local backend not configured" });
8822
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8342
8823
  return true;
8343
8824
  }
8344
- sendJSON10(res, 200, status);
8825
+ sendJSON11(res, 200, status);
8345
8826
  return true;
8346
8827
  }
8347
8828
  function handleLocalModelsRoute(req, res, getStatuses) {
8348
8829
  const { method, url } = req;
8349
8830
  if (url !== "/api/v1/local-models/status") return false;
8350
8831
  if (method !== "GET") {
8351
- sendJSON10(res, 405, { error: "Method not allowed" });
8832
+ sendJSON11(res, 405, { error: "Method not allowed" });
8352
8833
  return true;
8353
8834
  }
8354
8835
  const statuses = getStatuses ? getStatuses() : [];
8355
- sendJSON10(res, 200, statuses);
8836
+ sendJSON11(res, 200, statuses);
8356
8837
  return true;
8357
8838
  }
8358
8839
 
8359
8840
  // src/server/static.ts
8360
- import * as fs13 from "fs";
8361
- import * as path13 from "path";
8841
+ import * as fs14 from "fs";
8842
+ import * as path15 from "path";
8362
8843
  var MIME_TYPES = {
8363
8844
  ".html": "text/html; charset=utf-8",
8364
8845
  ".js": "application/javascript; charset=utf-8",
@@ -8378,29 +8859,29 @@ var MIME_TYPES = {
8378
8859
  function handleStaticFile(req, res, dashboardDir) {
8379
8860
  const { method, url } = req;
8380
8861
  if (method !== "GET") return false;
8381
- const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
8382
- const wsPath = path13.posix.join(path13.posix.sep, "ws");
8862
+ const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
8863
+ const wsPath = path15.posix.join(path15.posix.sep, "ws");
8383
8864
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
8384
8865
  const urlPath = new URL(url ?? "/", "http://localhost").pathname;
8385
- const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8386
- const resolved = path13.resolve(requestedPath);
8387
- if (!resolved.startsWith(path13.resolve(dashboardDir))) {
8388
- return serveFile(path13.join(dashboardDir, "index.html"), res);
8866
+ const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8867
+ const resolved = path15.resolve(requestedPath);
8868
+ if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8869
+ return serveFile(path15.join(dashboardDir, "index.html"), res);
8389
8870
  }
8390
- if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
8871
+ if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
8391
8872
  return serveFile(resolved, res);
8392
8873
  }
8393
- const indexPath = path13.join(dashboardDir, "index.html");
8394
- if (fs13.existsSync(indexPath)) {
8874
+ const indexPath = path15.join(dashboardDir, "index.html");
8875
+ if (fs14.existsSync(indexPath)) {
8395
8876
  return serveFile(indexPath, res);
8396
8877
  }
8397
8878
  return false;
8398
8879
  }
8399
8880
  function serveFile(filePath, res) {
8400
- const ext = path13.extname(filePath).toLowerCase();
8881
+ const ext = path15.extname(filePath).toLowerCase();
8401
8882
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8402
8883
  try {
8403
- const content = fs13.readFileSync(filePath);
8884
+ const content = fs14.readFileSync(filePath);
8404
8885
  res.writeHead(200, { "Content-Type": contentType });
8405
8886
  res.end(content);
8406
8887
  return true;
@@ -8410,8 +8891,8 @@ function serveFile(filePath, res) {
8410
8891
  }
8411
8892
 
8412
8893
  // src/server/plan-watcher.ts
8413
- import * as fs14 from "fs";
8414
- import * as path14 from "path";
8894
+ import * as fs15 from "fs";
8895
+ import * as path16 from "path";
8415
8896
  var PlanWatcher = class {
8416
8897
  plansDir;
8417
8898
  queue;
@@ -8425,11 +8906,11 @@ var PlanWatcher = class {
8425
8906
  * Creates the directory if it does not exist.
8426
8907
  */
8427
8908
  start() {
8428
- fs14.mkdirSync(this.plansDir, { recursive: true });
8429
- this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
8909
+ fs15.mkdirSync(this.plansDir, { recursive: true });
8910
+ this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
8430
8911
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
8431
- const filePath = path14.join(this.plansDir, filename);
8432
- if (fs14.existsSync(filePath)) {
8912
+ const filePath = path16.join(this.plansDir, filename);
8913
+ if (fs15.existsSync(filePath)) {
8433
8914
  void this.handleNewPlan(filename);
8434
8915
  }
8435
8916
  }
@@ -8462,7 +8943,7 @@ var PlanWatcher = class {
8462
8943
  // src/auth/tokens.ts
8463
8944
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
8464
8945
  import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
8465
- import { dirname as dirname4 } from "path";
8946
+ import { dirname as dirname5 } from "path";
8466
8947
  import bcrypt from "bcryptjs";
8467
8948
  import {
8468
8949
  AuthTokenSchema,
@@ -8482,8 +8963,8 @@ function parseToken(raw) {
8482
8963
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
8483
8964
  }
8484
8965
  var TokenStore = class {
8485
- constructor(path22) {
8486
- this.path = path22;
8966
+ constructor(path24) {
8967
+ this.path = path24;
8487
8968
  }
8488
8969
  path;
8489
8970
  cache = null;
@@ -8504,7 +8985,7 @@ var TokenStore = class {
8504
8985
  return this.cache;
8505
8986
  }
8506
8987
  async persist(records) {
8507
- await mkdir7(dirname4(this.path), { recursive: true });
8988
+ await mkdir7(dirname5(this.path), { recursive: true });
8508
8989
  const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
8509
8990
  await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
8510
8991
  await rename2(tmp, this.path);
@@ -8587,11 +9068,11 @@ var TokenStore = class {
8587
9068
 
8588
9069
  // src/auth/audit.ts
8589
9070
  import { appendFile, mkdir as mkdir8 } from "fs/promises";
8590
- import { dirname as dirname5 } from "path";
9071
+ import { dirname as dirname6 } from "path";
8591
9072
  import { AuthAuditEntrySchema } from "@harness-engineering/types";
8592
9073
  var AuditLogger = class {
8593
- constructor(path22, opts = {}) {
8594
- this.path = path22;
9074
+ constructor(path24, opts = {}) {
9075
+ this.path = path24;
8595
9076
  this.opts = opts;
8596
9077
  }
8597
9078
  path;
@@ -8618,7 +9099,7 @@ var AuditLogger = class {
8618
9099
  async writeLine(line) {
8619
9100
  try {
8620
9101
  if (this.opts.createDir !== false && !this.dirEnsured) {
8621
- await mkdir8(dirname5(this.path), { recursive: true });
9102
+ await mkdir8(dirname6(this.path), { recursive: true });
8622
9103
  this.dirEnsured = true;
8623
9104
  }
8624
9105
  await appendFile(this.path, line, "utf8");
@@ -8718,14 +9199,36 @@ var V1_BRIDGE_ROUTES = [
8718
9199
  pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
8719
9200
  scope: "read-telemetry",
8720
9201
  description: "Prompt-cache hit/miss snapshot (rolling window)."
9202
+ },
9203
+ // ── Spec B Phase 5 routing observability ──
9204
+ // D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
9205
+ // precedent (read-only observability). A dedicated `read-routing`
9206
+ // scope was rejected to avoid a TokenScopeSchema + ADR cascade.
9207
+ {
9208
+ method: "GET",
9209
+ pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
9210
+ scope: "read-telemetry",
9211
+ description: "Current routing config + resolved fallback chains + known backends."
9212
+ },
9213
+ {
9214
+ method: "GET",
9215
+ pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
9216
+ scope: "read-telemetry",
9217
+ description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
9218
+ },
9219
+ {
9220
+ method: "POST",
9221
+ pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
9222
+ scope: "read-telemetry",
9223
+ description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
8721
9224
  }
8722
9225
  ];
8723
9226
  function isV1Bridge(method, url) {
8724
9227
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
8725
9228
  }
8726
- function requiredBridgeScope(method, path22) {
9229
+ function requiredBridgeScope(method, path24) {
8727
9230
  for (const r of V1_BRIDGE_ROUTES) {
8728
- if (r.method === method && r.pattern.test(path22)) return r.scope;
9231
+ if (r.method === method && r.pattern.test(path24)) return r.scope;
8729
9232
  }
8730
9233
  return null;
8731
9234
  }
@@ -8735,24 +9238,24 @@ function hasScope(held, required) {
8735
9238
  if (held.includes("admin")) return true;
8736
9239
  return held.includes(required);
8737
9240
  }
8738
- function requiredScopeForRoute(method, path22) {
8739
- const bridgeScope = requiredBridgeScope(method, path22);
9241
+ function requiredScopeForRoute(method, path24) {
9242
+ const bridgeScope = requiredBridgeScope(method, path24);
8740
9243
  if (bridgeScope) return bridgeScope;
8741
- if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
8742
- if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
8743
- if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
8744
- if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
8745
- if (path22.startsWith("/api/interactions")) return "resolve-interaction";
8746
- if (path22.startsWith("/api/plans")) return "read-status";
8747
- if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
8748
- if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
8749
- if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
8750
- if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
9244
+ if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9245
+ if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9246
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9247
+ if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9248
+ if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9249
+ if (path24.startsWith("/api/plans")) return "read-status";
9250
+ if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9251
+ if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9252
+ if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9253
+ if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
8751
9254
  return "read-status";
8752
- if (path22.startsWith("/api/maintenance")) return "trigger-job";
8753
- if (path22.startsWith("/api/streams")) return "read-status";
8754
- if (path22.startsWith("/api/sessions")) return "read-status";
8755
- if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
9255
+ if (path24.startsWith("/api/maintenance")) return "trigger-job";
9256
+ if (path24.startsWith("/api/streams")) return "read-status";
9257
+ if (path24.startsWith("/api/sessions")) return "read-status";
9258
+ if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
8756
9259
  return null;
8757
9260
  }
8758
9261
 
@@ -8816,6 +9319,15 @@ var OrchestratorServer = class {
8816
9319
  getLocalModelStatuses = null;
8817
9320
  webhooks;
8818
9321
  cacheMetrics;
9322
+ // Spec B Phase 5 — routing observability accessor closures + the WS
9323
+ // broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
9324
+ // calls it explicitly; clearListeners in Orchestrator.stop() is the
9325
+ // belt-and-suspenders second line).
9326
+ getBackendRouterFn = null;
9327
+ getRoutingDecisionBusFn = null;
9328
+ getRoutingConfigFn = null;
9329
+ getBackendsFn = null;
9330
+ routingDecisionUnsubscribe = null;
8819
9331
  recorder = null;
8820
9332
  planWatcher = null;
8821
9333
  tokenStore;
@@ -8828,8 +9340,8 @@ var OrchestratorServer = class {
8828
9340
  this.orchestrator = orchestrator;
8829
9341
  this.port = port;
8830
9342
  this.initDependencies(deps);
8831
- const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
8832
- const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
9343
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
9344
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
8833
9345
  this.tokenStore = new TokenStore(tokensPath);
8834
9346
  this.auditLogger = new AuditLogger(auditPath);
8835
9347
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -8842,20 +9354,24 @@ var OrchestratorServer = class {
8842
9354
  }
8843
9355
  initDependencies(deps) {
8844
9356
  this.interactionQueue = deps?.interactionQueue;
8845
- this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8846
- this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
9357
+ this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
9358
+ this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
8847
9359
  this.claudeCommand = deps?.claudeCommand ?? "claude";
8848
9360
  this.pipeline = deps?.pipeline ?? null;
8849
9361
  this.analysisArchive = deps?.analysisArchive;
8850
9362
  this.roadmapPath = deps?.roadmapPath ?? null;
8851
9363
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
8852
- this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
9364
+ this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
8853
9365
  this.projectPath = deps?.projectPath ?? process.cwd();
8854
9366
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
8855
9367
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
8856
9368
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
8857
9369
  this.webhooks = deps?.webhooks;
8858
9370
  this.cacheMetrics = deps?.cacheMetrics;
9371
+ this.getBackendRouterFn = deps?.getBackendRouter ?? null;
9372
+ this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
9373
+ this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
9374
+ this.getBackendsFn = deps?.getBackends ?? null;
8859
9375
  }
8860
9376
  wireEvents() {
8861
9377
  this.stateChangeListener = (snapshot) => {
@@ -8866,6 +9382,12 @@ var OrchestratorServer = class {
8866
9382
  };
8867
9383
  this.orchestrator.on("state_change", this.stateChangeListener);
8868
9384
  this.orchestrator.on("agent_event", this.agentEventListener);
9385
+ const bus = this.getRoutingDecisionBusFn?.() ?? null;
9386
+ if (bus) {
9387
+ this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
9388
+ this.broadcaster.broadcast("routing:decision", decision);
9389
+ });
9390
+ }
8869
9391
  }
8870
9392
  /**
8871
9393
  * Broadcast a new interaction to all WebSocket clients.
@@ -9021,6 +9543,14 @@ var OrchestratorServer = class {
9021
9543
  (req, res) => handleV1TelemetryRoute(req, res, {
9022
9544
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
9023
9545
  }),
9546
+ // Spec B Phase 5 — routing observability. Returns 503 when the
9547
+ // backendFactory is null (legacy single-backend configs).
9548
+ (req, res) => handleV1RoutingRoute(req, res, {
9549
+ router: this.getBackendRouterFn?.() ?? null,
9550
+ bus: this.getRoutingDecisionBusFn?.() ?? null,
9551
+ routing: this.getRoutingConfigFn?.() ?? null,
9552
+ backends: this.getBackendsFn?.() ?? null
9553
+ }),
9024
9554
  // Hermes Phase 4 — skill proposal review queue. Read scopes
9025
9555
  // (`read-status`) and write scopes (`manage-proposals`) are enforced
9026
9556
  // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
@@ -9117,17 +9647,21 @@ var OrchestratorServer = class {
9117
9647
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
9118
9648
  this.planWatcher.start();
9119
9649
  }
9120
- return new Promise((resolve7) => {
9650
+ return new Promise((resolve8) => {
9121
9651
  const host = getBindHost();
9122
9652
  this.httpServer.listen(this.port, host, () => {
9123
9653
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
9124
- resolve7();
9654
+ resolve8();
9125
9655
  });
9126
9656
  });
9127
9657
  }
9128
9658
  stop() {
9129
9659
  this.orchestrator.removeListener("state_change", this.stateChangeListener);
9130
9660
  this.orchestrator.removeListener("agent_event", this.agentEventListener);
9661
+ if (this.routingDecisionUnsubscribe) {
9662
+ this.routingDecisionUnsubscribe();
9663
+ this.routingDecisionUnsubscribe = null;
9664
+ }
9131
9665
  if (this.planWatcher) {
9132
9666
  this.planWatcher.stop();
9133
9667
  this.planWatcher = null;
@@ -9140,7 +9674,7 @@ var OrchestratorServer = class {
9140
9674
  // src/gateway/webhooks/store.ts
9141
9675
  import { randomBytes as randomBytes4 } from "crypto";
9142
9676
  import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
9143
- import { dirname as dirname6 } from "path";
9677
+ import { dirname as dirname7 } from "path";
9144
9678
  import { WebhookSubscriptionSchema } from "@harness-engineering/types";
9145
9679
 
9146
9680
  // src/gateway/webhooks/signer.ts
@@ -9171,8 +9705,8 @@ function genSecret2() {
9171
9705
  return randomBytes4(32).toString("base64url");
9172
9706
  }
9173
9707
  var WebhookStore = class {
9174
- constructor(path22) {
9175
- this.path = path22;
9708
+ constructor(path24) {
9709
+ this.path = path24;
9176
9710
  }
9177
9711
  path;
9178
9712
  cache = null;
@@ -9195,7 +9729,7 @@ var WebhookStore = class {
9195
9729
  async persist(records) {
9196
9730
  const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
9197
9731
  try {
9198
- await mkdir9(dirname6(this.path), { recursive: true });
9732
+ await mkdir9(dirname7(this.path), { recursive: true });
9199
9733
  await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
9200
9734
  await rename3(tmp, this.path);
9201
9735
  await chmod(this.path, 384);
@@ -10164,8 +10698,8 @@ var StructuredLogger = class {
10164
10698
  };
10165
10699
 
10166
10700
  // src/workspace/config-scanner.ts
10167
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
10168
- import { join as join14, relative } from "path";
10701
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
10702
+ import { join as join15, relative } from "path";
10169
10703
  import {
10170
10704
  scanForInjection,
10171
10705
  SecurityScanner,
@@ -10189,10 +10723,10 @@ function adjustFindingSeverity(findings) {
10189
10723
  });
10190
10724
  }
10191
10725
  async function scanSingleFile(filePath, targetDir, scanner) {
10192
- if (!existsSync5(filePath)) return null;
10726
+ if (!existsSync6(filePath)) return null;
10193
10727
  let content;
10194
10728
  try {
10195
- content = readFileSync5(filePath, "utf8");
10729
+ content = readFileSync6(filePath, "utf8");
10196
10730
  } catch {
10197
10731
  return null;
10198
10732
  }
@@ -10211,7 +10745,7 @@ async function scanWorkspaceConfig(workspacePath) {
10211
10745
  const scanner = new SecurityScanner(parseSecurityConfig({}));
10212
10746
  const results = [];
10213
10747
  for (const configFile of CONFIG_FILES) {
10214
- const result = await scanSingleFile(join14(workspacePath, configFile), workspacePath, scanner);
10748
+ const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
10215
10749
  if (result) results.push(result);
10216
10750
  }
10217
10751
  return { exitCode: computeScanExitCode(results), results };
@@ -10725,19 +11259,19 @@ var SingleProcessLeaderElector = class {
10725
11259
  };
10726
11260
 
10727
11261
  // src/maintenance/reporter.ts
10728
- import * as fs15 from "fs";
10729
- import * as path16 from "path";
10730
- import { z as z16 } from "zod";
10731
- var RunResultSchema = z16.object({
10732
- taskId: z16.string(),
10733
- startedAt: z16.string(),
10734
- completedAt: z16.string(),
10735
- status: z16.enum(["success", "failure", "skipped", "no-issues"]),
10736
- findings: z16.number(),
10737
- fixed: z16.number(),
10738
- prUrl: z16.string().nullable(),
10739
- prUpdated: z16.boolean(),
10740
- error: z16.string().optional()
11262
+ import * as fs16 from "fs";
11263
+ import * as path18 from "path";
11264
+ import { z as z17 } from "zod";
11265
+ var RunResultSchema = z17.object({
11266
+ taskId: z17.string(),
11267
+ startedAt: z17.string(),
11268
+ completedAt: z17.string(),
11269
+ status: z17.enum(["success", "failure", "skipped", "no-issues"]),
11270
+ findings: z17.number(),
11271
+ fixed: z17.number(),
11272
+ prUrl: z17.string().nullable(),
11273
+ prUpdated: z17.boolean(),
11274
+ error: z17.string().optional()
10741
11275
  });
10742
11276
  var MAX_HISTORY = 500;
10743
11277
  var fallbackLogger = {
@@ -10761,10 +11295,10 @@ var MaintenanceReporter = class {
10761
11295
  */
10762
11296
  async load() {
10763
11297
  try {
10764
- await fs15.promises.mkdir(this.persistDir, { recursive: true });
10765
- const filePath = path16.join(this.persistDir, "history.json");
10766
- const data = await fs15.promises.readFile(filePath, "utf-8");
10767
- const parsed = z16.array(RunResultSchema).safeParse(JSON.parse(data));
11298
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11299
+ const filePath = path18.join(this.persistDir, "history.json");
11300
+ const data = await fs16.promises.readFile(filePath, "utf-8");
11301
+ const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
10768
11302
  if (parsed.success) {
10769
11303
  this.history = parsed.data.slice(0, MAX_HISTORY);
10770
11304
  }
@@ -10797,9 +11331,9 @@ var MaintenanceReporter = class {
10797
11331
  */
10798
11332
  async persist() {
10799
11333
  try {
10800
- await fs15.promises.mkdir(this.persistDir, { recursive: true });
10801
- const filePath = path16.join(this.persistDir, "history.json");
10802
- await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11334
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11335
+ const filePath = path18.join(this.persistDir, "history.json");
11336
+ await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10803
11337
  } catch (err) {
10804
11338
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
10805
11339
  }
@@ -11286,7 +11820,7 @@ function parseStatusLine(output) {
11286
11820
  // src/maintenance/check-script-runner.ts
11287
11821
  import { execFile as execFile6 } from "child_process";
11288
11822
  import { promisify as promisify3 } from "util";
11289
- import * as path17 from "path";
11823
+ import * as path19 from "path";
11290
11824
  var execFileAsync = promisify3(execFile6);
11291
11825
  var CheckScriptRunner = class {
11292
11826
  constructor(cwd) {
@@ -11305,7 +11839,7 @@ var CheckScriptRunner = class {
11305
11839
  }
11306
11840
  };
11307
11841
  async function captureScript(spec, projectRoot) {
11308
- const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11842
+ const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
11309
11843
  const args = spec.args ?? [];
11310
11844
  const timeoutMs = spec.timeoutMs ?? 12e4;
11311
11845
  try {
@@ -11395,8 +11929,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
11395
11929
  }
11396
11930
 
11397
11931
  // src/maintenance/output-store.ts
11398
- import * as fs16 from "fs";
11399
- import * as path18 from "path";
11932
+ import * as fs17 from "fs";
11933
+ import * as path20 from "path";
11400
11934
  var DEFAULT_RETENTION = {
11401
11935
  runs: 50,
11402
11936
  maxAgeDays: 30
@@ -11436,13 +11970,13 @@ var TaskOutputStore = class {
11436
11970
  async write(taskId, entry, retention) {
11437
11971
  this.ensureSafeTaskId(taskId);
11438
11972
  const dir = this.dirFor(taskId);
11439
- await fs16.promises.mkdir(dir, { recursive: true });
11973
+ await fs17.promises.mkdir(dir, { recursive: true });
11440
11974
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11441
- const filePath = path18.join(dir, fileName);
11975
+ const filePath = path20.join(dir, fileName);
11442
11976
  const tmpPath = `${filePath}.tmp`;
11443
11977
  const payload = JSON.stringify(entry, null, 2);
11444
- await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11445
- await fs16.promises.rename(tmpPath, filePath);
11978
+ await fs17.promises.writeFile(tmpPath, payload, "utf-8");
11979
+ await fs17.promises.rename(tmpPath, filePath);
11446
11980
  try {
11447
11981
  await this.applyRetention(taskId, retention);
11448
11982
  } catch (err) {
@@ -11466,7 +12000,7 @@ var TaskOutputStore = class {
11466
12000
  const slice = fileNames.slice(offset, offset + limit);
11467
12001
  const out = [];
11468
12002
  for (const name of slice) {
11469
- const entry = await this.readEntry(path18.join(dir, name));
12003
+ const entry = await this.readEntry(path20.join(dir, name));
11470
12004
  if (entry) out.push(entry);
11471
12005
  }
11472
12006
  return out;
@@ -11482,18 +12016,18 @@ var TaskOutputStore = class {
11482
12016
  }
11483
12017
  const dir = this.dirFor(taskId);
11484
12018
  const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11485
- return this.readEntry(path18.join(dir, fileName));
12019
+ return this.readEntry(path20.join(dir, fileName));
11486
12020
  }
11487
12021
  /**
11488
12022
  * The on-disk root for a given task. Exposed for tooling that needs to walk
11489
12023
  * outputs from outside the store API.
11490
12024
  */
11491
12025
  dirFor(taskId) {
11492
- return path18.join(this.rootDir, taskId, "outputs");
12026
+ return path20.join(this.rootDir, taskId, "outputs");
11493
12027
  }
11494
12028
  async readEntry(filePath) {
11495
12029
  try {
11496
- const buf = await fs16.promises.readFile(filePath, "utf-8");
12030
+ const buf = await fs17.promises.readFile(filePath, "utf-8");
11497
12031
  const parsed = JSON.parse(buf);
11498
12032
  return parsed;
11499
12033
  } catch {
@@ -11515,7 +12049,7 @@ var TaskOutputStore = class {
11515
12049
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11516
12050
  for (const name of toRemove) {
11517
12051
  try {
11518
- await fs16.promises.unlink(path18.join(dir, name));
12052
+ await fs17.promises.unlink(path20.join(dir, name));
11519
12053
  } catch {
11520
12054
  }
11521
12055
  }
@@ -11524,7 +12058,7 @@ var TaskOutputStore = class {
11524
12058
  async function listJsonFilesDescending(dir) {
11525
12059
  let names;
11526
12060
  try {
11527
- names = await fs16.promises.readdir(dir);
12061
+ names = await fs17.promises.readdir(dir);
11528
12062
  } catch {
11529
12063
  return [];
11530
12064
  }
@@ -11698,8 +12232,8 @@ function validateCheckShape(prefix, task, errors) {
11698
12232
  });
11699
12233
  }
11700
12234
  if (hasScript) {
11701
- const path22 = task.checkScript?.path;
11702
- if (!path22 || path22.trim().length === 0) {
12235
+ const path24 = task.checkScript?.path;
12236
+ if (!path24 || path24.trim().length === 0) {
11703
12237
  errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
11704
12238
  }
11705
12239
  if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
@@ -11825,9 +12359,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
11825
12359
  stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11826
12360
  }
11827
12361
  }
11828
- function reportCycle(path22, next, errors, reported) {
11829
- const cycleStart = path22.indexOf(next);
11830
- const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
12362
+ function reportCycle(path24, next, errors, reported) {
12363
+ const cycleStart = path24.indexOf(next);
12364
+ const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
11831
12365
  const key = cyclePath.join("\u2192");
11832
12366
  if (reported.has(key)) return;
11833
12367
  reported.add(key);
@@ -11838,11 +12372,6 @@ function reportCycle(path22, next, errors, reported) {
11838
12372
  }
11839
12373
 
11840
12374
  // src/orchestrator.ts
11841
- function useCaseForBackendParam(issue, backendParam) {
11842
- if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
11843
- const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
11844
- return { kind: "tier", tier };
11845
- }
11846
12375
  var Orchestrator = class extends EventEmitter {
11847
12376
  state;
11848
12377
  config;
@@ -11867,6 +12396,14 @@ var Orchestrator = class extends EventEmitter {
11867
12396
  * construction time. Eliminating this fallback is autopilot Phase 4+.
11868
12397
  */
11869
12398
  backendFactory;
12399
+ /**
12400
+ * Spec B Phase 4 (D8): per-orchestrator in-process bus for
12401
+ * `RoutingDecision` events. Constructed alongside backendFactory when
12402
+ * agent.backends synthesis succeeds; null when legacy single-backend
12403
+ * config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
12404
+ * subscribe via `getRoutingDecisionBus()`.
12405
+ */
12406
+ routingDecisionBus;
11870
12407
  /**
11871
12408
  * Test-only: when overrides.backend is provided, dispatch uses this
11872
12409
  * instance directly (bypassing the factory). Mirrors Phase 1
@@ -11889,6 +12426,15 @@ var Orchestrator = class extends EventEmitter {
11889
12426
  * so this map is the single source of truth post-migration.
11890
12427
  */
11891
12428
  localResolvers = /* @__PURE__ */ new Map();
12429
+ /**
12430
+ * Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
12431
+ * construction from `projectRoot/agents/skills/`. Consulted by
12432
+ * `buildRoutingUseCase` at dispatch start to construct
12433
+ * `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
12434
+ * Empty when the orchestrator runs outside a harness project root
12435
+ * (then dispatch falls through to per-tier, preserving F11/N2).
12436
+ */
12437
+ skillCatalog;
11892
12438
  /**
11893
12439
  * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
11894
12440
  * (SC39): each local/pi resolver gets its own listener emitting a
@@ -11937,7 +12483,7 @@ var Orchestrator = class extends EventEmitter {
11937
12483
  completionHandler;
11938
12484
  /** Project root directory, derived from workspace root. */
11939
12485
  get projectRoot() {
11940
- return path19.resolve(this.config.workspace.root, "..", "..");
12486
+ return path21.resolve(this.config.workspace.root, "..", "..");
11941
12487
  }
11942
12488
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
11943
12489
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -11981,6 +12527,13 @@ var Orchestrator = class extends EventEmitter {
11981
12527
  `migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
11982
12528
  );
11983
12529
  }
12530
+ const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
12531
+ this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
12532
+ if (this.skillCatalog.length === 0) {
12533
+ this.logger.warn(
12534
+ `Spec B Phase 3: skill catalog discovery returned 0 entries; per-skill / per-mode routing will fall through to per-tier. Looked under ${path21.join(skillCatalogRoot, "agents/skills")}.`
12535
+ );
12536
+ }
11984
12537
  this.tracker = overrides?.tracker || this.createTracker();
11985
12538
  this.workspace = new WorkspaceManager(config.workspace, {
11986
12539
  emitEvent: (event) => {
@@ -11992,10 +12545,10 @@ var Orchestrator = class extends EventEmitter {
11992
12545
  this.renderer = new PromptRenderer();
11993
12546
  this.overrideBackend = overrides?.backend ?? null;
11994
12547
  this.interactionQueue = new InteractionQueue(
11995
- path19.join(config.workspace.root, "..", "interactions"),
12548
+ path21.join(config.workspace.root, "..", "interactions"),
11996
12549
  this
11997
12550
  );
11998
- this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
12551
+ this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
11999
12552
  const backendsMap = this.config.agent.backends ?? {};
12000
12553
  for (const [name, def] of Object.entries(backendsMap)) {
12001
12554
  if (def.type === "local" || def.type === "pi") {
@@ -12016,6 +12569,10 @@ var Orchestrator = class extends EventEmitter {
12016
12569
  const routing = this.config.agent.routing ?? {
12017
12570
  default: firstBackendName ?? "primary"
12018
12571
  };
12572
+ this.routingDecisionBus = new RoutingDecisionBus({
12573
+ capacity: 500,
12574
+ logger: this.logger
12575
+ });
12019
12576
  this.backendFactory = new OrchestratorBackendFactory({
12020
12577
  backends: this.config.agent.backends,
12021
12578
  routing,
@@ -12023,6 +12580,7 @@ var Orchestrator = class extends EventEmitter {
12023
12580
  ...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
12024
12581
  ...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
12025
12582
  cacheMetrics: this.cacheMetrics,
12583
+ decisionBus: this.routingDecisionBus,
12026
12584
  getResolverModelFor: (name) => {
12027
12585
  const resolver = this.localResolvers.get(name);
12028
12586
  return resolver ? () => resolver.resolveModel() : void 0;
@@ -12030,6 +12588,7 @@ var Orchestrator = class extends EventEmitter {
12030
12588
  });
12031
12589
  } else {
12032
12590
  this.backendFactory = null;
12591
+ this.routingDecisionBus = null;
12033
12592
  }
12034
12593
  this.pipeline = null;
12035
12594
  this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
@@ -12039,7 +12598,7 @@ var Orchestrator = class extends EventEmitter {
12039
12598
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
12040
12599
  });
12041
12600
  this.recorder = new StreamRecorder(
12042
- path19.resolve(config.workspace.root, "..", "streams"),
12601
+ path21.resolve(config.workspace.root, "..", "streams"),
12043
12602
  this.logger
12044
12603
  );
12045
12604
  const self = this;
@@ -12070,10 +12629,10 @@ var Orchestrator = class extends EventEmitter {
12070
12629
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
12071
12630
  if (config.server?.port) {
12072
12631
  const webhookStore = new WebhookStore(
12073
- path19.join(this.projectRoot, ".harness", "webhooks.json")
12632
+ path21.join(this.projectRoot, ".harness", "webhooks.json")
12074
12633
  );
12075
12634
  this.webhookQueue = new WebhookQueue(
12076
- path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12635
+ path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12077
12636
  );
12078
12637
  const webhookDelivery = new WebhookDelivery({
12079
12638
  queue: this.webhookQueue,
@@ -12111,7 +12670,16 @@ var Orchestrator = class extends EventEmitter {
12111
12670
  queue: this.webhookQueue
12112
12671
  },
12113
12672
  cacheMetrics: this.cacheMetrics,
12114
- plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
12673
+ // Spec B Phase 5: routing observability accessors. Closures so the
12674
+ // server re-reads on every request — stop() / start() do not
12675
+ // require server reconstruction. Returns null if no backendFactory
12676
+ // (legacy single-backend configs), and the route handler renders
12677
+ // 503 in that case.
12678
+ getBackendRouter: () => this.getBackendRouter(),
12679
+ getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
12680
+ getRoutingConfig: () => this.getRoutingConfig(),
12681
+ getBackends: () => this.getBackends(),
12682
+ plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
12115
12683
  pipeline: this.pipeline,
12116
12684
  analysisArchive: this.analysisArchive,
12117
12685
  roadmapPath: config.tracker.filePath ?? null,
@@ -12221,7 +12789,7 @@ var Orchestrator = class extends EventEmitter {
12221
12789
  }
12222
12790
  };
12223
12791
  const outputStore = new TaskOutputStore({
12224
- rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12792
+ rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12225
12793
  logger: this.logger
12226
12794
  });
12227
12795
  const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
@@ -12262,7 +12830,7 @@ var Orchestrator = class extends EventEmitter {
12262
12830
  ${messages}`);
12263
12831
  }
12264
12832
  this.maintenanceReporter = new MaintenanceReporter({
12265
- persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12833
+ persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12266
12834
  logger: this.logger
12267
12835
  });
12268
12836
  await this.maintenanceReporter.load();
@@ -12313,10 +12881,17 @@ ${messages}`);
12313
12881
  }
12314
12882
  }
12315
12883
  createIntelligencePipeline() {
12884
+ if (!this.backendFactory) {
12885
+ this.logger.warn(
12886
+ "intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
12887
+ );
12888
+ return null;
12889
+ }
12316
12890
  const bundle = buildIntelligencePipeline({
12317
12891
  config: this.config,
12318
12892
  localResolvers: this.localResolvers,
12319
- logger: this.logger
12893
+ logger: this.logger,
12894
+ router: this.backendFactory.getRouter()
12320
12895
  });
12321
12896
  if (!bundle) return null;
12322
12897
  this.graphStore = bundle.graphStore;
@@ -12367,11 +12942,13 @@ ${messages}`);
12367
12942
  simulationResults,
12368
12943
  personaRecommendations
12369
12944
  } = pipelineResult ?? {};
12945
+ const selfAssignee = await this.orchestratorIdPromise;
12370
12946
  const tickEvent = {
12371
12947
  type: "tick",
12372
12948
  candidates,
12373
12949
  runningStates: runningStatesResult.value,
12374
12950
  nowMs,
12951
+ selfAssignee,
12375
12952
  ...concernSignals !== void 0 && { concernSignals },
12376
12953
  ...enrichedSpecs !== void 0 && { enrichedSpecs },
12377
12954
  ...complexityScores !== void 0 && { complexityScores },
@@ -12795,14 +13372,24 @@ ${messages}`);
12795
13372
  issue,
12796
13373
  attempt: attempt || 1
12797
13374
  });
12798
- const useCase = useCaseForBackendParam(issue, backend);
13375
+ const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
13376
+ const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
13377
+ const routerOpts = invocationOverride ? { invocationOverride } : void 0;
13378
+ if (invocationOverride) {
13379
+ this.logger.info(
13380
+ `Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
13381
+ { issueId: issue.id }
13382
+ );
13383
+ }
12799
13384
  let routedBackendName;
12800
13385
  if (this.overrideBackend !== null) {
12801
13386
  routedBackendName = this.overrideBackend.name;
12802
13387
  } else if (this.backendFactory !== null) {
12803
- routedBackendName = this.backendFactory.resolveName(useCase);
13388
+ routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
12804
13389
  } else {
12805
- routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
13390
+ const routingDefault = this.config.agent.routing?.default;
13391
+ const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
13392
+ routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
12806
13393
  }
12807
13394
  const session = {
12808
13395
  sessionId: `pending-${Date.now()}`,
@@ -12841,7 +13428,7 @@ ${messages}`);
12841
13428
  if (this.overrideBackend !== null) {
12842
13429
  agentBackend = this.overrideBackend;
12843
13430
  } else if (this.backendFactory !== null) {
12844
- agentBackend = this.backendFactory.forUseCase(useCase);
13431
+ agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
12845
13432
  } else {
12846
13433
  throw new Error(
12847
13434
  `Cannot dispatch ${issue.identifier}: agent.backends not synthesized (migration failed) and no override backend supplied. Migrate to agent.backends/agent.routing per docs/guides/multi-backend-routing.md.`
@@ -13131,6 +13718,8 @@ ${messages}`);
13131
13718
  unsub();
13132
13719
  }
13133
13720
  this.localModelStatusUnsubscribes = [];
13721
+ this.routingDecisionBus?.clearListeners();
13722
+ this.routingDecisionBus = null;
13134
13723
  for (const resolver of this.localResolvers.values()) {
13135
13724
  resolver.stop();
13136
13725
  }
@@ -13204,6 +13793,42 @@ ${messages}`);
13204
13793
  tickActivity: this.tickActivity
13205
13794
  };
13206
13795
  }
13796
+ /**
13797
+ * Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
13798
+ * Phase 7 (dashboard WS broadcast). Returns null when the legacy
13799
+ * single-backend config bypassed agent.backends synthesis.
13800
+ */
13801
+ getRoutingDecisionBus() {
13802
+ return this.routingDecisionBus;
13803
+ }
13804
+ /**
13805
+ * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
13806
+ * dispatch path uses the factory-owned router directly; observability
13807
+ * routes (config / decisions) reach it through this accessor. Returns
13808
+ * null when the legacy single-backend config bypassed agent.backends
13809
+ * synthesis (no backendFactory built).
13810
+ */
13811
+ getBackendRouter() {
13812
+ return this.backendFactory?.getRouter() ?? null;
13813
+ }
13814
+ /**
13815
+ * Spec B Phase 5: snapshot of the active RoutingConfig for the config
13816
+ * route and the trace route's bus-less router construction. Returns
13817
+ * null when the operator's harness.config.json carries no
13818
+ * `agent.routing` block.
13819
+ */
13820
+ getRoutingConfig() {
13821
+ return this.config.agent.routing ?? null;
13822
+ }
13823
+ /**
13824
+ * Spec B Phase 5: snapshot of `agent.backends` for the config route
13825
+ * (existence annotations) and the trace route (bus-less router
13826
+ * construction). Returns null when no synthesized backends map exists
13827
+ * (legacy single-backend configs).
13828
+ */
13829
+ getBackends() {
13830
+ return this.config.agent.backends ?? null;
13831
+ }
13207
13832
  /** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
13208
13833
  getMaintenanceStatus() {
13209
13834
  return this.maintenanceScheduler?.getStatus() ?? null;
@@ -13582,8 +14207,8 @@ async function syncMain(repoRoot, opts = {}) {
13582
14207
  }
13583
14208
 
13584
14209
  // src/sessions/search-index.ts
13585
- import * as fs17 from "fs";
13586
- import * as path20 from "path";
14210
+ import * as fs18 from "fs";
14211
+ import * as path22 from "path";
13587
14212
  import Database2 from "better-sqlite3";
13588
14213
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
13589
14214
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -13628,7 +14253,7 @@ function normalizeFts5Query(query) {
13628
14253
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
13629
14254
  }
13630
14255
  function searchIndexPath(projectPath) {
13631
- return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
14256
+ return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13632
14257
  }
13633
14258
  var FILE_KIND_TO_FILENAME = {
13634
14259
  summary: "summary.md",
@@ -13643,7 +14268,7 @@ var SqliteSearchIndex = class {
13643
14268
  removeSessionStmt;
13644
14269
  totalStmt;
13645
14270
  constructor(dbPath) {
13646
- fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
14271
+ fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
13647
14272
  this.db = new Database2(dbPath);
13648
14273
  this.db.pragma("journal_mode = WAL");
13649
14274
  this.db.pragma("synchronous = NORMAL");
@@ -13748,14 +14373,14 @@ function indexSessionDirectory(idx, args) {
13748
14373
  let docsWritten = 0;
13749
14374
  for (const kind of kinds) {
13750
14375
  const fileName = FILE_KIND_TO_FILENAME[kind];
13751
- const filePath = path20.join(args.sessionDir, fileName);
13752
- if (!fs17.existsSync(filePath)) continue;
13753
- let body = fs17.readFileSync(filePath, "utf8");
14376
+ const filePath = path22.join(args.sessionDir, fileName);
14377
+ if (!fs18.existsSync(filePath)) continue;
14378
+ let body = fs18.readFileSync(filePath, "utf8");
13754
14379
  if (Buffer.byteLength(body, "utf8") > cap) {
13755
14380
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
13756
14381
  }
13757
- const stat = fs17.statSync(filePath);
13758
- const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
14382
+ const stat = fs18.statSync(filePath);
14383
+ const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
13759
14384
  idx.upsertSessionDoc({
13760
14385
  sessionId: args.sessionId,
13761
14386
  archived: args.archived,
@@ -13770,17 +14395,17 @@ function indexSessionDirectory(idx, args) {
13770
14395
  }
13771
14396
  function reindexFromArchive(projectPath, opts = {}) {
13772
14397
  const start = Date.now();
13773
- const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
14398
+ const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
13774
14399
  const idx = openSearchIndex(projectPath);
13775
14400
  try {
13776
14401
  idx.resetArchived();
13777
14402
  let sessionsIndexed = 0;
13778
14403
  let docsWritten = 0;
13779
- if (fs17.existsSync(archiveBase)) {
13780
- const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
14404
+ if (fs18.existsSync(archiveBase)) {
14405
+ const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
13781
14406
  for (const entry of entries) {
13782
14407
  if (!entry.isDirectory()) continue;
13783
- const sessionDir = path20.join(archiveBase, entry.name);
14408
+ const sessionDir = path22.join(archiveBase, entry.name);
13784
14409
  const result = indexSessionDirectory(idx, {
13785
14410
  sessionId: entry.name,
13786
14411
  sessionDir,
@@ -13800,8 +14425,8 @@ function reindexFromArchive(projectPath, opts = {}) {
13800
14425
  }
13801
14426
 
13802
14427
  // src/sessions/summarize.ts
13803
- import * as fs18 from "fs";
13804
- import * as path21 from "path";
14428
+ import * as fs19 from "fs";
14429
+ import * as path23 from "path";
13805
14430
  import {
13806
14431
  SessionSummarySchema
13807
14432
  } from "@harness-engineering/types";
@@ -13831,10 +14456,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
13831
14456
  function readInputCorpus(archiveDir) {
13832
14457
  const parts = [];
13833
14458
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
13834
- const p = path21.join(archiveDir, filename);
13835
- if (!fs18.existsSync(p)) continue;
14459
+ const p = path23.join(archiveDir, filename);
14460
+ if (!fs19.existsSync(p)) continue;
13836
14461
  try {
13837
- const content = fs18.readFileSync(p, "utf8");
14462
+ const content = fs19.readFileSync(p, "utf8");
13838
14463
  if (content.trim().length === 0) continue;
13839
14464
  parts.push(`## FILE: ${kind}
13840
14465
 
@@ -13885,7 +14510,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
13885
14510
  return lines.join("\n");
13886
14511
  }
13887
14512
  function writeStubMarkdown(archiveDir, reason) {
13888
- const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
14513
+ const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
13889
14514
  const body = `---
13890
14515
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
13891
14516
  schemaVersion: 1
@@ -13896,12 +14521,12 @@ status: failed
13896
14521
 
13897
14522
  - reason: ${reason}
13898
14523
  `;
13899
- fs18.writeFileSync(filePath, body, "utf8");
14524
+ fs19.writeFileSync(filePath, body, "utf8");
13900
14525
  return filePath;
13901
14526
  }
13902
14527
  async function summarizeArchivedSession(ctx) {
13903
14528
  const writeStubOnError = ctx.writeStubOnError ?? true;
13904
- if (!fs18.existsSync(ctx.archiveDir)) {
14529
+ if (!fs19.existsSync(ctx.archiveDir)) {
13905
14530
  return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
13906
14531
  }
13907
14532
  const corpus = readInputCorpus(ctx.archiveDir);
@@ -13962,9 +14587,9 @@ async function summarizeArchivedSession(ctx) {
13962
14587
  outputTokens: response.tokenUsage.outputTokens,
13963
14588
  schemaVersion: 1
13964
14589
  };
13965
- const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14590
+ const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13966
14591
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
13967
- fs18.writeFileSync(filePath, body, "utf8");
14592
+ fs19.writeFileSync(filePath, body, "utf8");
13968
14593
  return Ok24({ summary: parsed.data, meta, filePath });
13969
14594
  }
13970
14595
  function isSummaryEnabled(config) {
@@ -14077,7 +14702,10 @@ export {
14077
14702
  computeRateLimitDelay,
14078
14703
  createBackend,
14079
14704
  createEmptyState,
14705
+ crossFieldRoutingIssues,
14080
14706
  detectScopeTier,
14707
+ discoverSkillCatalog,
14708
+ discoverSkillCatalogNames,
14081
14709
  emitProposalApproved,
14082
14710
  emitProposalCreated,
14083
14711
  emitProposalRejected,
@@ -14103,6 +14731,7 @@ export {
14103
14731
  resolveEscalationConfig,
14104
14732
  resolveOrchestratorId,
14105
14733
  routeIssue,
14734
+ routingWarnings,
14106
14735
  runGate,
14107
14736
  savePublishedIndex,
14108
14737
  searchIndexPath,