@harness-engineering/orchestrator 0.6.0 → 0.8.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) {
@@ -3431,6 +3544,14 @@ var DEFAULT_PROBE_INTERVAL_MS = 3e4;
3431
3544
  var MIN_PROBE_INTERVAL_MS = 1e3;
3432
3545
  var DEFAULT_API_KEY = "lm-studio";
3433
3546
  var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
3547
+ function normalizeLocalModel(input) {
3548
+ if (input === void 0) return [];
3549
+ if (typeof input === "string") return [input];
3550
+ if (input.length === 0) {
3551
+ throw new Error("localModel array must be non-empty when provided");
3552
+ }
3553
+ return [...input];
3554
+ }
3434
3555
  var noopLogger = {
3435
3556
  info: () => void 0,
3436
3557
  warn: () => void 0
@@ -3635,11 +3756,11 @@ function detectLegacyFields(agent) {
3635
3756
  }
3636
3757
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3637
3758
  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;
3759
+ for (const path24 of presentLegacy) {
3760
+ if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
3761
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
3641
3762
  warnings.push(
3642
- `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3763
+ `Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3643
3764
  );
3644
3765
  }
3645
3766
  return warnings;
@@ -3667,7 +3788,7 @@ function migrateAgentConfig(agent) {
3667
3788
  }
3668
3789
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3669
3790
  const warnings = presentLegacy.map(
3670
- (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3791
+ (path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3671
3792
  );
3672
3793
  return {
3673
3794
  config: { ...agent, backends, routing },
@@ -3730,61 +3851,160 @@ function synthesizeLocal(agent) {
3730
3851
  }
3731
3852
 
3732
3853
  // src/agent/backend-router.ts
3854
+ function toArray(value) {
3855
+ return Array.isArray(value) ? value : [value];
3856
+ }
3733
3857
  var BackendRouter = class {
3734
3858
  backends;
3735
3859
  routing;
3860
+ decisionBus;
3736
3861
  constructor(opts) {
3737
3862
  this.backends = opts.backends;
3738
3863
  this.routing = opts.routing;
3864
+ this.decisionBus = opts.decisionBus;
3739
3865
  this.validateReferences();
3740
3866
  }
3741
3867
  /**
3742
- * Returns the backend name for a given use case.
3868
+ * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
3743
3869
  *
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`.
3870
+ * @param useCase the routing query
3871
+ * @param opts.invocationOverride if set and the named backend exists,
3872
+ * beats all other sources (D7 — the `--backend <name>` escape hatch)
3873
+ */
3874
+ resolve(useCase, opts) {
3875
+ const startedAt = performance.now();
3876
+ const path24 = [];
3877
+ const tryChain = (source, value) => {
3878
+ if (value === void 0) return void 0;
3879
+ for (const name of toArray(value)) {
3880
+ const step = { source, candidate: name, outcome: "considered" };
3881
+ path24.push(step);
3882
+ if (this.backends[name]) {
3883
+ step.outcome = "chosen";
3884
+ return name;
3885
+ }
3886
+ step.outcome = "unknown-backend";
3887
+ }
3888
+ return void 0;
3889
+ };
3890
+ const decide = (backendName) => {
3891
+ const def = this.backends[backendName];
3892
+ if (!def) {
3893
+ throw new Error(
3894
+ `BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
3895
+ );
3896
+ }
3897
+ return {
3898
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3899
+ useCase,
3900
+ resolutionPath: path24,
3901
+ backendName,
3902
+ backendType: def.type,
3903
+ durationMs: performance.now() - startedAt
3904
+ };
3905
+ };
3906
+ const emitAndReturn = (decision) => {
3907
+ this.decisionBus?.emit(decision);
3908
+ return decision;
3909
+ };
3910
+ const fromInvocation = tryChain(
3911
+ "invocation",
3912
+ opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
3913
+ );
3914
+ if (fromInvocation) return emitAndReturn(decide(fromInvocation));
3915
+ if (useCase.kind === "skill") {
3916
+ const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
3917
+ if (fromSkill) return emitAndReturn(decide(fromSkill));
3918
+ }
3919
+ const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
3920
+ if (mode !== void 0) {
3921
+ const fromMode = tryChain("mode", this.routing.modes?.[mode]);
3922
+ if (fromMode) return emitAndReturn(decide(fromMode));
3923
+ }
3924
+ const fromExisting = this.resolveExistingUseCase(useCase);
3925
+ if (fromExisting !== void 0) {
3926
+ const chained = tryChain("tier", fromExisting);
3927
+ if (chained) return emitAndReturn(decide(chained));
3928
+ }
3929
+ const fromDefault = tryChain("default", this.routing.default);
3930
+ if (fromDefault) return emitAndReturn(decide(fromDefault));
3931
+ const knownList = Object.keys(this.backends).join(", ") || "(none)";
3932
+ throw new Error(
3933
+ `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
3934
+ );
3935
+ }
3936
+ /**
3937
+ * Returns the {@link BackendDef} reference for the resolved name.
3938
+ * Identity-equal to the entry in `backends` (no copy) so callers
3939
+ * relying on reference equality (SC21) continue to work.
3748
3940
  */
3749
- resolve(useCase) {
3941
+ resolveDefinition(useCase, opts) {
3942
+ const decision = this.resolve(useCase, opts);
3943
+ const def = this.backends[decision.backendName];
3944
+ if (!def) {
3945
+ throw new Error(
3946
+ `BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3947
+ );
3948
+ }
3949
+ return def;
3950
+ }
3951
+ /**
3952
+ * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
3953
+ * for callers that need both. Replaces the previous pattern of
3954
+ * `resolveDefinition(useCase) + resolve(useCase)` which produced two
3955
+ * RoutingDecision emissions per dispatch — doubling routing-decision
3956
+ * log volume now that Phase 4 emits.
3957
+ *
3958
+ * Identity-equal `BackendDef` (no copy) so callers relying on
3959
+ * reference equality (SC21) continue to work.
3960
+ */
3961
+ resolveDecisionAndDef(useCase, opts) {
3962
+ const decision = this.resolve(useCase, opts);
3963
+ const def = this.backends[decision.backendName];
3964
+ if (!def) {
3965
+ throw new Error(
3966
+ `BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3967
+ );
3968
+ }
3969
+ return { decision, def };
3970
+ }
3971
+ /**
3972
+ * The pre-Spec-B resolution helper: returns the configured
3973
+ * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
3974
+ * use cases (or `undefined` for skill/mode use cases, which are owned
3975
+ * by the per-skill / per-mode steps in {@link resolve}). Returning
3976
+ * `undefined` lets the caller fall through to `routing.default`.
3977
+ */
3978
+ resolveExistingUseCase(useCase) {
3750
3979
  switch (useCase.kind) {
3751
3980
  case "tier": {
3752
- const named = this.routing[useCase.tier];
3753
- return named ?? this.routing.default;
3981
+ const tierMap = this.routing;
3982
+ return tierMap[useCase.tier];
3754
3983
  }
3755
3984
  case "intelligence": {
3756
3985
  const intel = this.routing.intelligence;
3757
- return intel?.[useCase.layer] ?? this.routing.default;
3986
+ return intel?.[useCase.layer];
3758
3987
  }
3759
3988
  case "isolation": {
3760
3989
  const iso = this.routing.isolation;
3761
- return iso?.[useCase.tier] ?? this.routing.default;
3990
+ return iso?.[useCase.tier];
3762
3991
  }
3763
3992
  case "maintenance":
3764
3993
  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
- );
3994
+ return void 0;
3995
+ case "skill":
3996
+ case "mode":
3997
+ return void 0;
3780
3998
  }
3781
- return def;
3782
3999
  }
3783
4000
  validateReferences() {
3784
4001
  const known = new Set(Object.keys(this.backends));
3785
4002
  const missing = [];
3786
- const check = (path22, name) => {
3787
- if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
4003
+ const check = (label, value) => {
4004
+ if (value === void 0) return;
4005
+ for (const name of toArray(value)) {
4006
+ if (!known.has(name)) missing.push({ path: label, name });
4007
+ }
3788
4008
  };
3789
4009
  check("default", this.routing.default);
3790
4010
  check("quick-fix", this.routing["quick-fix"]);
@@ -3796,8 +4016,14 @@ var BackendRouter = class {
3796
4016
  check("isolation.none", this.routing.isolation?.none);
3797
4017
  check("isolation.container", this.routing.isolation?.container);
3798
4018
  check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
4019
+ for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
4020
+ check(`skills.${skill}`, value);
4021
+ }
4022
+ for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
4023
+ check(`modes.${mode}`, value);
4024
+ }
3799
4025
  if (missing.length > 0) {
3800
- const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
4026
+ const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
3801
4027
  const known_ = [...known].join(", ") || "(none)";
3802
4028
  throw new Error(
3803
4029
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3814,11 +4040,11 @@ import {
3814
4040
  Ok as Ok10,
3815
4041
  Err as Err7
3816
4042
  } from "@harness-engineering/types";
3817
- function resolveExitCode(code, command, resolve7) {
4043
+ function resolveExitCode(code, command, resolve8) {
3818
4044
  if (code === 0) {
3819
- resolve7(Ok10(void 0));
4045
+ resolve8(Ok10(void 0));
3820
4046
  } else {
3821
- resolve7(
4047
+ resolve8(
3822
4048
  Err7({
3823
4049
  category: "agent_not_found",
3824
4050
  message: `Claude command '${command}' not found or failed`
@@ -3826,8 +4052,8 @@ function resolveExitCode(code, command, resolve7) {
3826
4052
  );
3827
4053
  }
3828
4054
  }
3829
- function resolveSpawnError(command, resolve7) {
3830
- resolve7(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4055
+ function resolveSpawnError(command, resolve8) {
4056
+ resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3831
4057
  }
3832
4058
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3833
4059
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4140,10 +4366,10 @@ var ClaudeBackend = class {
4140
4366
  errRl.close();
4141
4367
  }
4142
4368
  if (exitCode === null) {
4143
- await new Promise((resolve7) => {
4369
+ await new Promise((resolve8) => {
4144
4370
  child.on("exit", (code) => {
4145
4371
  exitCode = code;
4146
- resolve7(null);
4372
+ resolve8(null);
4147
4373
  });
4148
4374
  });
4149
4375
  }
@@ -4165,10 +4391,10 @@ var ClaudeBackend = class {
4165
4391
  return Ok10(void 0);
4166
4392
  }
4167
4393
  async healthCheck() {
4168
- return new Promise((resolve7) => {
4394
+ return new Promise((resolve8) => {
4169
4395
  const child = spawn2(this.command, ["--version"]);
4170
- child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
4171
- child.on("error", () => resolveSpawnError(this.command, resolve7));
4396
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
4397
+ child.on("error", () => resolveSpawnError(this.command, resolve8));
4172
4398
  });
4173
4399
  }
4174
4400
  };
@@ -5100,14 +5326,14 @@ var SshBackend = class {
5100
5326
  async healthCheck() {
5101
5327
  const args = [...this.buildSshArgs()];
5102
5328
  args[args.length - 1] = "true";
5103
- return new Promise((resolve7) => {
5329
+ return new Promise((resolve8) => {
5104
5330
  let child;
5105
5331
  try {
5106
5332
  child = this.spawnImpl(this.config.sshBinary, args, {
5107
5333
  stdio: ["ignore", "ignore", "pipe"]
5108
5334
  });
5109
5335
  } catch (err) {
5110
- resolve7(
5336
+ resolve8(
5111
5337
  Err13({
5112
5338
  category: "agent_not_found",
5113
5339
  message: err instanceof Error ? err.message : "failed to spawn ssh"
@@ -5128,9 +5354,9 @@ var SshBackend = class {
5128
5354
  child.on("close", (code) => {
5129
5355
  clearTimeout(timer);
5130
5356
  if (code === 0) {
5131
- resolve7(Ok16(void 0));
5357
+ resolve8(Ok16(void 0));
5132
5358
  } else {
5133
- resolve7(
5359
+ resolve8(
5134
5360
  Err13({
5135
5361
  category: "agent_not_found",
5136
5362
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
@@ -5140,7 +5366,7 @@ var SshBackend = class {
5140
5366
  });
5141
5367
  child.on("error", (err) => {
5142
5368
  clearTimeout(timer);
5143
- resolve7(Err13({ category: "agent_not_found", message: err.message }));
5369
+ resolve8(Err13({ category: "agent_not_found", message: err.message }));
5144
5370
  });
5145
5371
  });
5146
5372
  }
@@ -5188,13 +5414,13 @@ async function* readLines(stream) {
5188
5414
  if (buffer.length > 0) yield buffer;
5189
5415
  }
5190
5416
  function waitForExit(child) {
5191
- return new Promise((resolve7) => {
5417
+ return new Promise((resolve8) => {
5192
5418
  if (child.exitCode !== null) {
5193
- resolve7(child.exitCode);
5419
+ resolve8(child.exitCode);
5194
5420
  return;
5195
5421
  }
5196
- child.once("close", (code) => resolve7(code));
5197
- child.once("error", () => resolve7(null));
5422
+ child.once("close", (code) => resolve8(code));
5423
+ child.once("error", () => resolve8(null));
5198
5424
  });
5199
5425
  }
5200
5426
 
@@ -5384,14 +5610,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
5384
5610
  return out;
5385
5611
  }
5386
5612
  runOneShot(binary, args) {
5387
- return new Promise((resolve7) => {
5613
+ return new Promise((resolve8) => {
5388
5614
  let child;
5389
5615
  try {
5390
5616
  child = this.spawnImpl(binary, args, {
5391
5617
  stdio: ["ignore", "pipe", "pipe"]
5392
5618
  });
5393
5619
  } catch (err) {
5394
- resolve7(
5620
+ resolve8(
5395
5621
  Err14({
5396
5622
  category: "agent_not_found",
5397
5623
  message: err instanceof Error ? err.message : "failed to spawn runtime"
@@ -5416,9 +5642,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
5416
5642
  child.on("close", (code) => {
5417
5643
  clearTimeout(timer);
5418
5644
  if (code === 0) {
5419
- resolve7(Ok17(stdout));
5645
+ resolve8(Ok17(stdout));
5420
5646
  } else {
5421
- resolve7(
5647
+ resolve8(
5422
5648
  Err14({
5423
5649
  category: "response_error",
5424
5650
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
@@ -5428,7 +5654,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5428
5654
  });
5429
5655
  child.on("error", (err) => {
5430
5656
  clearTimeout(timer);
5431
- resolve7(Err14({ category: "agent_not_found", message: err.message }));
5657
+ resolve8(Err14({ category: "agent_not_found", message: err.message }));
5432
5658
  });
5433
5659
  });
5434
5660
  }
@@ -5488,13 +5714,13 @@ async function* readLines2(stream) {
5488
5714
  if (buffer.length > 0) yield buffer;
5489
5715
  }
5490
5716
  function waitForExit2(child) {
5491
- return new Promise((resolve7) => {
5717
+ return new Promise((resolve8) => {
5492
5718
  if (child.exitCode !== null) {
5493
- resolve7(child.exitCode);
5719
+ resolve8(child.exitCode);
5494
5720
  return;
5495
5721
  }
5496
- child.once("close", (code) => resolve7(code));
5497
- child.once("error", () => resolve7(null));
5722
+ child.once("close", (code) => resolve8(code));
5723
+ child.once("error", () => resolve8(null));
5498
5724
  });
5499
5725
  }
5500
5726
 
@@ -5694,13 +5920,13 @@ var ContainerBackend = class {
5694
5920
  import { execFile as execFile3, spawn as spawn5 } from "child_process";
5695
5921
  import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
5696
5922
  function dockerExec(args) {
5697
- return new Promise((resolve7, reject) => {
5923
+ return new Promise((resolve8, reject) => {
5698
5924
  execFile3("docker", args, (error, stdout) => {
5699
5925
  if (error) {
5700
5926
  reject(error);
5701
5927
  return;
5702
5928
  }
5703
- resolve7(stdout.trim());
5929
+ resolve8(stdout.trim());
5704
5930
  });
5705
5931
  });
5706
5932
  }
@@ -5759,11 +5985,11 @@ var DockerRuntime = class {
5759
5985
  } finally {
5760
5986
  rl.close();
5761
5987
  }
5762
- const exitCode = await new Promise((resolve7) => {
5988
+ const exitCode = await new Promise((resolve8) => {
5763
5989
  if (child.exitCode !== null) {
5764
- resolve7(child.exitCode);
5990
+ resolve8(child.exitCode);
5765
5991
  } else {
5766
- child.on("exit", (code) => resolve7(code ?? 1));
5992
+ child.on("exit", (code) => resolve8(code ?? 1));
5767
5993
  }
5768
5994
  });
5769
5995
  return exitCode;
@@ -5822,13 +6048,13 @@ var EnvSecretBackend = class {
5822
6048
  import { execFile as execFile4 } from "child_process";
5823
6049
  import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
5824
6050
  function opExec(args) {
5825
- return new Promise((resolve7, reject) => {
6051
+ return new Promise((resolve8, reject) => {
5826
6052
  execFile4("op", args, (error, stdout) => {
5827
6053
  if (error) {
5828
6054
  reject(error);
5829
6055
  return;
5830
6056
  }
5831
- resolve7(stdout.trim());
6057
+ resolve8(stdout.trim());
5832
6058
  });
5833
6059
  });
5834
6060
  }
@@ -5871,13 +6097,13 @@ var OnePasswordSecretBackend = class {
5871
6097
  import { execFile as execFile5 } from "child_process";
5872
6098
  import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
5873
6099
  function vaultExec(args, env) {
5874
- return new Promise((resolve7, reject) => {
6100
+ return new Promise((resolve8, reject) => {
5875
6101
  execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5876
6102
  if (error) {
5877
6103
  reject(error);
5878
6104
  return;
5879
6105
  }
5880
- resolve7(stdout.trim());
6106
+ resolve8(stdout.trim());
5881
6107
  });
5882
6108
  });
5883
6109
  }
@@ -5952,7 +6178,11 @@ var OrchestratorBackendFactory = class {
5952
6178
  opts;
5953
6179
  constructor(opts) {
5954
6180
  this.opts = opts;
5955
- this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
6181
+ this.router = new BackendRouter({
6182
+ backends: opts.backends,
6183
+ routing: opts.routing,
6184
+ ...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
6185
+ });
5956
6186
  }
5957
6187
  /**
5958
6188
  * Resolve `useCase` to a backend name, materialize a fresh
@@ -5971,12 +6201,21 @@ var OrchestratorBackendFactory = class {
5971
6201
  * is `undefined` for pure-modern configs. Threading the routed name
5972
6202
  * through dispatch eliminates that gap.
5973
6203
  */
5974
- resolveName(useCase) {
5975
- return this.router.resolve(useCase);
6204
+ resolveName(useCase, opts) {
6205
+ return this.router.resolve(useCase, opts).backendName;
5976
6206
  }
5977
- forUseCase(useCase) {
5978
- const def = this.router.resolveDefinition(useCase);
5979
- const name = this.router.resolve(useCase);
6207
+ /**
6208
+ * Spec B Phase 1: expose the underlying router for callers that need
6209
+ * it directly (e.g., {@link buildIntelligencePipeline} for the
6210
+ * I1 SEL/PESL comparison fix). Read-only access; consumers must not
6211
+ * mutate router state.
6212
+ */
6213
+ getRouter() {
6214
+ return this.router;
6215
+ }
6216
+ forUseCase(useCase, opts) {
6217
+ const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
6218
+ const name = decision.backendName;
5980
6219
  let backend;
5981
6220
  const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
5982
6221
  if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
@@ -6149,15 +6388,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
6149
6388
 
6150
6389
  // src/agent/intelligence-factory.ts
6151
6390
  function buildIntelligencePipeline(deps) {
6152
- const { config } = deps;
6391
+ const { config, router } = deps;
6153
6392
  const intel = config.intelligence;
6154
6393
  if (!intel?.enabled) return null;
6155
6394
  const selProvider = buildAnalysisProviderForLayer("sel", deps);
6156
6395
  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;
6396
+ const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
6397
+ const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
6398
+ const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6161
6399
  const peslModel = intel.models?.pesl ?? config.agent.model;
6162
6400
  const graphStore = new GraphStore();
6163
6401
  const pipeline = new IntelligencePipeline(selProvider, graphStore, {
@@ -6174,7 +6412,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
6174
6412
  const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
6175
6413
  return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
6176
6414
  }
6177
- const routed = resolveRoutedBackend(layer, config, logger);
6415
+ const routed = resolveRoutedBackend(layer, deps);
6178
6416
  if (!routed) return null;
6179
6417
  const { name, def } = routed;
6180
6418
  const resolver = localResolvers.get(name);
@@ -6199,20 +6437,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
6199
6437
  logger
6200
6438
  });
6201
6439
  }
6202
- function resolveRoutedBackend(layer, config, logger) {
6203
- const routing = config.agent.routing;
6440
+ function resolveRoutedBackend(layer, deps) {
6441
+ const { config, router, logger } = deps;
6204
6442
  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) {
6443
+ if (!backends || !router) return null;
6444
+ try {
6445
+ const decision = router.resolve({ kind: "intelligence", layer });
6446
+ const def = backends[decision.backendName];
6447
+ if (!def) {
6448
+ logger.warn(
6449
+ `Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
6450
+ );
6451
+ return null;
6452
+ }
6453
+ return { name: decision.backendName, def };
6454
+ } catch (err) {
6210
6455
  logger.warn(
6211
- `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
6456
+ `Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
6212
6457
  );
6213
6458
  return null;
6214
6459
  }
6215
- return { name, def };
6216
6460
  }
6217
6461
  function buildExplicitProvider(provider, selModel, config) {
6218
6462
  if (provider.kind === "anthropic") {
@@ -6247,9 +6491,104 @@ function buildExplicitProvider(provider, selModel, config) {
6247
6491
  });
6248
6492
  }
6249
6493
 
6494
+ // src/routing/decision-bus.ts
6495
+ var RoutingDecisionBus = class {
6496
+ ringBuffer = [];
6497
+ listeners = /* @__PURE__ */ new Set();
6498
+ capacity;
6499
+ logger;
6500
+ constructor(opts) {
6501
+ this.capacity = opts?.capacity ?? 500;
6502
+ this.logger = opts?.logger;
6503
+ }
6504
+ emit(decision) {
6505
+ this.ringBuffer.push(decision);
6506
+ if (this.ringBuffer.length > this.capacity) {
6507
+ this.ringBuffer.shift();
6508
+ }
6509
+ if (this.logger) {
6510
+ this.logger.info("routing-decision", {
6511
+ useCase: decision.useCase,
6512
+ backendName: decision.backendName,
6513
+ resolutionPathLength: decision.resolutionPath.length,
6514
+ durationMs: decision.durationMs
6515
+ });
6516
+ }
6517
+ for (const listener of this.listeners) {
6518
+ try {
6519
+ listener(decision);
6520
+ } catch (err) {
6521
+ if (this.logger) {
6522
+ this.logger.warn("RoutingDecisionBus subscriber threw", {
6523
+ error: String(err)
6524
+ });
6525
+ }
6526
+ }
6527
+ }
6528
+ }
6529
+ recent(filter) {
6530
+ let out = this.ringBuffer.slice();
6531
+ if (filter?.skillName !== void 0) {
6532
+ out = out.filter(
6533
+ (d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
6534
+ );
6535
+ }
6536
+ if (filter?.mode !== void 0) {
6537
+ const m = filter.mode;
6538
+ out = out.filter(
6539
+ (d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
6540
+ );
6541
+ }
6542
+ if (filter?.backendName !== void 0) {
6543
+ out = out.filter((d) => d.backendName === filter.backendName);
6544
+ }
6545
+ if (filter?.limit !== void 0) {
6546
+ out = out.slice(-filter.limit).reverse();
6547
+ } else {
6548
+ out = out.reverse();
6549
+ }
6550
+ return out;
6551
+ }
6552
+ subscribe(listener) {
6553
+ this.listeners.add(listener);
6554
+ return () => {
6555
+ this.listeners.delete(listener);
6556
+ };
6557
+ }
6558
+ /**
6559
+ * Spec B Phase 5 (review-S2 fix): release all subscriber references so
6560
+ * teardown can complete without anchoring closures. Called from
6561
+ * `Orchestrator.stop()` before nulling the bus reference. The bus
6562
+ * remains usable after clear — `subscribe()` works as normal.
6563
+ */
6564
+ clearListeners() {
6565
+ this.listeners.clear();
6566
+ }
6567
+ };
6568
+
6569
+ // src/agent/triage-skill-mapping.ts
6570
+ function resolveSkillForTriage(triageSkill, catalog) {
6571
+ const expected = `harness-${triageSkill}`;
6572
+ const match = catalog.find((e) => e.name === expected);
6573
+ if (!match) return void 0;
6574
+ return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
6575
+ }
6576
+
6577
+ // src/agent/use-case-builder.ts
6578
+ function buildRoutingUseCase(issue, backendParam, catalog) {
6579
+ if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
6580
+ const decision = triageIssue(issue, {});
6581
+ const resolved = resolveSkillForTriage(decision.skill, catalog);
6582
+ if (resolved) {
6583
+ return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
6584
+ }
6585
+ const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
6586
+ return { kind: "tier", tier };
6587
+ }
6588
+
6250
6589
  // src/server/http.ts
6251
6590
  import * as http from "http";
6252
- import * as path15 from "path";
6591
+ import * as path17 from "path";
6253
6592
  import { assertPortUsable } from "@harness-engineering/core";
6254
6593
 
6255
6594
  // src/server/websocket.ts
@@ -6312,7 +6651,7 @@ import { z as z3 } from "zod";
6312
6651
  // src/server/utils.ts
6313
6652
  var DEFAULT_MAX_BYTES = 1048576;
6314
6653
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6315
- return new Promise((resolve7, reject) => {
6654
+ return new Promise((resolve8, reject) => {
6316
6655
  let body = "";
6317
6656
  let bytes = 0;
6318
6657
  req.on("data", (chunk) => {
@@ -6324,7 +6663,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6324
6663
  }
6325
6664
  body += String(chunk);
6326
6665
  });
6327
- req.on("end", () => resolve7(body));
6666
+ req.on("end", () => resolve8(body));
6328
6667
  req.on("error", reject);
6329
6668
  });
6330
6669
  }
@@ -6445,8 +6784,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6445
6784
 
6446
6785
  // src/server/routes/plans.ts
6447
6786
  import { z as z5 } from "zod";
6448
- import * as fs9 from "fs/promises";
6449
- import * as path9 from "path";
6787
+ import * as fs10 from "fs/promises";
6788
+ import * as path11 from "path";
6450
6789
  var PlanWriteSchema = z5.object({
6451
6790
  filename: z5.string().min(1),
6452
6791
  content: z5.string().min(1)
@@ -6466,7 +6805,7 @@ function handlePlansRoute(req, res, plansDir) {
6466
6805
  return;
6467
6806
  }
6468
6807
  const parsed = result.data;
6469
- const basename3 = path9.basename(parsed.filename);
6808
+ const basename3 = path11.basename(parsed.filename);
6470
6809
  if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
6471
6810
  res.writeHead(400, { "Content-Type": "application/json" });
6472
6811
  res.end(
@@ -6474,9 +6813,9 @@ function handlePlansRoute(req, res, plansDir) {
6474
6813
  );
6475
6814
  return;
6476
6815
  }
6477
- await fs9.mkdir(plansDir, { recursive: true });
6478
- const filePath = path9.join(plansDir, basename3);
6479
- await fs9.writeFile(filePath, parsed.content, "utf-8");
6816
+ await fs10.mkdir(plansDir, { recursive: true });
6817
+ const filePath = path11.join(plansDir, basename3);
6818
+ await fs10.writeFile(filePath, parsed.content, "utf-8");
6480
6819
  res.writeHead(201, { "Content-Type": "application/json" });
6481
6820
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6482
6821
  } catch {
@@ -6851,8 +7190,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
6851
7190
  }
6852
7191
 
6853
7192
  // src/server/routes/roadmap-actions.ts
6854
- import * as fs10 from "fs/promises";
6855
- import * as path10 from "path";
7193
+ import * as fs11 from "fs/promises";
7194
+ import * as path12 from "path";
6856
7195
  import {
6857
7196
  parseRoadmap as parseRoadmap2,
6858
7197
  serializeRoadmap as serializeRoadmap2,
@@ -6888,7 +7227,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6888
7227
  sendJSON2(res, 503, { error: "Roadmap path not configured" });
6889
7228
  return;
6890
7229
  }
6891
- const projectRoot = path10.dirname(path10.dirname(roadmapPath));
7230
+ const projectRoot = path12.dirname(path12.dirname(roadmapPath));
6892
7231
  const mode = loadProjectRoadmapMode(projectRoot);
6893
7232
  if (mode === "file-less") {
6894
7233
  const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
@@ -6941,7 +7280,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6941
7280
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
6942
7281
  return;
6943
7282
  }
6944
- const content = await fs10.readFile(roadmapPath, "utf-8");
7283
+ const content = await fs11.readFile(roadmapPath, "utf-8");
6945
7284
  const roadmapResult = parseRoadmap2(content);
6946
7285
  if (!roadmapResult.ok) {
6947
7286
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
@@ -6972,8 +7311,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6972
7311
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
6973
7312
  const tmpPath = roadmapPath + ".tmp";
6974
7313
  const serialized = serializeRoadmap2(roadmap);
6975
- await fs10.writeFile(tmpPath, serialized, "utf-8");
6976
- await fs10.rename(tmpPath, roadmapPath);
7314
+ await fs11.writeFile(tmpPath, serialized, "utf-8");
7315
+ await fs11.rename(tmpPath, roadmapPath);
6977
7316
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
6978
7317
  } catch (err) {
6979
7318
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7473,7 +7812,7 @@ import {
7473
7812
  } from "@harness-engineering/types";
7474
7813
 
7475
7814
  // src/proposals/gate.ts
7476
- import { parse as parseYaml } from "yaml";
7815
+ import { parse as parseYaml2 } from "yaml";
7477
7816
  import {
7478
7817
  getProposal,
7479
7818
  updateProposal,
@@ -7490,7 +7829,7 @@ function checkSkillYaml(yaml) {
7490
7829
  const findings = [];
7491
7830
  let doc;
7492
7831
  try {
7493
- doc = parseYaml(yaml);
7832
+ doc = parseYaml2(yaml);
7494
7833
  } catch (err) {
7495
7834
  findings.push({
7496
7835
  severity: "error",
@@ -7613,9 +7952,9 @@ async function runGate(projectPath, proposalId) {
7613
7952
  }
7614
7953
 
7615
7954
  // 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";
7955
+ import * as fs12 from "fs";
7956
+ import * as path13 from "path";
7957
+ import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
7619
7958
  import {
7620
7959
  getProposal as getProposal2,
7621
7960
  updateProposal as updateProposal2,
@@ -7635,11 +7974,11 @@ var PromotionError = class extends Error {
7635
7974
  };
7636
7975
  var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7637
7976
  function skillDir(projectPath, name) {
7638
- return path11.join(projectPath, "agents", "skills", "claude-code", name);
7977
+ return path13.join(projectPath, "agents", "skills", "claude-code", name);
7639
7978
  }
7640
7979
  function readIfExists(p) {
7641
7980
  try {
7642
- return fs11.readFileSync(p, "utf-8");
7981
+ return fs12.readFileSync(p, "utf-8");
7643
7982
  } catch {
7644
7983
  return null;
7645
7984
  }
@@ -7647,7 +7986,7 @@ function readIfExists(p) {
7647
7986
  function injectProvenanceIntoYaml(yamlText, proposalId) {
7648
7987
  let doc;
7649
7988
  try {
7650
- doc = parseYaml2(yamlText);
7989
+ doc = parseYaml3(yamlText);
7651
7990
  } catch (err) {
7652
7991
  throw new PromotionError(
7653
7992
  `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
@@ -7685,15 +8024,15 @@ function assertGateReady(proposal) {
7685
8024
  }
7686
8025
  async function promoteNewSkill(projectPath, proposal) {
7687
8026
  const target = skillDir(projectPath, proposal.content.name);
7688
- if (fs11.existsSync(target)) {
8027
+ if (fs12.existsSync(target)) {
7689
8028
  throw new PromotionError(
7690
8029
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7691
8030
  );
7692
8031
  }
7693
- fs11.mkdirSync(target, { recursive: true });
8032
+ fs12.mkdirSync(target, { recursive: true });
7694
8033
  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 ?? "");
8034
+ fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8035
+ fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7697
8036
  return { skillPath: target };
7698
8037
  }
7699
8038
  async function promoteRefinement(projectPath, proposal) {
@@ -7701,12 +8040,12 @@ async function promoteRefinement(projectPath, proposal) {
7701
8040
  throw new PromotionError("refinement proposal is missing targetSkill");
7702
8041
  }
7703
8042
  const target = skillDir(projectPath, proposal.targetSkill);
7704
- if (!fs11.existsSync(target)) {
8043
+ if (!fs12.existsSync(target)) {
7705
8044
  throw new PromotionError(
7706
8045
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7707
8046
  );
7708
8047
  }
7709
- const yamlPath = path11.join(target, "skill.yaml");
8048
+ const yamlPath = path13.join(target, "skill.yaml");
7710
8049
  const before = readIfExists(yamlPath) ?? "";
7711
8050
  const after = injectProvenanceIntoYaml(before, proposal.id);
7712
8051
  if (after === before) {
@@ -7714,7 +8053,7 @@ async function promoteRefinement(projectPath, proposal) {
7714
8053
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7715
8054
  );
7716
8055
  }
7717
- fs11.writeFileSync(yamlPath, after);
8056
+ fs12.writeFileSync(yamlPath, after);
7718
8057
  return { skillPath: target };
7719
8058
  }
7720
8059
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8001,35 +8340,185 @@ function handleV1ProposalsRoute(req, res, deps) {
8001
8340
  return false;
8002
8341
  }
8003
8342
 
8004
- // src/server/routes/sessions.ts
8005
- import * as fs12 from "fs/promises";
8006
- import * as path12 from "path";
8343
+ // src/server/routes/v1/routing.ts
8007
8344
  import { z as z14 } from "zod";
8008
- var SessionCreateSchema = z14.object({
8009
- sessionId: z14.string().min(1)
8345
+ var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
8346
+ var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
8347
+ var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
8348
+ function sendJSON9(res, status, body) {
8349
+ res.writeHead(status, { "Content-Type": "application/json" });
8350
+ res.end(JSON.stringify(body));
8351
+ }
8352
+ function unavailable(res) {
8353
+ sendJSON9(res, 503, { error: "BackendRouter not available" });
8354
+ return true;
8355
+ }
8356
+ function resolveChain(value, backends) {
8357
+ return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
8358
+ }
8359
+ function buildResolvedChains(routing, backends) {
8360
+ const out = {};
8361
+ out["default"] = resolveChain(routing.default, backends);
8362
+ for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
8363
+ const v = routing[tier];
8364
+ if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
8365
+ }
8366
+ if (routing.intelligence) {
8367
+ for (const [layer, v] of Object.entries(routing.intelligence)) {
8368
+ if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
8369
+ }
8370
+ }
8371
+ if (routing.isolation) {
8372
+ for (const [tier, v] of Object.entries(routing.isolation)) {
8373
+ if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
8374
+ }
8375
+ }
8376
+ if (routing.skills) {
8377
+ for (const [name, v] of Object.entries(routing.skills)) {
8378
+ if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
8379
+ }
8380
+ }
8381
+ if (routing.modes) {
8382
+ for (const [mode, v] of Object.entries(routing.modes)) {
8383
+ if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
8384
+ }
8385
+ }
8386
+ return out;
8387
+ }
8388
+ function handleConfig(res, deps) {
8389
+ if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
8390
+ sendJSON9(res, 200, {
8391
+ routing: deps.routing,
8392
+ resolvedChains: buildResolvedChains(deps.routing, deps.backends),
8393
+ backends: Object.keys(deps.backends)
8394
+ });
8395
+ return true;
8396
+ }
8397
+ function parseDecisionsQuery(url) {
8398
+ const qIdx = url.indexOf("?");
8399
+ if (qIdx === -1) return {};
8400
+ const p = new URLSearchParams(url.slice(qIdx + 1));
8401
+ const filter = {};
8402
+ const skill = p.get("skill");
8403
+ const mode = p.get("mode");
8404
+ const backend = p.get("backend");
8405
+ const limit = p.get("limit");
8406
+ if (skill) filter.skillName = skill;
8407
+ if (mode) filter.mode = mode;
8408
+ if (backend) filter.backendName = backend;
8409
+ if (limit) {
8410
+ const n = Number(limit);
8411
+ if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
8412
+ }
8413
+ return filter;
8414
+ }
8415
+ function handleDecisions(req, res, deps) {
8416
+ if (!deps.bus) return unavailable(res);
8417
+ const filter = parseDecisionsQuery(req.url ?? "");
8418
+ sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
8419
+ return true;
8420
+ }
8421
+ var UseCaseSchema = z14.discriminatedUnion("kind", [
8422
+ z14.object({
8423
+ kind: z14.literal("tier"),
8424
+ tier: z14.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
8425
+ }),
8426
+ z14.object({ kind: z14.literal("intelligence"), layer: z14.enum(["sel", "pesl"]) }),
8427
+ z14.object({ kind: z14.literal("isolation"), tier: z14.string() }),
8428
+ z14.object({ kind: z14.literal("maintenance") }),
8429
+ z14.object({ kind: z14.literal("chat") }),
8430
+ z14.object({
8431
+ kind: z14.literal("skill"),
8432
+ skillName: z14.string().min(1),
8433
+ cognitiveMode: z14.string().optional()
8434
+ }),
8435
+ z14.object({ kind: z14.literal("mode"), cognitiveMode: z14.string().min(1) })
8436
+ ]);
8437
+ var TraceBodySchema = z14.object({
8438
+ useCase: UseCaseSchema,
8439
+ invocationOverride: z14.string().min(1).optional()
8440
+ });
8441
+ async function handleTrace(req, res, deps) {
8442
+ if (!deps.routing || !deps.backends) {
8443
+ unavailable(res);
8444
+ return true;
8445
+ }
8446
+ let raw;
8447
+ try {
8448
+ raw = await readBody(req);
8449
+ } catch {
8450
+ sendJSON9(res, 400, { error: "body read failed" });
8451
+ return true;
8452
+ }
8453
+ let parsed;
8454
+ try {
8455
+ parsed = JSON.parse(raw);
8456
+ } catch {
8457
+ sendJSON9(res, 400, { error: "invalid JSON body" });
8458
+ return true;
8459
+ }
8460
+ const r = TraceBodySchema.safeParse(parsed);
8461
+ if (!r.success) {
8462
+ sendJSON9(res, 400, { error: r.error.message });
8463
+ return true;
8464
+ }
8465
+ const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
8466
+ try {
8467
+ const dryRunRouter = new BackendRouter({
8468
+ backends: deps.backends,
8469
+ routing: deps.routing
8470
+ });
8471
+ const { decision, def } = dryRunRouter.resolveDecisionAndDef(
8472
+ r.data.useCase,
8473
+ opts
8474
+ );
8475
+ sendJSON9(res, 200, { decision, def: { type: def.type } });
8476
+ } catch (err) {
8477
+ sendJSON9(res, 500, { error: String(err) });
8478
+ }
8479
+ return true;
8480
+ }
8481
+ function handleV1RoutingRoute(req, res, deps) {
8482
+ const url = req.url ?? "";
8483
+ const method = req.method ?? "GET";
8484
+ if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
8485
+ if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
8486
+ if (method === "POST" && TRACE_RE.test(url)) {
8487
+ void handleTrace(req, res, deps);
8488
+ return true;
8489
+ }
8490
+ return false;
8491
+ }
8492
+
8493
+ // src/server/routes/sessions.ts
8494
+ import * as fs13 from "fs/promises";
8495
+ import * as path14 from "path";
8496
+ import { z as z15 } from "zod";
8497
+ var SessionCreateSchema = z15.object({
8498
+ sessionId: z15.string().min(1)
8010
8499
  }).passthrough();
8011
8500
  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
8501
  function isSafeId(id) {
8013
- return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
8502
+ return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
8014
8503
  }
8015
8504
  function jsonResponse(res, status, data) {
8016
8505
  res.writeHead(status, { "Content-Type": "application/json" });
8017
8506
  res.end(JSON.stringify(data));
8018
8507
  }
8019
8508
  function extractSessionId(url) {
8020
- const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
8509
+ const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
8021
8510
  const id = segments.pop();
8022
8511
  return id && id !== "sessions" ? id : null;
8023
8512
  }
8024
8513
  async function handleList2(res, sessionsDir) {
8025
8514
  try {
8026
- const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
8515
+ const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8027
8516
  const sessions = [];
8028
8517
  for (const entry of entries) {
8029
8518
  if (!entry.isDirectory()) continue;
8030
8519
  try {
8031
- const content = await fs12.readFile(
8032
- path12.join(sessionsDir, entry.name, "session.json"),
8520
+ const content = await fs13.readFile(
8521
+ path14.join(sessionsDir, entry.name, "session.json"),
8033
8522
  "utf-8"
8034
8523
  );
8035
8524
  sessions.push(JSON.parse(content));
@@ -8054,7 +8543,7 @@ async function handleGet2(res, id, sessionsDir) {
8054
8543
  return;
8055
8544
  }
8056
8545
  try {
8057
- const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
8546
+ const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8058
8547
  jsonResponse(res, 200, JSON.parse(content));
8059
8548
  } catch (err) {
8060
8549
  if (err.code === "ENOENT") {
@@ -8077,9 +8566,9 @@ async function handleCreate(req, res, sessionsDir) {
8077
8566
  jsonResponse(res, 400, { error: "Invalid sessionId" });
8078
8567
  return;
8079
8568
  }
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));
8569
+ const sessionDir = path14.join(sessionsDir, session.sessionId);
8570
+ await fs13.mkdir(sessionDir, { recursive: true });
8571
+ await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8083
8572
  jsonResponse(res, 200, { ok: true });
8084
8573
  } catch {
8085
8574
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8093,10 +8582,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
8093
8582
  return;
8094
8583
  }
8095
8584
  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));
8585
+ const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
8586
+ const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8587
+ const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8588
+ await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8100
8589
  jsonResponse(res, 200, { ok: true });
8101
8590
  } catch {
8102
8591
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8109,7 +8598,7 @@ async function handleDelete(res, url, sessionsDir) {
8109
8598
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8110
8599
  return;
8111
8600
  }
8112
- await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
8601
+ await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8113
8602
  jsonResponse(res, 200, { ok: true });
8114
8603
  } catch {
8115
8604
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8213,20 +8702,20 @@ function handleStreamsRoute(req, res, recorder) {
8213
8702
  }
8214
8703
 
8215
8704
  // src/server/routes/auth.ts
8216
- import { z as z15 } from "zod";
8705
+ import { z as z16 } from "zod";
8217
8706
  import {
8218
8707
  TokenScopeSchema,
8219
8708
  BridgeKindSchema,
8220
8709
  AuthTokenPublicSchema
8221
8710
  } from "@harness-engineering/types";
8222
- var CreateBodySchema = z15.object({
8223
- name: z15.string().min(1).max(100),
8224
- scopes: z15.array(TokenScopeSchema).min(1),
8711
+ var CreateBodySchema = z16.object({
8712
+ name: z16.string().min(1).max(100),
8713
+ scopes: z16.array(TokenScopeSchema).min(1),
8225
8714
  bridgeKind: BridgeKindSchema.optional(),
8226
- tenantId: z15.string().optional(),
8227
- expiresAt: z15.string().datetime().optional()
8715
+ tenantId: z16.string().optional(),
8716
+ expiresAt: z16.string().datetime().optional()
8228
8717
  });
8229
- function sendJSON9(res, status, body) {
8718
+ function sendJSON10(res, status, body) {
8230
8719
  res.writeHead(status, { "Content-Type": "application/json" });
8231
8720
  res.end(JSON.stringify(body));
8232
8721
  }
@@ -8236,19 +8725,19 @@ async function handlePost(req, res, store) {
8236
8725
  raw = await readBody(req);
8237
8726
  } catch (err) {
8238
8727
  const msg = err instanceof Error ? err.message : "Failed to read body";
8239
- sendJSON9(res, 413, { error: msg });
8728
+ sendJSON10(res, 413, { error: msg });
8240
8729
  return;
8241
8730
  }
8242
8731
  let json;
8243
8732
  try {
8244
8733
  json = JSON.parse(raw);
8245
8734
  } catch {
8246
- sendJSON9(res, 400, { error: "Invalid JSON body" });
8735
+ sendJSON10(res, 400, { error: "Invalid JSON body" });
8247
8736
  return;
8248
8737
  }
8249
8738
  const parsed = CreateBodySchema.safeParse(json);
8250
8739
  if (!parsed.success) {
8251
- sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8740
+ sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8252
8741
  return;
8253
8742
  }
8254
8743
  try {
@@ -8261,37 +8750,37 @@ async function handlePost(req, res, store) {
8261
8750
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
8262
8751
  const result = await store.create(input);
8263
8752
  const publicRecord = AuthTokenPublicSchema.parse(result.record);
8264
- sendJSON9(res, 200, {
8753
+ sendJSON10(res, 200, {
8265
8754
  ...publicRecord,
8266
8755
  token: result.token
8267
8756
  });
8268
8757
  } catch (err) {
8269
8758
  const msg = err instanceof Error ? err.message : "Failed to create token";
8270
8759
  if (msg.includes("already exists")) {
8271
- sendJSON9(res, 409, { error: msg });
8760
+ sendJSON10(res, 409, { error: msg });
8272
8761
  return;
8273
8762
  }
8274
- sendJSON9(res, 500, { error: "Internal error creating token" });
8763
+ sendJSON10(res, 500, { error: "Internal error creating token" });
8275
8764
  }
8276
8765
  }
8277
8766
  async function handleList3(res, store) {
8278
8767
  try {
8279
8768
  const list = await store.list();
8280
- sendJSON9(res, 200, list);
8769
+ sendJSON10(res, 200, list);
8281
8770
  } catch {
8282
- sendJSON9(res, 500, { error: "Internal error listing tokens" });
8771
+ sendJSON10(res, 500, { error: "Internal error listing tokens" });
8283
8772
  }
8284
8773
  }
8285
8774
  async function handleDelete2(res, store, id) {
8286
8775
  try {
8287
8776
  const ok = await store.revoke(id);
8288
8777
  if (!ok) {
8289
- sendJSON9(res, 404, { error: "Token not found" });
8778
+ sendJSON10(res, 404, { error: "Token not found" });
8290
8779
  return;
8291
8780
  }
8292
- sendJSON9(res, 200, { deleted: true });
8781
+ sendJSON10(res, 200, { deleted: true });
8293
8782
  } catch {
8294
- sendJSON9(res, 500, { error: "Internal error revoking token" });
8783
+ sendJSON10(res, 500, { error: "Internal error revoking token" });
8295
8784
  }
8296
8785
  }
8297
8786
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -8316,12 +8805,12 @@ function handleAuthRoute(req, res, store) {
8316
8805
  return true;
8317
8806
  }
8318
8807
  }
8319
- sendJSON9(res, 405, { error: "Method not allowed" });
8808
+ sendJSON10(res, 405, { error: "Method not allowed" });
8320
8809
  return true;
8321
8810
  }
8322
8811
 
8323
8812
  // src/server/routes/local-model.ts
8324
- function sendJSON10(res, status, body) {
8813
+ function sendJSON11(res, status, body) {
8325
8814
  res.writeHead(status, { "Content-Type": "application/json" });
8326
8815
  res.end(JSON.stringify(body));
8327
8816
  }
@@ -8329,36 +8818,36 @@ function handleLocalModelRoute(req, res, getStatus) {
8329
8818
  const { method, url } = req;
8330
8819
  if (url !== "/api/v1/local-model/status") return false;
8331
8820
  if (method !== "GET") {
8332
- sendJSON10(res, 405, { error: "Method not allowed" });
8821
+ sendJSON11(res, 405, { error: "Method not allowed" });
8333
8822
  return true;
8334
8823
  }
8335
8824
  if (!getStatus) {
8336
- sendJSON10(res, 503, { error: "Local backend not configured" });
8825
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8337
8826
  return true;
8338
8827
  }
8339
8828
  const status = getStatus();
8340
8829
  if (!status) {
8341
- sendJSON10(res, 503, { error: "Local backend not configured" });
8830
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8342
8831
  return true;
8343
8832
  }
8344
- sendJSON10(res, 200, status);
8833
+ sendJSON11(res, 200, status);
8345
8834
  return true;
8346
8835
  }
8347
8836
  function handleLocalModelsRoute(req, res, getStatuses) {
8348
8837
  const { method, url } = req;
8349
8838
  if (url !== "/api/v1/local-models/status") return false;
8350
8839
  if (method !== "GET") {
8351
- sendJSON10(res, 405, { error: "Method not allowed" });
8840
+ sendJSON11(res, 405, { error: "Method not allowed" });
8352
8841
  return true;
8353
8842
  }
8354
8843
  const statuses = getStatuses ? getStatuses() : [];
8355
- sendJSON10(res, 200, statuses);
8844
+ sendJSON11(res, 200, statuses);
8356
8845
  return true;
8357
8846
  }
8358
8847
 
8359
8848
  // src/server/static.ts
8360
- import * as fs13 from "fs";
8361
- import * as path13 from "path";
8849
+ import * as fs14 from "fs";
8850
+ import * as path15 from "path";
8362
8851
  var MIME_TYPES = {
8363
8852
  ".html": "text/html; charset=utf-8",
8364
8853
  ".js": "application/javascript; charset=utf-8",
@@ -8378,29 +8867,29 @@ var MIME_TYPES = {
8378
8867
  function handleStaticFile(req, res, dashboardDir) {
8379
8868
  const { method, url } = req;
8380
8869
  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");
8870
+ const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
8871
+ const wsPath = path15.posix.join(path15.posix.sep, "ws");
8383
8872
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
8384
8873
  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);
8874
+ const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8875
+ const resolved = path15.resolve(requestedPath);
8876
+ if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8877
+ return serveFile(path15.join(dashboardDir, "index.html"), res);
8389
8878
  }
8390
- if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
8879
+ if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
8391
8880
  return serveFile(resolved, res);
8392
8881
  }
8393
- const indexPath = path13.join(dashboardDir, "index.html");
8394
- if (fs13.existsSync(indexPath)) {
8882
+ const indexPath = path15.join(dashboardDir, "index.html");
8883
+ if (fs14.existsSync(indexPath)) {
8395
8884
  return serveFile(indexPath, res);
8396
8885
  }
8397
8886
  return false;
8398
8887
  }
8399
8888
  function serveFile(filePath, res) {
8400
- const ext = path13.extname(filePath).toLowerCase();
8889
+ const ext = path15.extname(filePath).toLowerCase();
8401
8890
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8402
8891
  try {
8403
- const content = fs13.readFileSync(filePath);
8892
+ const content = fs14.readFileSync(filePath);
8404
8893
  res.writeHead(200, { "Content-Type": contentType });
8405
8894
  res.end(content);
8406
8895
  return true;
@@ -8410,8 +8899,8 @@ function serveFile(filePath, res) {
8410
8899
  }
8411
8900
 
8412
8901
  // src/server/plan-watcher.ts
8413
- import * as fs14 from "fs";
8414
- import * as path14 from "path";
8902
+ import * as fs15 from "fs";
8903
+ import * as path16 from "path";
8415
8904
  var PlanWatcher = class {
8416
8905
  plansDir;
8417
8906
  queue;
@@ -8425,11 +8914,11 @@ var PlanWatcher = class {
8425
8914
  * Creates the directory if it does not exist.
8426
8915
  */
8427
8916
  start() {
8428
- fs14.mkdirSync(this.plansDir, { recursive: true });
8429
- this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
8917
+ fs15.mkdirSync(this.plansDir, { recursive: true });
8918
+ this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
8430
8919
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
8431
- const filePath = path14.join(this.plansDir, filename);
8432
- if (fs14.existsSync(filePath)) {
8920
+ const filePath = path16.join(this.plansDir, filename);
8921
+ if (fs15.existsSync(filePath)) {
8433
8922
  void this.handleNewPlan(filename);
8434
8923
  }
8435
8924
  }
@@ -8462,7 +8951,7 @@ var PlanWatcher = class {
8462
8951
  // src/auth/tokens.ts
8463
8952
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
8464
8953
  import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
8465
- import { dirname as dirname4 } from "path";
8954
+ import { dirname as dirname5 } from "path";
8466
8955
  import bcrypt from "bcryptjs";
8467
8956
  import {
8468
8957
  AuthTokenSchema,
@@ -8482,8 +8971,8 @@ function parseToken(raw) {
8482
8971
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
8483
8972
  }
8484
8973
  var TokenStore = class {
8485
- constructor(path22) {
8486
- this.path = path22;
8974
+ constructor(path24) {
8975
+ this.path = path24;
8487
8976
  }
8488
8977
  path;
8489
8978
  cache = null;
@@ -8504,7 +8993,7 @@ var TokenStore = class {
8504
8993
  return this.cache;
8505
8994
  }
8506
8995
  async persist(records) {
8507
- await mkdir7(dirname4(this.path), { recursive: true });
8996
+ await mkdir7(dirname5(this.path), { recursive: true });
8508
8997
  const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
8509
8998
  await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
8510
8999
  await rename2(tmp, this.path);
@@ -8587,11 +9076,11 @@ var TokenStore = class {
8587
9076
 
8588
9077
  // src/auth/audit.ts
8589
9078
  import { appendFile, mkdir as mkdir8 } from "fs/promises";
8590
- import { dirname as dirname5 } from "path";
9079
+ import { dirname as dirname6 } from "path";
8591
9080
  import { AuthAuditEntrySchema } from "@harness-engineering/types";
8592
9081
  var AuditLogger = class {
8593
- constructor(path22, opts = {}) {
8594
- this.path = path22;
9082
+ constructor(path24, opts = {}) {
9083
+ this.path = path24;
8595
9084
  this.opts = opts;
8596
9085
  }
8597
9086
  path;
@@ -8618,7 +9107,7 @@ var AuditLogger = class {
8618
9107
  async writeLine(line) {
8619
9108
  try {
8620
9109
  if (this.opts.createDir !== false && !this.dirEnsured) {
8621
- await mkdir8(dirname5(this.path), { recursive: true });
9110
+ await mkdir8(dirname6(this.path), { recursive: true });
8622
9111
  this.dirEnsured = true;
8623
9112
  }
8624
9113
  await appendFile(this.path, line, "utf8");
@@ -8718,14 +9207,36 @@ var V1_BRIDGE_ROUTES = [
8718
9207
  pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
8719
9208
  scope: "read-telemetry",
8720
9209
  description: "Prompt-cache hit/miss snapshot (rolling window)."
9210
+ },
9211
+ // ── Spec B Phase 5 routing observability ──
9212
+ // D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
9213
+ // precedent (read-only observability). A dedicated `read-routing`
9214
+ // scope was rejected to avoid a TokenScopeSchema + ADR cascade.
9215
+ {
9216
+ method: "GET",
9217
+ pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
9218
+ scope: "read-telemetry",
9219
+ description: "Current routing config + resolved fallback chains + known backends."
9220
+ },
9221
+ {
9222
+ method: "GET",
9223
+ pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
9224
+ scope: "read-telemetry",
9225
+ description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
9226
+ },
9227
+ {
9228
+ method: "POST",
9229
+ pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
9230
+ scope: "read-telemetry",
9231
+ description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
8721
9232
  }
8722
9233
  ];
8723
9234
  function isV1Bridge(method, url) {
8724
9235
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
8725
9236
  }
8726
- function requiredBridgeScope(method, path22) {
9237
+ function requiredBridgeScope(method, path24) {
8727
9238
  for (const r of V1_BRIDGE_ROUTES) {
8728
- if (r.method === method && r.pattern.test(path22)) return r.scope;
9239
+ if (r.method === method && r.pattern.test(path24)) return r.scope;
8729
9240
  }
8730
9241
  return null;
8731
9242
  }
@@ -8735,24 +9246,24 @@ function hasScope(held, required) {
8735
9246
  if (held.includes("admin")) return true;
8736
9247
  return held.includes(required);
8737
9248
  }
8738
- function requiredScopeForRoute(method, path22) {
8739
- const bridgeScope = requiredBridgeScope(method, path22);
9249
+ function requiredScopeForRoute(method, path24) {
9250
+ const bridgeScope = requiredBridgeScope(method, path24);
8740
9251
  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"))
9252
+ if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9253
+ if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9254
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9255
+ if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9256
+ if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9257
+ if (path24.startsWith("/api/plans")) return "read-status";
9258
+ if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9259
+ if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9260
+ if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9261
+ if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
8751
9262
  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";
9263
+ if (path24.startsWith("/api/maintenance")) return "trigger-job";
9264
+ if (path24.startsWith("/api/streams")) return "read-status";
9265
+ if (path24.startsWith("/api/sessions")) return "read-status";
9266
+ if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
8756
9267
  return null;
8757
9268
  }
8758
9269
 
@@ -8816,6 +9327,15 @@ var OrchestratorServer = class {
8816
9327
  getLocalModelStatuses = null;
8817
9328
  webhooks;
8818
9329
  cacheMetrics;
9330
+ // Spec B Phase 5 — routing observability accessor closures + the WS
9331
+ // broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
9332
+ // calls it explicitly; clearListeners in Orchestrator.stop() is the
9333
+ // belt-and-suspenders second line).
9334
+ getBackendRouterFn = null;
9335
+ getRoutingDecisionBusFn = null;
9336
+ getRoutingConfigFn = null;
9337
+ getBackendsFn = null;
9338
+ routingDecisionUnsubscribe = null;
8819
9339
  recorder = null;
8820
9340
  planWatcher = null;
8821
9341
  tokenStore;
@@ -8828,8 +9348,8 @@ var OrchestratorServer = class {
8828
9348
  this.orchestrator = orchestrator;
8829
9349
  this.port = port;
8830
9350
  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");
9351
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
9352
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
8833
9353
  this.tokenStore = new TokenStore(tokensPath);
8834
9354
  this.auditLogger = new AuditLogger(auditPath);
8835
9355
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -8842,20 +9362,24 @@ var OrchestratorServer = class {
8842
9362
  }
8843
9363
  initDependencies(deps) {
8844
9364
  this.interactionQueue = deps?.interactionQueue;
8845
- this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8846
- this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
9365
+ this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
9366
+ this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
8847
9367
  this.claudeCommand = deps?.claudeCommand ?? "claude";
8848
9368
  this.pipeline = deps?.pipeline ?? null;
8849
9369
  this.analysisArchive = deps?.analysisArchive;
8850
9370
  this.roadmapPath = deps?.roadmapPath ?? null;
8851
9371
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
8852
- this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
9372
+ this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
8853
9373
  this.projectPath = deps?.projectPath ?? process.cwd();
8854
9374
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
8855
9375
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
8856
9376
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
8857
9377
  this.webhooks = deps?.webhooks;
8858
9378
  this.cacheMetrics = deps?.cacheMetrics;
9379
+ this.getBackendRouterFn = deps?.getBackendRouter ?? null;
9380
+ this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
9381
+ this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
9382
+ this.getBackendsFn = deps?.getBackends ?? null;
8859
9383
  }
8860
9384
  wireEvents() {
8861
9385
  this.stateChangeListener = (snapshot) => {
@@ -8866,6 +9390,12 @@ var OrchestratorServer = class {
8866
9390
  };
8867
9391
  this.orchestrator.on("state_change", this.stateChangeListener);
8868
9392
  this.orchestrator.on("agent_event", this.agentEventListener);
9393
+ const bus = this.getRoutingDecisionBusFn?.() ?? null;
9394
+ if (bus) {
9395
+ this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
9396
+ this.broadcaster.broadcast("routing:decision", decision);
9397
+ });
9398
+ }
8869
9399
  }
8870
9400
  /**
8871
9401
  * Broadcast a new interaction to all WebSocket clients.
@@ -9021,6 +9551,14 @@ var OrchestratorServer = class {
9021
9551
  (req, res) => handleV1TelemetryRoute(req, res, {
9022
9552
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
9023
9553
  }),
9554
+ // Spec B Phase 5 — routing observability. Returns 503 when the
9555
+ // backendFactory is null (legacy single-backend configs).
9556
+ (req, res) => handleV1RoutingRoute(req, res, {
9557
+ router: this.getBackendRouterFn?.() ?? null,
9558
+ bus: this.getRoutingDecisionBusFn?.() ?? null,
9559
+ routing: this.getRoutingConfigFn?.() ?? null,
9560
+ backends: this.getBackendsFn?.() ?? null
9561
+ }),
9024
9562
  // Hermes Phase 4 — skill proposal review queue. Read scopes
9025
9563
  // (`read-status`) and write scopes (`manage-proposals`) are enforced
9026
9564
  // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
@@ -9117,17 +9655,21 @@ var OrchestratorServer = class {
9117
9655
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
9118
9656
  this.planWatcher.start();
9119
9657
  }
9120
- return new Promise((resolve7) => {
9658
+ return new Promise((resolve8) => {
9121
9659
  const host = getBindHost();
9122
9660
  this.httpServer.listen(this.port, host, () => {
9123
9661
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
9124
- resolve7();
9662
+ resolve8();
9125
9663
  });
9126
9664
  });
9127
9665
  }
9128
9666
  stop() {
9129
9667
  this.orchestrator.removeListener("state_change", this.stateChangeListener);
9130
9668
  this.orchestrator.removeListener("agent_event", this.agentEventListener);
9669
+ if (this.routingDecisionUnsubscribe) {
9670
+ this.routingDecisionUnsubscribe();
9671
+ this.routingDecisionUnsubscribe = null;
9672
+ }
9131
9673
  if (this.planWatcher) {
9132
9674
  this.planWatcher.stop();
9133
9675
  this.planWatcher = null;
@@ -9140,7 +9682,7 @@ var OrchestratorServer = class {
9140
9682
  // src/gateway/webhooks/store.ts
9141
9683
  import { randomBytes as randomBytes4 } from "crypto";
9142
9684
  import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
9143
- import { dirname as dirname6 } from "path";
9685
+ import { dirname as dirname7 } from "path";
9144
9686
  import { WebhookSubscriptionSchema } from "@harness-engineering/types";
9145
9687
 
9146
9688
  // src/gateway/webhooks/signer.ts
@@ -9171,8 +9713,8 @@ function genSecret2() {
9171
9713
  return randomBytes4(32).toString("base64url");
9172
9714
  }
9173
9715
  var WebhookStore = class {
9174
- constructor(path22) {
9175
- this.path = path22;
9716
+ constructor(path24) {
9717
+ this.path = path24;
9176
9718
  }
9177
9719
  path;
9178
9720
  cache = null;
@@ -9195,7 +9737,7 @@ var WebhookStore = class {
9195
9737
  async persist(records) {
9196
9738
  const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
9197
9739
  try {
9198
- await mkdir9(dirname6(this.path), { recursive: true });
9740
+ await mkdir9(dirname7(this.path), { recursive: true });
9199
9741
  await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
9200
9742
  await rename3(tmp, this.path);
9201
9743
  await chmod(this.path, 384);
@@ -10164,8 +10706,8 @@ var StructuredLogger = class {
10164
10706
  };
10165
10707
 
10166
10708
  // src/workspace/config-scanner.ts
10167
- import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
10168
- import { join as join14, relative } from "path";
10709
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
10710
+ import { join as join15, relative } from "path";
10169
10711
  import {
10170
10712
  scanForInjection,
10171
10713
  SecurityScanner,
@@ -10189,10 +10731,10 @@ function adjustFindingSeverity(findings) {
10189
10731
  });
10190
10732
  }
10191
10733
  async function scanSingleFile(filePath, targetDir, scanner) {
10192
- if (!existsSync5(filePath)) return null;
10734
+ if (!existsSync6(filePath)) return null;
10193
10735
  let content;
10194
10736
  try {
10195
- content = readFileSync5(filePath, "utf8");
10737
+ content = readFileSync6(filePath, "utf8");
10196
10738
  } catch {
10197
10739
  return null;
10198
10740
  }
@@ -10211,7 +10753,7 @@ async function scanWorkspaceConfig(workspacePath) {
10211
10753
  const scanner = new SecurityScanner(parseSecurityConfig({}));
10212
10754
  const results = [];
10213
10755
  for (const configFile of CONFIG_FILES) {
10214
- const result = await scanSingleFile(join14(workspacePath, configFile), workspacePath, scanner);
10756
+ const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
10215
10757
  if (result) results.push(result);
10216
10758
  }
10217
10759
  return { exitCode: computeScanExitCode(results), results };
@@ -10725,19 +11267,19 @@ var SingleProcessLeaderElector = class {
10725
11267
  };
10726
11268
 
10727
11269
  // 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()
11270
+ import * as fs16 from "fs";
11271
+ import * as path18 from "path";
11272
+ import { z as z17 } from "zod";
11273
+ var RunResultSchema = z17.object({
11274
+ taskId: z17.string(),
11275
+ startedAt: z17.string(),
11276
+ completedAt: z17.string(),
11277
+ status: z17.enum(["success", "failure", "skipped", "no-issues"]),
11278
+ findings: z17.number(),
11279
+ fixed: z17.number(),
11280
+ prUrl: z17.string().nullable(),
11281
+ prUpdated: z17.boolean(),
11282
+ error: z17.string().optional()
10741
11283
  });
10742
11284
  var MAX_HISTORY = 500;
10743
11285
  var fallbackLogger = {
@@ -10761,10 +11303,10 @@ var MaintenanceReporter = class {
10761
11303
  */
10762
11304
  async load() {
10763
11305
  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));
11306
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11307
+ const filePath = path18.join(this.persistDir, "history.json");
11308
+ const data = await fs16.promises.readFile(filePath, "utf-8");
11309
+ const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
10768
11310
  if (parsed.success) {
10769
11311
  this.history = parsed.data.slice(0, MAX_HISTORY);
10770
11312
  }
@@ -10797,9 +11339,9 @@ var MaintenanceReporter = class {
10797
11339
  */
10798
11340
  async persist() {
10799
11341
  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");
11342
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11343
+ const filePath = path18.join(this.persistDir, "history.json");
11344
+ await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10803
11345
  } catch (err) {
10804
11346
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
10805
11347
  }
@@ -11286,7 +11828,7 @@ function parseStatusLine(output) {
11286
11828
  // src/maintenance/check-script-runner.ts
11287
11829
  import { execFile as execFile6 } from "child_process";
11288
11830
  import { promisify as promisify3 } from "util";
11289
- import * as path17 from "path";
11831
+ import * as path19 from "path";
11290
11832
  var execFileAsync = promisify3(execFile6);
11291
11833
  var CheckScriptRunner = class {
11292
11834
  constructor(cwd) {
@@ -11305,7 +11847,7 @@ var CheckScriptRunner = class {
11305
11847
  }
11306
11848
  };
11307
11849
  async function captureScript(spec, projectRoot) {
11308
- const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11850
+ const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
11309
11851
  const args = spec.args ?? [];
11310
11852
  const timeoutMs = spec.timeoutMs ?? 12e4;
11311
11853
  try {
@@ -11395,8 +11937,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
11395
11937
  }
11396
11938
 
11397
11939
  // src/maintenance/output-store.ts
11398
- import * as fs16 from "fs";
11399
- import * as path18 from "path";
11940
+ import * as fs17 from "fs";
11941
+ import * as path20 from "path";
11400
11942
  var DEFAULT_RETENTION = {
11401
11943
  runs: 50,
11402
11944
  maxAgeDays: 30
@@ -11436,13 +11978,13 @@ var TaskOutputStore = class {
11436
11978
  async write(taskId, entry, retention) {
11437
11979
  this.ensureSafeTaskId(taskId);
11438
11980
  const dir = this.dirFor(taskId);
11439
- await fs16.promises.mkdir(dir, { recursive: true });
11981
+ await fs17.promises.mkdir(dir, { recursive: true });
11440
11982
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11441
- const filePath = path18.join(dir, fileName);
11983
+ const filePath = path20.join(dir, fileName);
11442
11984
  const tmpPath = `${filePath}.tmp`;
11443
11985
  const payload = JSON.stringify(entry, null, 2);
11444
- await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11445
- await fs16.promises.rename(tmpPath, filePath);
11986
+ await fs17.promises.writeFile(tmpPath, payload, "utf-8");
11987
+ await fs17.promises.rename(tmpPath, filePath);
11446
11988
  try {
11447
11989
  await this.applyRetention(taskId, retention);
11448
11990
  } catch (err) {
@@ -11466,7 +12008,7 @@ var TaskOutputStore = class {
11466
12008
  const slice = fileNames.slice(offset, offset + limit);
11467
12009
  const out = [];
11468
12010
  for (const name of slice) {
11469
- const entry = await this.readEntry(path18.join(dir, name));
12011
+ const entry = await this.readEntry(path20.join(dir, name));
11470
12012
  if (entry) out.push(entry);
11471
12013
  }
11472
12014
  return out;
@@ -11482,18 +12024,18 @@ var TaskOutputStore = class {
11482
12024
  }
11483
12025
  const dir = this.dirFor(taskId);
11484
12026
  const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11485
- return this.readEntry(path18.join(dir, fileName));
12027
+ return this.readEntry(path20.join(dir, fileName));
11486
12028
  }
11487
12029
  /**
11488
12030
  * The on-disk root for a given task. Exposed for tooling that needs to walk
11489
12031
  * outputs from outside the store API.
11490
12032
  */
11491
12033
  dirFor(taskId) {
11492
- return path18.join(this.rootDir, taskId, "outputs");
12034
+ return path20.join(this.rootDir, taskId, "outputs");
11493
12035
  }
11494
12036
  async readEntry(filePath) {
11495
12037
  try {
11496
- const buf = await fs16.promises.readFile(filePath, "utf-8");
12038
+ const buf = await fs17.promises.readFile(filePath, "utf-8");
11497
12039
  const parsed = JSON.parse(buf);
11498
12040
  return parsed;
11499
12041
  } catch {
@@ -11515,7 +12057,7 @@ var TaskOutputStore = class {
11515
12057
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11516
12058
  for (const name of toRemove) {
11517
12059
  try {
11518
- await fs16.promises.unlink(path18.join(dir, name));
12060
+ await fs17.promises.unlink(path20.join(dir, name));
11519
12061
  } catch {
11520
12062
  }
11521
12063
  }
@@ -11524,7 +12066,7 @@ var TaskOutputStore = class {
11524
12066
  async function listJsonFilesDescending(dir) {
11525
12067
  let names;
11526
12068
  try {
11527
- names = await fs16.promises.readdir(dir);
12069
+ names = await fs17.promises.readdir(dir);
11528
12070
  } catch {
11529
12071
  return [];
11530
12072
  }
@@ -11698,8 +12240,8 @@ function validateCheckShape(prefix, task, errors) {
11698
12240
  });
11699
12241
  }
11700
12242
  if (hasScript) {
11701
- const path22 = task.checkScript?.path;
11702
- if (!path22 || path22.trim().length === 0) {
12243
+ const path24 = task.checkScript?.path;
12244
+ if (!path24 || path24.trim().length === 0) {
11703
12245
  errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
11704
12246
  }
11705
12247
  if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
@@ -11825,9 +12367,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
11825
12367
  stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11826
12368
  }
11827
12369
  }
11828
- function reportCycle(path22, next, errors, reported) {
11829
- const cycleStart = path22.indexOf(next);
11830
- const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
12370
+ function reportCycle(path24, next, errors, reported) {
12371
+ const cycleStart = path24.indexOf(next);
12372
+ const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
11831
12373
  const key = cyclePath.join("\u2192");
11832
12374
  if (reported.has(key)) return;
11833
12375
  reported.add(key);
@@ -11838,11 +12380,6 @@ function reportCycle(path22, next, errors, reported) {
11838
12380
  }
11839
12381
 
11840
12382
  // 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
12383
  var Orchestrator = class extends EventEmitter {
11847
12384
  state;
11848
12385
  config;
@@ -11867,6 +12404,14 @@ var Orchestrator = class extends EventEmitter {
11867
12404
  * construction time. Eliminating this fallback is autopilot Phase 4+.
11868
12405
  */
11869
12406
  backendFactory;
12407
+ /**
12408
+ * Spec B Phase 4 (D8): per-orchestrator in-process bus for
12409
+ * `RoutingDecision` events. Constructed alongside backendFactory when
12410
+ * agent.backends synthesis succeeds; null when legacy single-backend
12411
+ * config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
12412
+ * subscribe via `getRoutingDecisionBus()`.
12413
+ */
12414
+ routingDecisionBus;
11870
12415
  /**
11871
12416
  * Test-only: when overrides.backend is provided, dispatch uses this
11872
12417
  * instance directly (bypassing the factory). Mirrors Phase 1
@@ -11889,6 +12434,15 @@ var Orchestrator = class extends EventEmitter {
11889
12434
  * so this map is the single source of truth post-migration.
11890
12435
  */
11891
12436
  localResolvers = /* @__PURE__ */ new Map();
12437
+ /**
12438
+ * Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
12439
+ * construction from `projectRoot/agents/skills/`. Consulted by
12440
+ * `buildRoutingUseCase` at dispatch start to construct
12441
+ * `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
12442
+ * Empty when the orchestrator runs outside a harness project root
12443
+ * (then dispatch falls through to per-tier, preserving F11/N2).
12444
+ */
12445
+ skillCatalog;
11892
12446
  /**
11893
12447
  * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
11894
12448
  * (SC39): each local/pi resolver gets its own listener emitting a
@@ -11937,7 +12491,7 @@ var Orchestrator = class extends EventEmitter {
11937
12491
  completionHandler;
11938
12492
  /** Project root directory, derived from workspace root. */
11939
12493
  get projectRoot() {
11940
- return path19.resolve(this.config.workspace.root, "..", "..");
12494
+ return path21.resolve(this.config.workspace.root, "..", "..");
11941
12495
  }
11942
12496
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
11943
12497
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -11981,6 +12535,13 @@ var Orchestrator = class extends EventEmitter {
11981
12535
  `migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
11982
12536
  );
11983
12537
  }
12538
+ const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
12539
+ this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
12540
+ if (this.skillCatalog.length === 0) {
12541
+ this.logger.warn(
12542
+ `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")}.`
12543
+ );
12544
+ }
11984
12545
  this.tracker = overrides?.tracker || this.createTracker();
11985
12546
  this.workspace = new WorkspaceManager(config.workspace, {
11986
12547
  emitEvent: (event) => {
@@ -11992,10 +12553,10 @@ var Orchestrator = class extends EventEmitter {
11992
12553
  this.renderer = new PromptRenderer();
11993
12554
  this.overrideBackend = overrides?.backend ?? null;
11994
12555
  this.interactionQueue = new InteractionQueue(
11995
- path19.join(config.workspace.root, "..", "interactions"),
12556
+ path21.join(config.workspace.root, "..", "interactions"),
11996
12557
  this
11997
12558
  );
11998
- this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
12559
+ this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
11999
12560
  const backendsMap = this.config.agent.backends ?? {};
12000
12561
  for (const [name, def] of Object.entries(backendsMap)) {
12001
12562
  if (def.type === "local" || def.type === "pi") {
@@ -12016,6 +12577,10 @@ var Orchestrator = class extends EventEmitter {
12016
12577
  const routing = this.config.agent.routing ?? {
12017
12578
  default: firstBackendName ?? "primary"
12018
12579
  };
12580
+ this.routingDecisionBus = new RoutingDecisionBus({
12581
+ capacity: 500,
12582
+ logger: this.logger
12583
+ });
12019
12584
  this.backendFactory = new OrchestratorBackendFactory({
12020
12585
  backends: this.config.agent.backends,
12021
12586
  routing,
@@ -12023,6 +12588,7 @@ var Orchestrator = class extends EventEmitter {
12023
12588
  ...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
12024
12589
  ...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
12025
12590
  cacheMetrics: this.cacheMetrics,
12591
+ decisionBus: this.routingDecisionBus,
12026
12592
  getResolverModelFor: (name) => {
12027
12593
  const resolver = this.localResolvers.get(name);
12028
12594
  return resolver ? () => resolver.resolveModel() : void 0;
@@ -12030,6 +12596,7 @@ var Orchestrator = class extends EventEmitter {
12030
12596
  });
12031
12597
  } else {
12032
12598
  this.backendFactory = null;
12599
+ this.routingDecisionBus = null;
12033
12600
  }
12034
12601
  this.pipeline = null;
12035
12602
  this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
@@ -12039,7 +12606,7 @@ var Orchestrator = class extends EventEmitter {
12039
12606
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
12040
12607
  });
12041
12608
  this.recorder = new StreamRecorder(
12042
- path19.resolve(config.workspace.root, "..", "streams"),
12609
+ path21.resolve(config.workspace.root, "..", "streams"),
12043
12610
  this.logger
12044
12611
  );
12045
12612
  const self = this;
@@ -12070,10 +12637,10 @@ var Orchestrator = class extends EventEmitter {
12070
12637
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
12071
12638
  if (config.server?.port) {
12072
12639
  const webhookStore = new WebhookStore(
12073
- path19.join(this.projectRoot, ".harness", "webhooks.json")
12640
+ path21.join(this.projectRoot, ".harness", "webhooks.json")
12074
12641
  );
12075
12642
  this.webhookQueue = new WebhookQueue(
12076
- path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12643
+ path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12077
12644
  );
12078
12645
  const webhookDelivery = new WebhookDelivery({
12079
12646
  queue: this.webhookQueue,
@@ -12111,7 +12678,16 @@ var Orchestrator = class extends EventEmitter {
12111
12678
  queue: this.webhookQueue
12112
12679
  },
12113
12680
  cacheMetrics: this.cacheMetrics,
12114
- plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
12681
+ // Spec B Phase 5: routing observability accessors. Closures so the
12682
+ // server re-reads on every request — stop() / start() do not
12683
+ // require server reconstruction. Returns null if no backendFactory
12684
+ // (legacy single-backend configs), and the route handler renders
12685
+ // 503 in that case.
12686
+ getBackendRouter: () => this.getBackendRouter(),
12687
+ getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
12688
+ getRoutingConfig: () => this.getRoutingConfig(),
12689
+ getBackends: () => this.getBackends(),
12690
+ plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
12115
12691
  pipeline: this.pipeline,
12116
12692
  analysisArchive: this.analysisArchive,
12117
12693
  roadmapPath: config.tracker.filePath ?? null,
@@ -12221,7 +12797,7 @@ var Orchestrator = class extends EventEmitter {
12221
12797
  }
12222
12798
  };
12223
12799
  const outputStore = new TaskOutputStore({
12224
- rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12800
+ rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12225
12801
  logger: this.logger
12226
12802
  });
12227
12803
  const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
@@ -12262,7 +12838,7 @@ var Orchestrator = class extends EventEmitter {
12262
12838
  ${messages}`);
12263
12839
  }
12264
12840
  this.maintenanceReporter = new MaintenanceReporter({
12265
- persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12841
+ persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12266
12842
  logger: this.logger
12267
12843
  });
12268
12844
  await this.maintenanceReporter.load();
@@ -12313,10 +12889,17 @@ ${messages}`);
12313
12889
  }
12314
12890
  }
12315
12891
  createIntelligencePipeline() {
12892
+ if (!this.backendFactory) {
12893
+ this.logger.warn(
12894
+ "intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
12895
+ );
12896
+ return null;
12897
+ }
12316
12898
  const bundle = buildIntelligencePipeline({
12317
12899
  config: this.config,
12318
12900
  localResolvers: this.localResolvers,
12319
- logger: this.logger
12901
+ logger: this.logger,
12902
+ router: this.backendFactory.getRouter()
12320
12903
  });
12321
12904
  if (!bundle) return null;
12322
12905
  this.graphStore = bundle.graphStore;
@@ -12367,11 +12950,13 @@ ${messages}`);
12367
12950
  simulationResults,
12368
12951
  personaRecommendations
12369
12952
  } = pipelineResult ?? {};
12953
+ const selfAssignee = await this.orchestratorIdPromise;
12370
12954
  const tickEvent = {
12371
12955
  type: "tick",
12372
12956
  candidates,
12373
12957
  runningStates: runningStatesResult.value,
12374
12958
  nowMs,
12959
+ selfAssignee,
12375
12960
  ...concernSignals !== void 0 && { concernSignals },
12376
12961
  ...enrichedSpecs !== void 0 && { enrichedSpecs },
12377
12962
  ...complexityScores !== void 0 && { complexityScores },
@@ -12795,14 +13380,24 @@ ${messages}`);
12795
13380
  issue,
12796
13381
  attempt: attempt || 1
12797
13382
  });
12798
- const useCase = useCaseForBackendParam(issue, backend);
13383
+ const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
13384
+ const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
13385
+ const routerOpts = invocationOverride ? { invocationOverride } : void 0;
13386
+ if (invocationOverride) {
13387
+ this.logger.info(
13388
+ `Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
13389
+ { issueId: issue.id }
13390
+ );
13391
+ }
12799
13392
  let routedBackendName;
12800
13393
  if (this.overrideBackend !== null) {
12801
13394
  routedBackendName = this.overrideBackend.name;
12802
13395
  } else if (this.backendFactory !== null) {
12803
- routedBackendName = this.backendFactory.resolveName(useCase);
13396
+ routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
12804
13397
  } else {
12805
- routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
13398
+ const routingDefault = this.config.agent.routing?.default;
13399
+ const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
13400
+ routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
12806
13401
  }
12807
13402
  const session = {
12808
13403
  sessionId: `pending-${Date.now()}`,
@@ -12841,7 +13436,7 @@ ${messages}`);
12841
13436
  if (this.overrideBackend !== null) {
12842
13437
  agentBackend = this.overrideBackend;
12843
13438
  } else if (this.backendFactory !== null) {
12844
- agentBackend = this.backendFactory.forUseCase(useCase);
13439
+ agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
12845
13440
  } else {
12846
13441
  throw new Error(
12847
13442
  `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 +13726,8 @@ ${messages}`);
13131
13726
  unsub();
13132
13727
  }
13133
13728
  this.localModelStatusUnsubscribes = [];
13729
+ this.routingDecisionBus?.clearListeners();
13730
+ this.routingDecisionBus = null;
13134
13731
  for (const resolver of this.localResolvers.values()) {
13135
13732
  resolver.stop();
13136
13733
  }
@@ -13204,6 +13801,42 @@ ${messages}`);
13204
13801
  tickActivity: this.tickActivity
13205
13802
  };
13206
13803
  }
13804
+ /**
13805
+ * Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
13806
+ * Phase 7 (dashboard WS broadcast). Returns null when the legacy
13807
+ * single-backend config bypassed agent.backends synthesis.
13808
+ */
13809
+ getRoutingDecisionBus() {
13810
+ return this.routingDecisionBus;
13811
+ }
13812
+ /**
13813
+ * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
13814
+ * dispatch path uses the factory-owned router directly; observability
13815
+ * routes (config / decisions) reach it through this accessor. Returns
13816
+ * null when the legacy single-backend config bypassed agent.backends
13817
+ * synthesis (no backendFactory built).
13818
+ */
13819
+ getBackendRouter() {
13820
+ return this.backendFactory?.getRouter() ?? null;
13821
+ }
13822
+ /**
13823
+ * Spec B Phase 5: snapshot of the active RoutingConfig for the config
13824
+ * route and the trace route's bus-less router construction. Returns
13825
+ * null when the operator's harness.config.json carries no
13826
+ * `agent.routing` block.
13827
+ */
13828
+ getRoutingConfig() {
13829
+ return this.config.agent.routing ?? null;
13830
+ }
13831
+ /**
13832
+ * Spec B Phase 5: snapshot of `agent.backends` for the config route
13833
+ * (existence annotations) and the trace route (bus-less router
13834
+ * construction). Returns null when no synthesized backends map exists
13835
+ * (legacy single-backend configs).
13836
+ */
13837
+ getBackends() {
13838
+ return this.config.agent.backends ?? null;
13839
+ }
13207
13840
  /** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
13208
13841
  getMaintenanceStatus() {
13209
13842
  return this.maintenanceScheduler?.getStatus() ?? null;
@@ -13582,8 +14215,8 @@ async function syncMain(repoRoot, opts = {}) {
13582
14215
  }
13583
14216
 
13584
14217
  // src/sessions/search-index.ts
13585
- import * as fs17 from "fs";
13586
- import * as path20 from "path";
14218
+ import * as fs18 from "fs";
14219
+ import * as path22 from "path";
13587
14220
  import Database2 from "better-sqlite3";
13588
14221
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
13589
14222
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -13628,7 +14261,7 @@ function normalizeFts5Query(query) {
13628
14261
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
13629
14262
  }
13630
14263
  function searchIndexPath(projectPath) {
13631
- return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
14264
+ return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13632
14265
  }
13633
14266
  var FILE_KIND_TO_FILENAME = {
13634
14267
  summary: "summary.md",
@@ -13643,7 +14276,7 @@ var SqliteSearchIndex = class {
13643
14276
  removeSessionStmt;
13644
14277
  totalStmt;
13645
14278
  constructor(dbPath) {
13646
- fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
14279
+ fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
13647
14280
  this.db = new Database2(dbPath);
13648
14281
  this.db.pragma("journal_mode = WAL");
13649
14282
  this.db.pragma("synchronous = NORMAL");
@@ -13748,14 +14381,14 @@ function indexSessionDirectory(idx, args) {
13748
14381
  let docsWritten = 0;
13749
14382
  for (const kind of kinds) {
13750
14383
  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");
14384
+ const filePath = path22.join(args.sessionDir, fileName);
14385
+ if (!fs18.existsSync(filePath)) continue;
14386
+ let body = fs18.readFileSync(filePath, "utf8");
13754
14387
  if (Buffer.byteLength(body, "utf8") > cap) {
13755
14388
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
13756
14389
  }
13757
- const stat = fs17.statSync(filePath);
13758
- const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
14390
+ const stat = fs18.statSync(filePath);
14391
+ const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
13759
14392
  idx.upsertSessionDoc({
13760
14393
  sessionId: args.sessionId,
13761
14394
  archived: args.archived,
@@ -13770,17 +14403,17 @@ function indexSessionDirectory(idx, args) {
13770
14403
  }
13771
14404
  function reindexFromArchive(projectPath, opts = {}) {
13772
14405
  const start = Date.now();
13773
- const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
14406
+ const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
13774
14407
  const idx = openSearchIndex(projectPath);
13775
14408
  try {
13776
14409
  idx.resetArchived();
13777
14410
  let sessionsIndexed = 0;
13778
14411
  let docsWritten = 0;
13779
- if (fs17.existsSync(archiveBase)) {
13780
- const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
14412
+ if (fs18.existsSync(archiveBase)) {
14413
+ const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
13781
14414
  for (const entry of entries) {
13782
14415
  if (!entry.isDirectory()) continue;
13783
- const sessionDir = path20.join(archiveBase, entry.name);
14416
+ const sessionDir = path22.join(archiveBase, entry.name);
13784
14417
  const result = indexSessionDirectory(idx, {
13785
14418
  sessionId: entry.name,
13786
14419
  sessionDir,
@@ -13800,8 +14433,8 @@ function reindexFromArchive(projectPath, opts = {}) {
13800
14433
  }
13801
14434
 
13802
14435
  // src/sessions/summarize.ts
13803
- import * as fs18 from "fs";
13804
- import * as path21 from "path";
14436
+ import * as fs19 from "fs";
14437
+ import * as path23 from "path";
13805
14438
  import {
13806
14439
  SessionSummarySchema
13807
14440
  } from "@harness-engineering/types";
@@ -13831,10 +14464,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
13831
14464
  function readInputCorpus(archiveDir) {
13832
14465
  const parts = [];
13833
14466
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
13834
- const p = path21.join(archiveDir, filename);
13835
- if (!fs18.existsSync(p)) continue;
14467
+ const p = path23.join(archiveDir, filename);
14468
+ if (!fs19.existsSync(p)) continue;
13836
14469
  try {
13837
- const content = fs18.readFileSync(p, "utf8");
14470
+ const content = fs19.readFileSync(p, "utf8");
13838
14471
  if (content.trim().length === 0) continue;
13839
14472
  parts.push(`## FILE: ${kind}
13840
14473
 
@@ -13885,7 +14518,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
13885
14518
  return lines.join("\n");
13886
14519
  }
13887
14520
  function writeStubMarkdown(archiveDir, reason) {
13888
- const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
14521
+ const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
13889
14522
  const body = `---
13890
14523
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
13891
14524
  schemaVersion: 1
@@ -13896,12 +14529,12 @@ status: failed
13896
14529
 
13897
14530
  - reason: ${reason}
13898
14531
  `;
13899
- fs18.writeFileSync(filePath, body, "utf8");
14532
+ fs19.writeFileSync(filePath, body, "utf8");
13900
14533
  return filePath;
13901
14534
  }
13902
14535
  async function summarizeArchivedSession(ctx) {
13903
14536
  const writeStubOnError = ctx.writeStubOnError ?? true;
13904
- if (!fs18.existsSync(ctx.archiveDir)) {
14537
+ if (!fs19.existsSync(ctx.archiveDir)) {
13905
14538
  return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
13906
14539
  }
13907
14540
  const corpus = readInputCorpus(ctx.archiveDir);
@@ -13962,9 +14595,9 @@ async function summarizeArchivedSession(ctx) {
13962
14595
  outputTokens: response.tokenUsage.outputTokens,
13963
14596
  schemaVersion: 1
13964
14597
  };
13965
- const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14598
+ const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13966
14599
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
13967
- fs18.writeFileSync(filePath, body, "utf8");
14600
+ fs19.writeFileSync(filePath, body, "utf8");
13968
14601
  return Ok24({ summary: parsed.data, meta, filePath });
13969
14602
  }
13970
14603
  function isSummaryEnabled(config) {
@@ -14042,12 +14675,14 @@ function buildArchiveHooks(opts) {
14042
14675
  export {
14043
14676
  AnalysisArchive,
14044
14677
  BUILT_IN_TASKS,
14678
+ BackendDefSchema,
14045
14679
  BackendRouter,
14046
14680
  ClaimManager,
14047
14681
  GateNotReadyError,
14048
14682
  GateRunError,
14049
14683
  InteractionQueue,
14050
14684
  LinearGraphQLStub,
14685
+ LocalModelResolver,
14051
14686
  MAX_ATTEMPTS,
14052
14687
  MockBackend,
14053
14688
  ORCHESTRATOR_IDENTITY_FILE,
@@ -14058,6 +14693,8 @@ export {
14058
14693
  PromptRenderer,
14059
14694
  RETRY_DELAYS_MS,
14060
14695
  RoadmapTrackerAdapter,
14696
+ RoutingConfigSchema,
14697
+ RoutingValueSchema,
14061
14698
  SinkConfigError,
14062
14699
  SinkRegistry,
14063
14700
  SlackSink,
@@ -14077,7 +14714,11 @@ export {
14077
14714
  computeRateLimitDelay,
14078
14715
  createBackend,
14079
14716
  createEmptyState,
14717
+ crossFieldRoutingIssues,
14718
+ defaultFetchModels,
14080
14719
  detectScopeTier,
14720
+ discoverSkillCatalog,
14721
+ discoverSkillCatalogNames,
14081
14722
  emitProposalApproved,
14082
14723
  emitProposalCreated,
14083
14724
  emitProposalRejected,
@@ -14093,6 +14734,7 @@ export {
14093
14734
  loadPublishedIndex,
14094
14735
  migrateAgentConfig,
14095
14736
  normalizeFts5Query,
14737
+ normalizeLocalModel,
14096
14738
  openSearchIndex,
14097
14739
  promote,
14098
14740
  reconcile,
@@ -14103,6 +14745,7 @@ export {
14103
14745
  resolveEscalationConfig,
14104
14746
  resolveOrchestratorId,
14105
14747
  routeIssue,
14748
+ routingWarnings,
14106
14749
  runGate,
14107
14750
  savePublishedIndex,
14108
14751
  searchIndexPath,