@harness-engineering/orchestrator 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -67,7 +67,10 @@ __export(index_exports, {
67
67
  computeRateLimitDelay: () => computeRateLimitDelay,
68
68
  createBackend: () => createBackend,
69
69
  createEmptyState: () => createEmptyState,
70
+ crossFieldRoutingIssues: () => crossFieldRoutingIssues,
70
71
  detectScopeTier: () => detectScopeTier,
72
+ discoverSkillCatalog: () => discoverSkillCatalog,
73
+ discoverSkillCatalogNames: () => discoverSkillCatalogNames,
71
74
  emitProposalApproved: () => emitProposalApproved,
72
75
  emitProposalCreated: () => emitProposalCreated,
73
76
  emitProposalRejected: () => emitProposalRejected,
@@ -93,6 +96,7 @@ __export(index_exports, {
93
96
  resolveEscalationConfig: () => resolveEscalationConfig,
94
97
  resolveOrchestratorId: () => resolveOrchestratorId,
95
98
  routeIssue: () => routeIssue,
99
+ routingWarnings: () => routingWarnings,
96
100
  runGate: () => runGate,
97
101
  savePublishedIndex: () => savePublishedIndex,
98
102
  searchIndexPath: () => searchIndexPath,
@@ -138,7 +142,7 @@ function sortCandidates(issues) {
138
142
  return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
139
143
  });
140
144
  }
141
- function isEligible(issue, state, activeStates, terminalStates) {
145
+ function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
142
146
  if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
143
147
  return false;
144
148
  }
@@ -160,6 +164,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
160
164
  if (state.completed.has(issue.id)) {
161
165
  return false;
162
166
  }
167
+ if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
168
+ return false;
169
+ }
163
170
  if (normalizedState === "todo" && issue.blockedBy.length > 0) {
164
171
  const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
165
172
  if (blocker.state === null) return true;
@@ -171,9 +178,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
171
178
  }
172
179
  return true;
173
180
  }
174
- function selectCandidates(issues, state, activeStates, terminalStates) {
181
+ function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
175
182
  const sorted = sortCandidates(issues);
176
- return sorted.filter((issue) => isEligible(issue, state, activeStates, terminalStates));
183
+ return sorted.filter(
184
+ (issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
185
+ );
177
186
  }
178
187
 
179
188
  // src/core/concurrency.ts
@@ -769,7 +778,8 @@ function handleTick(state, event, config) {
769
778
  candidates,
770
779
  next,
771
780
  config.tracker.activeStates,
772
- config.tracker.terminalStates
781
+ config.tracker.terminalStates,
782
+ event.selfAssignee
773
783
  );
774
784
  const escalationConfig = resolveEscalationConfig(config);
775
785
  for (const issue of eligible) {
@@ -1255,7 +1265,7 @@ var ClaimManager = class {
1255
1265
  const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
1256
1266
  if (!claimResult.ok) return claimResult;
1257
1267
  if (this.verifyDelayMs > 0) {
1258
- await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
1268
+ await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
1259
1269
  }
1260
1270
  const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
1261
1271
  if (!statesResult.ok) return statesResult;
@@ -1907,8 +1917,9 @@ function formatFilesList(files) {
1907
1917
  }
1908
1918
 
1909
1919
  // src/workflow/loader.ts
1910
- var fs6 = __toESM(require("fs/promises"));
1911
- var import_yaml = require("yaml");
1920
+ var fs7 = __toESM(require("fs/promises"));
1921
+ var path7 = __toESM(require("path"));
1922
+ var import_yaml2 = require("yaml");
1912
1923
  var import_types3 = require("@harness-engineering/types");
1913
1924
 
1914
1925
  // src/workflow/config.ts
@@ -1960,16 +1971,29 @@ var BackendDefSchema = import_zod.z.discriminatedUnion("type", [
1960
1971
  probeIntervalMs: import_zod.z.number().int().min(1e3).optional()
1961
1972
  }).strict()
1962
1973
  ]);
1974
+ var RoutingValueSchema = import_zod.z.union([
1975
+ import_zod.z.string().min(1),
1976
+ import_zod.z.array(import_zod.z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
1977
+ ]);
1963
1978
  var RoutingConfigSchema = import_zod.z.object({
1964
- default: import_zod.z.string().min(1),
1965
- "quick-fix": import_zod.z.string().optional(),
1966
- "guided-change": import_zod.z.string().optional(),
1967
- "full-exploration": import_zod.z.string().optional(),
1968
- diagnostic: import_zod.z.string().optional(),
1979
+ default: RoutingValueSchema,
1980
+ "quick-fix": RoutingValueSchema.optional(),
1981
+ "guided-change": RoutingValueSchema.optional(),
1982
+ "full-exploration": RoutingValueSchema.optional(),
1983
+ diagnostic: RoutingValueSchema.optional(),
1969
1984
  intelligence: import_zod.z.object({
1970
- sel: import_zod.z.string().optional(),
1971
- pesl: import_zod.z.string().optional()
1972
- }).strict().optional()
1985
+ sel: RoutingValueSchema.optional(),
1986
+ pesl: RoutingValueSchema.optional()
1987
+ }).strict().optional(),
1988
+ // --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
1989
+ isolation: import_zod.z.object({
1990
+ none: RoutingValueSchema.optional(),
1991
+ container: RoutingValueSchema.optional(),
1992
+ "remote-sandbox": RoutingValueSchema.optional()
1993
+ }).strict().optional(),
1994
+ // --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
1995
+ skills: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional(),
1996
+ modes: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional()
1973
1997
  }).strict();
1974
1998
 
1975
1999
  // src/workflow/config.ts
@@ -1978,13 +2002,17 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
1978
2002
  function crossFieldRoutingIssues(backends, routing) {
1979
2003
  const issues = [];
1980
2004
  const names = new Set(Object.keys(backends));
1981
- const checkRef = (path22, name) => {
1982
- if (name !== void 0 && !names.has(name)) {
2005
+ const checkRef = (path24, value) => {
2006
+ if (value === void 0) return;
2007
+ const entries = Array.isArray(value) ? value : [value];
2008
+ entries.forEach((name, idx) => {
2009
+ if (names.has(name)) return;
2010
+ const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
1983
2011
  issues.push({
1984
- path: path22,
1985
- message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
2012
+ path: pathWithIdx,
2013
+ message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1986
2014
  });
1987
- }
2015
+ });
1988
2016
  };
1989
2017
  checkRef(["default"], routing.default);
1990
2018
  checkRef(["quick-fix"], routing["quick-fix"]);
@@ -1993,9 +2021,44 @@ function crossFieldRoutingIssues(backends, routing) {
1993
2021
  checkRef(["diagnostic"], routing.diagnostic);
1994
2022
  checkRef(["intelligence", "sel"], routing.intelligence?.sel);
1995
2023
  checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
2024
+ checkRef(["isolation", "none"], routing.isolation?.none);
2025
+ checkRef(["isolation", "container"], routing.isolation?.container);
2026
+ checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
2027
+ if (routing.skills) {
2028
+ for (const [skill, value] of Object.entries(routing.skills)) {
2029
+ checkRef(["skills", skill], value);
2030
+ }
2031
+ }
2032
+ if (routing.modes) {
2033
+ for (const [mode, value] of Object.entries(routing.modes)) {
2034
+ checkRef(["modes", mode], value);
2035
+ }
2036
+ }
1996
2037
  return issues;
1997
2038
  }
1998
- function validateWorkflowConfig(config) {
2039
+ function routingWarnings(routing, knownSkillNames) {
2040
+ const warnings = [];
2041
+ if (knownSkillNames.length > 0 && routing.skills) {
2042
+ const known = new Set(knownSkillNames);
2043
+ for (const name of Object.keys(routing.skills)) {
2044
+ if (known.has(name)) continue;
2045
+ warnings.push(
2046
+ `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.`
2047
+ );
2048
+ }
2049
+ }
2050
+ if (routing.modes) {
2051
+ const standardModes = new Set(import_types2.STANDARD_COGNITIVE_MODES);
2052
+ for (const mode of Object.keys(routing.modes)) {
2053
+ if (standardModes.has(mode)) continue;
2054
+ warnings.push(
2055
+ `routing.modes.${mode} is not in STANDARD_COGNITIVE_MODES (${[...import_types2.STANDARD_COGNITIVE_MODES].join(", ")}). Custom cognitive modes are allowed but uncommon; verify this is not a typo.`
2056
+ );
2057
+ }
2058
+ }
2059
+ return warnings;
2060
+ }
2061
+ function validateWorkflowConfig(config, options = {}) {
1999
2062
  if (!config || typeof config !== "object")
2000
2063
  return (0, import_types2.Err)(new Error("Config is missing or not an object"));
2001
2064
  const c = config;
@@ -2011,6 +2074,7 @@ function validateWorkflowConfig(config) {
2011
2074
  if (!hasLegacyBackend && !hasModernBackends) {
2012
2075
  return (0, import_types2.Err)(new Error("Config must define agent.backend or agent.backends."));
2013
2076
  }
2077
+ const warnings = [];
2014
2078
  if (hasModernBackends) {
2015
2079
  const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
2016
2080
  if (!backendsParsed.success) {
@@ -2021,9 +2085,10 @@ function validateWorkflowConfig(config) {
2021
2085
  return (0, import_types2.Err)(new Error(`agent.routing: ${routingParsed.error.message}`));
2022
2086
  }
2023
2087
  if (routingParsed.data) {
2088
+ const routingData = routingParsed.data;
2024
2089
  const cross = crossFieldRoutingIssues(
2025
2090
  backendsParsed.data,
2026
- routingParsed.data
2091
+ routingData
2027
2092
  );
2028
2093
  if (cross.length > 0) {
2029
2094
  return (0, import_types2.Err)(
@@ -2032,9 +2097,10 @@ function validateWorkflowConfig(config) {
2032
2097
  )
2033
2098
  );
2034
2099
  }
2100
+ warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
2035
2101
  }
2036
2102
  }
2037
- return (0, import_types2.Ok)(config);
2103
+ return (0, import_types2.Ok)({ config, warnings });
2038
2104
  }
2039
2105
  function getDefaultConfig() {
2040
2106
  return {
@@ -2087,11 +2153,55 @@ function getDefaultConfig() {
2087
2153
  };
2088
2154
  }
2089
2155
 
2156
+ // src/workflow/skill-catalog.ts
2157
+ var fs6 = __toESM(require("fs"));
2158
+ var path6 = __toESM(require("path"));
2159
+ var import_yaml = require("yaml");
2160
+ function discoverSkillCatalog(projectRoot) {
2161
+ const skillsRoot = path6.join(projectRoot, "agents", "skills");
2162
+ if (!fs6.existsSync(skillsRoot)) return [];
2163
+ const byName = /* @__PURE__ */ new Map();
2164
+ let hosts;
2165
+ try {
2166
+ hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
2167
+ } catch {
2168
+ return [];
2169
+ }
2170
+ for (const host of hosts) {
2171
+ if (!host.isDirectory()) continue;
2172
+ const hostDir = path6.join(skillsRoot, host.name);
2173
+ let skills;
2174
+ try {
2175
+ skills = fs6.readdirSync(hostDir, { withFileTypes: true });
2176
+ } catch {
2177
+ continue;
2178
+ }
2179
+ for (const skill of skills) {
2180
+ if (!skill.isDirectory()) continue;
2181
+ const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
2182
+ if (!fs6.existsSync(skillYamlPath)) continue;
2183
+ try {
2184
+ const content = fs6.readFileSync(skillYamlPath, "utf-8");
2185
+ const parsed = (0, import_yaml.parse)(content);
2186
+ if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
2187
+ const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
2188
+ byName.set(parsed.name, entry);
2189
+ }
2190
+ } catch {
2191
+ }
2192
+ }
2193
+ }
2194
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2195
+ }
2196
+ function discoverSkillCatalogNames(projectRoot) {
2197
+ return discoverSkillCatalog(projectRoot).map((e) => e.name);
2198
+ }
2199
+
2090
2200
  // src/workflow/loader.ts
2091
2201
  var WorkflowLoader = class {
2092
2202
  async loadWorkflow(filePath) {
2093
2203
  try {
2094
- const content = await fs6.readFile(filePath, "utf-8");
2204
+ const content = await fs7.readFile(filePath, "utf-8");
2095
2205
  const parts = content.split("---");
2096
2206
  if (parts.length < 3) {
2097
2207
  return (0, import_types3.Err)(
@@ -2102,14 +2212,17 @@ var WorkflowLoader = class {
2102
2212
  }
2103
2213
  const yamlContent = parts[1].trim();
2104
2214
  const promptTemplate = parts.slice(2).join("---").trim();
2105
- const configData = (0, import_yaml.parse)(yamlContent);
2106
- const configResult = validateWorkflowConfig(configData);
2215
+ const configData = (0, import_yaml2.parse)(yamlContent);
2216
+ const projectRoot = path7.dirname(path7.resolve(filePath));
2217
+ const knownSkillNames = discoverSkillCatalogNames(projectRoot);
2218
+ const configResult = validateWorkflowConfig(configData, { knownSkillNames });
2107
2219
  if (!configResult.ok) {
2108
2220
  return (0, import_types3.Err)(configResult.error);
2109
2221
  }
2110
2222
  return (0, import_types3.Ok)({
2111
- config: configResult.value,
2112
- promptTemplate
2223
+ config: configResult.value.config,
2224
+ promptTemplate,
2225
+ warnings: configResult.value.warnings
2113
2226
  });
2114
2227
  } catch (error) {
2115
2228
  return (0, import_types3.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2118,7 +2231,7 @@ var WorkflowLoader = class {
2118
2231
  };
2119
2232
 
2120
2233
  // src/tracker/adapters/roadmap.ts
2121
- var fs7 = __toESM(require("fs/promises"));
2234
+ var fs8 = __toESM(require("fs/promises"));
2122
2235
  var import_node_crypto2 = require("crypto");
2123
2236
  var import_core = require("@harness-engineering/core");
2124
2237
  var import_types4 = require("@harness-engineering/types");
@@ -2149,7 +2262,7 @@ var RoadmapTrackerAdapter = class {
2149
2262
  async fetchIssuesByStates(stateNames) {
2150
2263
  try {
2151
2264
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2152
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2265
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2153
2266
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2154
2267
  if (!roadmapResult.ok) return roadmapResult;
2155
2268
  const issues = [];
@@ -2181,7 +2294,7 @@ var RoadmapTrackerAdapter = class {
2181
2294
  if (!terminal) {
2182
2295
  return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
2183
2296
  }
2184
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2297
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2185
2298
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2186
2299
  if (!roadmapResult.ok) return roadmapResult;
2187
2300
  const roadmap = roadmapResult.value;
@@ -2190,7 +2303,7 @@ var RoadmapTrackerAdapter = class {
2190
2303
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2191
2304
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2192
2305
  target.status = terminal;
2193
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2306
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2194
2307
  return (0, import_types4.Ok)(void 0);
2195
2308
  } catch (error) {
2196
2309
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2204,19 +2317,22 @@ var RoadmapTrackerAdapter = class {
2204
2317
  async claimIssue(issueId, orchestratorId) {
2205
2318
  try {
2206
2319
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2207
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2320
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2208
2321
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2209
2322
  if (!roadmapResult.ok) return roadmapResult;
2210
2323
  const roadmap = roadmapResult.value;
2211
2324
  const target = this.findFeatureById(roadmap.milestones, issueId);
2212
2325
  if (!target) return (0, import_types4.Ok)(void 0);
2326
+ if (target.assignee != null && target.assignee !== orchestratorId) {
2327
+ return (0, import_types4.Ok)(void 0);
2328
+ }
2213
2329
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2214
2330
  return (0, import_types4.Ok)(void 0);
2215
2331
  }
2216
2332
  target.status = "in-progress";
2217
2333
  target.assignee = orchestratorId;
2218
2334
  target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2219
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2335
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2220
2336
  return (0, import_types4.Ok)(void 0);
2221
2337
  } catch (error) {
2222
2338
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2233,7 +2349,7 @@ var RoadmapTrackerAdapter = class {
2233
2349
  if (!activeState) {
2234
2350
  return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
2235
2351
  }
2236
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2352
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2237
2353
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2238
2354
  if (!roadmapResult.ok) return roadmapResult;
2239
2355
  const roadmap = roadmapResult.value;
@@ -2245,7 +2361,7 @@ var RoadmapTrackerAdapter = class {
2245
2361
  target.status = activeState;
2246
2362
  target.assignee = null;
2247
2363
  target.updatedAt = null;
2248
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2364
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2249
2365
  return (0, import_types4.Ok)(void 0);
2250
2366
  } catch (error) {
2251
2367
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2267,7 +2383,7 @@ var RoadmapTrackerAdapter = class {
2267
2383
  async fetchIssueStatesByIds(issueIds) {
2268
2384
  try {
2269
2385
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2270
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2386
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2271
2387
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2272
2388
  if (!roadmapResult.ok) return roadmapResult;
2273
2389
  const issueMap = /* @__PURE__ */ new Map();
@@ -2332,8 +2448,8 @@ var LinearGraphQLStub = class {
2332
2448
  };
2333
2449
 
2334
2450
  // src/workspace/manager.ts
2335
- var fs8 = __toESM(require("fs/promises"));
2336
- var path6 = __toESM(require("path"));
2451
+ var fs9 = __toESM(require("fs/promises"));
2452
+ var path8 = __toESM(require("path"));
2337
2453
  var import_node_child_process2 = require("child_process");
2338
2454
  var import_node_util2 = require("util");
2339
2455
  var import_types6 = require("@harness-engineering/types");
@@ -2364,15 +2480,15 @@ var WorkspaceManager = class {
2364
2480
  */
2365
2481
  resolvePath(identifier) {
2366
2482
  const sanitized = this.sanitizeIdentifier(identifier);
2367
- return path6.join(this.config.root, sanitized);
2483
+ return path8.join(this.config.root, sanitized);
2368
2484
  }
2369
2485
  /**
2370
2486
  * Discovers the git repository root from the workspace root directory.
2371
2487
  */
2372
2488
  async getRepoRoot() {
2373
2489
  if (this.repoRoot) return this.repoRoot;
2374
- const root = path6.resolve(this.config.root);
2375
- await fs8.mkdir(root, { recursive: true });
2490
+ const root = path8.resolve(this.config.root);
2491
+ await fs9.mkdir(root, { recursive: true });
2376
2492
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2377
2493
  this.repoRoot = stdout.trim();
2378
2494
  return this.repoRoot;
@@ -2383,23 +2499,23 @@ var WorkspaceManager = class {
2383
2499
  */
2384
2500
  async ensureWorkspace(identifier) {
2385
2501
  try {
2386
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2502
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2387
2503
  try {
2388
- await fs8.access(path6.join(workspacePath, ".git"));
2504
+ await fs9.access(path8.join(workspacePath, ".git"));
2389
2505
  const repoRoot2 = await this.getRepoRoot();
2390
2506
  try {
2391
2507
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2392
2508
  } catch {
2393
- await fs8.rm(workspacePath, { recursive: true, force: true });
2509
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2394
2510
  }
2395
2511
  } catch {
2396
2512
  try {
2397
- await fs8.access(workspacePath);
2513
+ await fs9.access(workspacePath);
2398
2514
  const repoRoot2 = await this.getRepoRoot();
2399
2515
  try {
2400
2516
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2401
2517
  } catch {
2402
- await fs8.rm(workspacePath, { recursive: true, force: true });
2518
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2403
2519
  }
2404
2520
  } catch {
2405
2521
  }
@@ -2495,7 +2611,7 @@ var WorkspaceManager = class {
2495
2611
  async exists(identifier) {
2496
2612
  try {
2497
2613
  const workspacePath = this.resolvePath(identifier);
2498
- await fs8.access(workspacePath);
2614
+ await fs9.access(workspacePath);
2499
2615
  return true;
2500
2616
  } catch {
2501
2617
  return false;
@@ -2508,9 +2624,9 @@ var WorkspaceManager = class {
2508
2624
  */
2509
2625
  async findPushedBranch(identifier) {
2510
2626
  try {
2511
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2627
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2512
2628
  try {
2513
- await fs8.access(path6.join(workspacePath, ".git"));
2629
+ await fs9.access(path8.join(workspacePath, ".git"));
2514
2630
  } catch {
2515
2631
  return null;
2516
2632
  }
@@ -2616,12 +2732,12 @@ var WorkspaceManager = class {
2616
2732
  */
2617
2733
  async removeWorkspace(identifier) {
2618
2734
  try {
2619
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2735
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2620
2736
  try {
2621
2737
  const repoRoot = await this.getRepoRoot();
2622
2738
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2623
2739
  } catch {
2624
- await fs8.rm(workspacePath, { recursive: true, force: true });
2740
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2625
2741
  }
2626
2742
  return (0, import_types6.Ok)(void 0);
2627
2743
  } catch (error) {
@@ -2646,7 +2762,7 @@ var WorkspaceHooks = class {
2646
2762
  if (!command) {
2647
2763
  return (0, import_types7.Ok)(void 0);
2648
2764
  }
2649
- return new Promise((resolve7) => {
2765
+ return new Promise((resolve8) => {
2650
2766
  const filteredEnv = {};
2651
2767
  for (const [k, v] of Object.entries(process.env)) {
2652
2768
  if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
@@ -2659,19 +2775,19 @@ var WorkspaceHooks = class {
2659
2775
  });
2660
2776
  const timeout = setTimeout(() => {
2661
2777
  child.kill();
2662
- resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2778
+ resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2663
2779
  }, this.config.timeoutMs);
2664
2780
  child.on("exit", (code) => {
2665
2781
  clearTimeout(timeout);
2666
2782
  if (code === 0) {
2667
- resolve7((0, import_types7.Ok)(void 0));
2783
+ resolve8((0, import_types7.Ok)(void 0));
2668
2784
  } else {
2669
- resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2785
+ resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2670
2786
  }
2671
2787
  });
2672
2788
  child.on("error", (error) => {
2673
2789
  clearTimeout(timeout);
2674
- resolve7((0, import_types7.Err)(error));
2790
+ resolve8((0, import_types7.Err)(error));
2675
2791
  });
2676
2792
  });
2677
2793
  }
@@ -2709,7 +2825,7 @@ var MockBackend = class {
2709
2825
  content: "Thinking...",
2710
2826
  sessionId: session.sessionId
2711
2827
  };
2712
- await new Promise((resolve7) => setTimeout(resolve7, 100));
2828
+ await new Promise((resolve8) => setTimeout(resolve8, 100));
2713
2829
  yield {
2714
2830
  type: "thought",
2715
2831
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2761,12 +2877,12 @@ var PromptRenderer = class {
2761
2877
 
2762
2878
  // src/orchestrator.ts
2763
2879
  var import_node_events = require("events");
2764
- var path19 = __toESM(require("path"));
2880
+ var path21 = __toESM(require("path"));
2765
2881
  var import_node_crypto16 = require("crypto");
2766
2882
  var import_core14 = require("@harness-engineering/core");
2767
2883
 
2768
2884
  // src/intelligence/pipeline-runner.ts
2769
- var path7 = __toESM(require("path"));
2885
+ var path9 = __toESM(require("path"));
2770
2886
  var import_intelligence = require("@harness-engineering/intelligence");
2771
2887
  var import_core2 = require("@harness-engineering/core");
2772
2888
  var CONNECTION_ERROR_PATTERNS = [
@@ -2885,7 +3001,7 @@ var IntelligencePipelineRunner = class {
2885
3001
  }
2886
3002
  async loadGraphStore() {
2887
3003
  try {
2888
- const graphDir = path7.join(this.ctx.config.workspace.root, "..", "graph");
3004
+ const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
2889
3005
  const loaded = await this.ctx.graphStore.load(graphDir);
2890
3006
  if (loaded) {
2891
3007
  this.ctx.logger.info("Graph store loaded from disk");
@@ -3163,7 +3279,7 @@ var IntelligencePipelineRunner = class {
3163
3279
  };
3164
3280
 
3165
3281
  // src/completion/handler.ts
3166
- var path8 = __toESM(require("path"));
3282
+ var path10 = __toESM(require("path"));
3167
3283
  var import_core3 = require("@harness-engineering/core");
3168
3284
  var CompletionHandler = class {
3169
3285
  ctx;
@@ -3246,7 +3362,7 @@ var CompletionHandler = class {
3246
3362
  result: outcome.result
3247
3363
  });
3248
3364
  if (this.ctx.graphStore) {
3249
- const graphDir = path8.join(this.ctx.config.workspace.root, "..", "graph");
3365
+ const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
3250
3366
  await this.ctx.graphStore.save(graphDir);
3251
3367
  }
3252
3368
  } catch (err) {
@@ -3728,11 +3844,11 @@ function detectLegacyFields(agent) {
3728
3844
  }
3729
3845
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3730
3846
  const warnings = [];
3731
- for (const path22 of presentLegacy) {
3732
- if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
3733
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
3847
+ for (const path24 of presentLegacy) {
3848
+ if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
3849
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
3734
3850
  warnings.push(
3735
- `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3851
+ `Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3736
3852
  );
3737
3853
  }
3738
3854
  return warnings;
@@ -3760,7 +3876,7 @@ function migrateAgentConfig(agent) {
3760
3876
  }
3761
3877
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3762
3878
  const warnings = presentLegacy.map(
3763
- (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3879
+ (path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3764
3880
  );
3765
3881
  return {
3766
3882
  config: { ...agent, backends, routing },
@@ -3823,61 +3939,160 @@ function synthesizeLocal(agent) {
3823
3939
  }
3824
3940
 
3825
3941
  // src/agent/backend-router.ts
3942
+ function toArray(value) {
3943
+ return Array.isArray(value) ? value : [value];
3944
+ }
3826
3945
  var BackendRouter = class {
3827
3946
  backends;
3828
3947
  routing;
3948
+ decisionBus;
3829
3949
  constructor(opts) {
3830
3950
  this.backends = opts.backends;
3831
3951
  this.routing = opts.routing;
3952
+ this.decisionBus = opts.decisionBus;
3832
3953
  this.validateReferences();
3833
3954
  }
3834
3955
  /**
3835
- * Returns the backend name for a given use case.
3956
+ * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
3957
+ *
3958
+ * @param useCase the routing query
3959
+ * @param opts.invocationOverride if set and the named backend exists,
3960
+ * beats all other sources (D7 — the `--backend <name>` escape hatch)
3961
+ */
3962
+ resolve(useCase, opts) {
3963
+ const startedAt = performance.now();
3964
+ const path24 = [];
3965
+ const tryChain = (source, value) => {
3966
+ if (value === void 0) return void 0;
3967
+ for (const name of toArray(value)) {
3968
+ const step = { source, candidate: name, outcome: "considered" };
3969
+ path24.push(step);
3970
+ if (this.backends[name]) {
3971
+ step.outcome = "chosen";
3972
+ return name;
3973
+ }
3974
+ step.outcome = "unknown-backend";
3975
+ }
3976
+ return void 0;
3977
+ };
3978
+ const decide = (backendName) => {
3979
+ const def = this.backends[backendName];
3980
+ if (!def) {
3981
+ throw new Error(
3982
+ `BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
3983
+ );
3984
+ }
3985
+ return {
3986
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3987
+ useCase,
3988
+ resolutionPath: path24,
3989
+ backendName,
3990
+ backendType: def.type,
3991
+ durationMs: performance.now() - startedAt
3992
+ };
3993
+ };
3994
+ const emitAndReturn = (decision) => {
3995
+ this.decisionBus?.emit(decision);
3996
+ return decision;
3997
+ };
3998
+ const fromInvocation = tryChain(
3999
+ "invocation",
4000
+ opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
4001
+ );
4002
+ if (fromInvocation) return emitAndReturn(decide(fromInvocation));
4003
+ if (useCase.kind === "skill") {
4004
+ const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
4005
+ if (fromSkill) return emitAndReturn(decide(fromSkill));
4006
+ }
4007
+ const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
4008
+ if (mode !== void 0) {
4009
+ const fromMode = tryChain("mode", this.routing.modes?.[mode]);
4010
+ if (fromMode) return emitAndReturn(decide(fromMode));
4011
+ }
4012
+ const fromExisting = this.resolveExistingUseCase(useCase);
4013
+ if (fromExisting !== void 0) {
4014
+ const chained = tryChain("tier", fromExisting);
4015
+ if (chained) return emitAndReturn(decide(chained));
4016
+ }
4017
+ const fromDefault = tryChain("default", this.routing.default);
4018
+ if (fromDefault) return emitAndReturn(decide(fromDefault));
4019
+ const knownList = Object.keys(this.backends).join(", ") || "(none)";
4020
+ throw new Error(
4021
+ `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
4022
+ );
4023
+ }
4024
+ /**
4025
+ * Returns the {@link BackendDef} reference for the resolved name.
4026
+ * Identity-equal to the entry in `backends` (no copy) so callers
4027
+ * relying on reference equality (SC21) continue to work.
4028
+ */
4029
+ resolveDefinition(useCase, opts) {
4030
+ const decision = this.resolve(useCase, opts);
4031
+ const def = this.backends[decision.backendName];
4032
+ if (!def) {
4033
+ throw new Error(
4034
+ `BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
4035
+ );
4036
+ }
4037
+ return def;
4038
+ }
4039
+ /**
4040
+ * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
4041
+ * for callers that need both. Replaces the previous pattern of
4042
+ * `resolveDefinition(useCase) + resolve(useCase)` which produced two
4043
+ * RoutingDecision emissions per dispatch — doubling routing-decision
4044
+ * log volume now that Phase 4 emits.
3836
4045
  *
3837
- * - `tier`: per-tier override, falling back to `routing.default`.
3838
- * - `intelligence`: per-layer override under `routing.intelligence`,
3839
- * falling back to `routing.default`.
3840
- * - `maintenance` / `chat`: always `routing.default`.
4046
+ * Identity-equal `BackendDef` (no copy) so callers relying on
4047
+ * reference equality (SC21) continue to work.
3841
4048
  */
3842
- resolve(useCase) {
4049
+ resolveDecisionAndDef(useCase, opts) {
4050
+ const decision = this.resolve(useCase, opts);
4051
+ const def = this.backends[decision.backendName];
4052
+ if (!def) {
4053
+ throw new Error(
4054
+ `BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
4055
+ );
4056
+ }
4057
+ return { decision, def };
4058
+ }
4059
+ /**
4060
+ * The pre-Spec-B resolution helper: returns the configured
4061
+ * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
4062
+ * use cases (or `undefined` for skill/mode use cases, which are owned
4063
+ * by the per-skill / per-mode steps in {@link resolve}). Returning
4064
+ * `undefined` lets the caller fall through to `routing.default`.
4065
+ */
4066
+ resolveExistingUseCase(useCase) {
3843
4067
  switch (useCase.kind) {
3844
4068
  case "tier": {
3845
- const named = this.routing[useCase.tier];
3846
- return named ?? this.routing.default;
4069
+ const tierMap = this.routing;
4070
+ return tierMap[useCase.tier];
3847
4071
  }
3848
4072
  case "intelligence": {
3849
4073
  const intel = this.routing.intelligence;
3850
- return intel?.[useCase.layer] ?? this.routing.default;
4074
+ return intel?.[useCase.layer];
3851
4075
  }
3852
4076
  case "isolation": {
3853
4077
  const iso = this.routing.isolation;
3854
- return iso?.[useCase.tier] ?? this.routing.default;
4078
+ return iso?.[useCase.tier];
3855
4079
  }
3856
4080
  case "maintenance":
3857
4081
  case "chat":
3858
- return this.routing.default;
3859
- }
3860
- }
3861
- /**
3862
- * Returns the BackendDef reference for the resolved name. Returns the
3863
- * exact reference held in `backends` (no copy) so identity comparisons
3864
- * succeed (SC21).
3865
- */
3866
- resolveDefinition(useCase) {
3867
- const name = this.resolve(useCase);
3868
- const def = this.backends[name];
3869
- if (!def) {
3870
- throw new Error(
3871
- `BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
3872
- );
4082
+ return void 0;
4083
+ case "skill":
4084
+ case "mode":
4085
+ return void 0;
3873
4086
  }
3874
- return def;
3875
4087
  }
3876
4088
  validateReferences() {
3877
4089
  const known = new Set(Object.keys(this.backends));
3878
4090
  const missing = [];
3879
- const check = (path22, name) => {
3880
- if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
4091
+ const check = (label, value) => {
4092
+ if (value === void 0) return;
4093
+ for (const name of toArray(value)) {
4094
+ if (!known.has(name)) missing.push({ path: label, name });
4095
+ }
3881
4096
  };
3882
4097
  check("default", this.routing.default);
3883
4098
  check("quick-fix", this.routing["quick-fix"]);
@@ -3889,8 +4104,14 @@ var BackendRouter = class {
3889
4104
  check("isolation.none", this.routing.isolation?.none);
3890
4105
  check("isolation.container", this.routing.isolation?.container);
3891
4106
  check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
4107
+ for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
4108
+ check(`skills.${skill}`, value);
4109
+ }
4110
+ for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
4111
+ check(`modes.${mode}`, value);
4112
+ }
3892
4113
  if (missing.length > 0) {
3893
- const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
4114
+ const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
3894
4115
  const known_ = [...known].join(", ") || "(none)";
3895
4116
  throw new Error(
3896
4117
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3904,11 +4125,11 @@ var import_node_child_process4 = require("child_process");
3904
4125
  var readline = __toESM(require("readline"));
3905
4126
  var import_node_crypto3 = require("crypto");
3906
4127
  var import_types10 = require("@harness-engineering/types");
3907
- function resolveExitCode(code, command, resolve7) {
4128
+ function resolveExitCode(code, command, resolve8) {
3908
4129
  if (code === 0) {
3909
- resolve7((0, import_types10.Ok)(void 0));
4130
+ resolve8((0, import_types10.Ok)(void 0));
3910
4131
  } else {
3911
- resolve7(
4132
+ resolve8(
3912
4133
  (0, import_types10.Err)({
3913
4134
  category: "agent_not_found",
3914
4135
  message: `Claude command '${command}' not found or failed`
@@ -3916,8 +4137,8 @@ function resolveExitCode(code, command, resolve7) {
3916
4137
  );
3917
4138
  }
3918
4139
  }
3919
- function resolveSpawnError(command, resolve7) {
3920
- resolve7((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4140
+ function resolveSpawnError(command, resolve8) {
4141
+ resolve8((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3921
4142
  }
3922
4143
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3923
4144
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4230,10 +4451,10 @@ var ClaudeBackend = class {
4230
4451
  errRl.close();
4231
4452
  }
4232
4453
  if (exitCode === null) {
4233
- await new Promise((resolve7) => {
4454
+ await new Promise((resolve8) => {
4234
4455
  child.on("exit", (code) => {
4235
4456
  exitCode = code;
4236
- resolve7(null);
4457
+ resolve8(null);
4237
4458
  });
4238
4459
  });
4239
4460
  }
@@ -4255,10 +4476,10 @@ var ClaudeBackend = class {
4255
4476
  return (0, import_types10.Ok)(void 0);
4256
4477
  }
4257
4478
  async healthCheck() {
4258
- return new Promise((resolve7) => {
4479
+ return new Promise((resolve8) => {
4259
4480
  const child = (0, import_node_child_process4.spawn)(this.command, ["--version"]);
4260
- child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
4261
- child.on("error", () => resolveSpawnError(this.command, resolve7));
4481
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
4482
+ child.on("error", () => resolveSpawnError(this.command, resolve8));
4262
4483
  });
4263
4484
  }
4264
4485
  };
@@ -5172,14 +5393,14 @@ var SshBackend = class {
5172
5393
  async healthCheck() {
5173
5394
  const args = [...this.buildSshArgs()];
5174
5395
  args[args.length - 1] = "true";
5175
- return new Promise((resolve7) => {
5396
+ return new Promise((resolve8) => {
5176
5397
  let child;
5177
5398
  try {
5178
5399
  child = this.spawnImpl(this.config.sshBinary, args, {
5179
5400
  stdio: ["ignore", "ignore", "pipe"]
5180
5401
  });
5181
5402
  } catch (err) {
5182
- resolve7(
5403
+ resolve8(
5183
5404
  (0, import_types16.Err)({
5184
5405
  category: "agent_not_found",
5185
5406
  message: err instanceof Error ? err.message : "failed to spawn ssh"
@@ -5200,9 +5421,9 @@ var SshBackend = class {
5200
5421
  child.on("close", (code) => {
5201
5422
  clearTimeout(timer);
5202
5423
  if (code === 0) {
5203
- resolve7((0, import_types16.Ok)(void 0));
5424
+ resolve8((0, import_types16.Ok)(void 0));
5204
5425
  } else {
5205
- resolve7(
5426
+ resolve8(
5206
5427
  (0, import_types16.Err)({
5207
5428
  category: "agent_not_found",
5208
5429
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
@@ -5212,7 +5433,7 @@ var SshBackend = class {
5212
5433
  });
5213
5434
  child.on("error", (err) => {
5214
5435
  clearTimeout(timer);
5215
- resolve7((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
5436
+ resolve8((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
5216
5437
  });
5217
5438
  });
5218
5439
  }
@@ -5260,13 +5481,13 @@ async function* readLines(stream) {
5260
5481
  if (buffer.length > 0) yield buffer;
5261
5482
  }
5262
5483
  function waitForExit(child) {
5263
- return new Promise((resolve7) => {
5484
+ return new Promise((resolve8) => {
5264
5485
  if (child.exitCode !== null) {
5265
- resolve7(child.exitCode);
5486
+ resolve8(child.exitCode);
5266
5487
  return;
5267
5488
  }
5268
- child.once("close", (code) => resolve7(code));
5269
- child.once("error", () => resolve7(null));
5489
+ child.once("close", (code) => resolve8(code));
5490
+ child.once("error", () => resolve8(null));
5270
5491
  });
5271
5492
  }
5272
5493
 
@@ -5453,14 +5674,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
5453
5674
  return out;
5454
5675
  }
5455
5676
  runOneShot(binary, args) {
5456
- return new Promise((resolve7) => {
5677
+ return new Promise((resolve8) => {
5457
5678
  let child;
5458
5679
  try {
5459
5680
  child = this.spawnImpl(binary, args, {
5460
5681
  stdio: ["ignore", "pipe", "pipe"]
5461
5682
  });
5462
5683
  } catch (err) {
5463
- resolve7(
5684
+ resolve8(
5464
5685
  (0, import_types17.Err)({
5465
5686
  category: "agent_not_found",
5466
5687
  message: err instanceof Error ? err.message : "failed to spawn runtime"
@@ -5485,9 +5706,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
5485
5706
  child.on("close", (code) => {
5486
5707
  clearTimeout(timer);
5487
5708
  if (code === 0) {
5488
- resolve7((0, import_types17.Ok)(stdout));
5709
+ resolve8((0, import_types17.Ok)(stdout));
5489
5710
  } else {
5490
- resolve7(
5711
+ resolve8(
5491
5712
  (0, import_types17.Err)({
5492
5713
  category: "response_error",
5493
5714
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
@@ -5497,7 +5718,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5497
5718
  });
5498
5719
  child.on("error", (err) => {
5499
5720
  clearTimeout(timer);
5500
- resolve7((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
5721
+ resolve8((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
5501
5722
  });
5502
5723
  });
5503
5724
  }
@@ -5557,13 +5778,13 @@ async function* readLines2(stream) {
5557
5778
  if (buffer.length > 0) yield buffer;
5558
5779
  }
5559
5780
  function waitForExit2(child) {
5560
- return new Promise((resolve7) => {
5781
+ return new Promise((resolve8) => {
5561
5782
  if (child.exitCode !== null) {
5562
- resolve7(child.exitCode);
5783
+ resolve8(child.exitCode);
5563
5784
  return;
5564
5785
  }
5565
- child.once("close", (code) => resolve7(code));
5566
- child.once("error", () => resolve7(null));
5786
+ child.once("close", (code) => resolve8(code));
5787
+ child.once("error", () => resolve8(null));
5567
5788
  });
5568
5789
  }
5569
5790
 
@@ -5761,13 +5982,13 @@ var ContainerBackend = class {
5761
5982
  var import_node_child_process7 = require("child_process");
5762
5983
  var import_types19 = require("@harness-engineering/types");
5763
5984
  function dockerExec(args) {
5764
- return new Promise((resolve7, reject) => {
5985
+ return new Promise((resolve8, reject) => {
5765
5986
  (0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
5766
5987
  if (error) {
5767
5988
  reject(error);
5768
5989
  return;
5769
5990
  }
5770
- resolve7(stdout.trim());
5991
+ resolve8(stdout.trim());
5771
5992
  });
5772
5993
  });
5773
5994
  }
@@ -5826,11 +6047,11 @@ var DockerRuntime = class {
5826
6047
  } finally {
5827
6048
  rl.close();
5828
6049
  }
5829
- const exitCode = await new Promise((resolve7) => {
6050
+ const exitCode = await new Promise((resolve8) => {
5830
6051
  if (child.exitCode !== null) {
5831
- resolve7(child.exitCode);
6052
+ resolve8(child.exitCode);
5832
6053
  } else {
5833
- child.on("exit", (code) => resolve7(code ?? 1));
6054
+ child.on("exit", (code) => resolve8(code ?? 1));
5834
6055
  }
5835
6056
  });
5836
6057
  return exitCode;
@@ -5889,13 +6110,13 @@ var EnvSecretBackend = class {
5889
6110
  var import_node_child_process8 = require("child_process");
5890
6111
  var import_types21 = require("@harness-engineering/types");
5891
6112
  function opExec(args) {
5892
- return new Promise((resolve7, reject) => {
6113
+ return new Promise((resolve8, reject) => {
5893
6114
  (0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
5894
6115
  if (error) {
5895
6116
  reject(error);
5896
6117
  return;
5897
6118
  }
5898
- resolve7(stdout.trim());
6119
+ resolve8(stdout.trim());
5899
6120
  });
5900
6121
  });
5901
6122
  }
@@ -5938,13 +6159,13 @@ var OnePasswordSecretBackend = class {
5938
6159
  var import_node_child_process9 = require("child_process");
5939
6160
  var import_types22 = require("@harness-engineering/types");
5940
6161
  function vaultExec(args, env) {
5941
- return new Promise((resolve7, reject) => {
6162
+ return new Promise((resolve8, reject) => {
5942
6163
  (0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5943
6164
  if (error) {
5944
6165
  reject(error);
5945
6166
  return;
5946
6167
  }
5947
- resolve7(stdout.trim());
6168
+ resolve8(stdout.trim());
5948
6169
  });
5949
6170
  });
5950
6171
  }
@@ -6019,7 +6240,11 @@ var OrchestratorBackendFactory = class {
6019
6240
  opts;
6020
6241
  constructor(opts) {
6021
6242
  this.opts = opts;
6022
- this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
6243
+ this.router = new BackendRouter({
6244
+ backends: opts.backends,
6245
+ routing: opts.routing,
6246
+ ...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
6247
+ });
6023
6248
  }
6024
6249
  /**
6025
6250
  * Resolve `useCase` to a backend name, materialize a fresh
@@ -6038,12 +6263,21 @@ var OrchestratorBackendFactory = class {
6038
6263
  * is `undefined` for pure-modern configs. Threading the routed name
6039
6264
  * through dispatch eliminates that gap.
6040
6265
  */
6041
- resolveName(useCase) {
6042
- return this.router.resolve(useCase);
6266
+ resolveName(useCase, opts) {
6267
+ return this.router.resolve(useCase, opts).backendName;
6043
6268
  }
6044
- forUseCase(useCase) {
6045
- const def = this.router.resolveDefinition(useCase);
6046
- const name = this.router.resolve(useCase);
6269
+ /**
6270
+ * Spec B Phase 1: expose the underlying router for callers that need
6271
+ * it directly (e.g., {@link buildIntelligencePipeline} for the
6272
+ * I1 SEL/PESL comparison fix). Read-only access; consumers must not
6273
+ * mutate router state.
6274
+ */
6275
+ getRouter() {
6276
+ return this.router;
6277
+ }
6278
+ forUseCase(useCase, opts) {
6279
+ const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
6280
+ const name = decision.backendName;
6047
6281
  let backend;
6048
6282
  const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
6049
6283
  if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
@@ -6207,15 +6441,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
6207
6441
 
6208
6442
  // src/agent/intelligence-factory.ts
6209
6443
  function buildIntelligencePipeline(deps) {
6210
- const { config } = deps;
6444
+ const { config, router } = deps;
6211
6445
  const intel = config.intelligence;
6212
6446
  if (!intel?.enabled) return null;
6213
6447
  const selProvider = buildAnalysisProviderForLayer("sel", deps);
6214
6448
  if (!selProvider) return null;
6215
- const routing = config.agent.routing;
6216
- const peslName = routing?.intelligence?.pesl;
6217
- const selName = routing?.intelligence?.sel ?? routing?.default;
6218
- const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6449
+ const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
6450
+ const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
6451
+ const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6219
6452
  const peslModel = intel.models?.pesl ?? config.agent.model;
6220
6453
  const graphStore = new import_graph.GraphStore();
6221
6454
  const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
@@ -6232,7 +6465,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
6232
6465
  const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
6233
6466
  return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
6234
6467
  }
6235
- const routed = resolveRoutedBackend(layer, config, logger);
6468
+ const routed = resolveRoutedBackend(layer, deps);
6236
6469
  if (!routed) return null;
6237
6470
  const { name, def } = routed;
6238
6471
  const resolver = localResolvers.get(name);
@@ -6257,20 +6490,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
6257
6490
  logger
6258
6491
  });
6259
6492
  }
6260
- function resolveRoutedBackend(layer, config, logger) {
6261
- const routing = config.agent.routing;
6493
+ function resolveRoutedBackend(layer, deps) {
6494
+ const { config, router, logger } = deps;
6262
6495
  const backends = config.agent.backends;
6263
- if (!routing || !backends) return null;
6264
- const layerName = routing.intelligence?.[layer];
6265
- const name = layerName ?? routing.default;
6266
- const def = backends[name];
6267
- if (!def) {
6496
+ if (!backends || !router) return null;
6497
+ try {
6498
+ const decision = router.resolve({ kind: "intelligence", layer });
6499
+ const def = backends[decision.backendName];
6500
+ if (!def) {
6501
+ logger.warn(
6502
+ `Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
6503
+ );
6504
+ return null;
6505
+ }
6506
+ return { name: decision.backendName, def };
6507
+ } catch (err) {
6268
6508
  logger.warn(
6269
- `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
6509
+ `Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
6270
6510
  );
6271
6511
  return null;
6272
6512
  }
6273
- return { name, def };
6274
6513
  }
6275
6514
  function buildExplicitProvider(provider, selModel, config) {
6276
6515
  if (provider.kind === "anthropic") {
@@ -6305,9 +6544,104 @@ function buildExplicitProvider(provider, selModel, config) {
6305
6544
  });
6306
6545
  }
6307
6546
 
6547
+ // src/routing/decision-bus.ts
6548
+ var RoutingDecisionBus = class {
6549
+ ringBuffer = [];
6550
+ listeners = /* @__PURE__ */ new Set();
6551
+ capacity;
6552
+ logger;
6553
+ constructor(opts) {
6554
+ this.capacity = opts?.capacity ?? 500;
6555
+ this.logger = opts?.logger;
6556
+ }
6557
+ emit(decision) {
6558
+ this.ringBuffer.push(decision);
6559
+ if (this.ringBuffer.length > this.capacity) {
6560
+ this.ringBuffer.shift();
6561
+ }
6562
+ if (this.logger) {
6563
+ this.logger.info("routing-decision", {
6564
+ useCase: decision.useCase,
6565
+ backendName: decision.backendName,
6566
+ resolutionPathLength: decision.resolutionPath.length,
6567
+ durationMs: decision.durationMs
6568
+ });
6569
+ }
6570
+ for (const listener of this.listeners) {
6571
+ try {
6572
+ listener(decision);
6573
+ } catch (err) {
6574
+ if (this.logger) {
6575
+ this.logger.warn("RoutingDecisionBus subscriber threw", {
6576
+ error: String(err)
6577
+ });
6578
+ }
6579
+ }
6580
+ }
6581
+ }
6582
+ recent(filter) {
6583
+ let out = this.ringBuffer.slice();
6584
+ if (filter?.skillName !== void 0) {
6585
+ out = out.filter(
6586
+ (d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
6587
+ );
6588
+ }
6589
+ if (filter?.mode !== void 0) {
6590
+ const m = filter.mode;
6591
+ out = out.filter(
6592
+ (d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
6593
+ );
6594
+ }
6595
+ if (filter?.backendName !== void 0) {
6596
+ out = out.filter((d) => d.backendName === filter.backendName);
6597
+ }
6598
+ if (filter?.limit !== void 0) {
6599
+ out = out.slice(-filter.limit).reverse();
6600
+ } else {
6601
+ out = out.reverse();
6602
+ }
6603
+ return out;
6604
+ }
6605
+ subscribe(listener) {
6606
+ this.listeners.add(listener);
6607
+ return () => {
6608
+ this.listeners.delete(listener);
6609
+ };
6610
+ }
6611
+ /**
6612
+ * Spec B Phase 5 (review-S2 fix): release all subscriber references so
6613
+ * teardown can complete without anchoring closures. Called from
6614
+ * `Orchestrator.stop()` before nulling the bus reference. The bus
6615
+ * remains usable after clear — `subscribe()` works as normal.
6616
+ */
6617
+ clearListeners() {
6618
+ this.listeners.clear();
6619
+ }
6620
+ };
6621
+
6622
+ // src/agent/triage-skill-mapping.ts
6623
+ function resolveSkillForTriage(triageSkill, catalog) {
6624
+ const expected = `harness-${triageSkill}`;
6625
+ const match = catalog.find((e) => e.name === expected);
6626
+ if (!match) return void 0;
6627
+ return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
6628
+ }
6629
+
6630
+ // src/agent/use-case-builder.ts
6631
+ function buildRoutingUseCase(issue, backendParam, catalog) {
6632
+ if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
6633
+ const decision = triageIssue(issue, {});
6634
+ const resolved = resolveSkillForTriage(decision.skill, catalog);
6635
+ if (resolved) {
6636
+ return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
6637
+ }
6638
+ const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
6639
+ return { kind: "tier", tier };
6640
+ }
6641
+
6308
6642
  // src/server/http.ts
6309
6643
  var http = __toESM(require("http"));
6310
- var path15 = __toESM(require("path"));
6644
+ var path17 = __toESM(require("path"));
6311
6645
  var import_core11 = require("@harness-engineering/core");
6312
6646
 
6313
6647
  // src/server/websocket.ts
@@ -6370,7 +6704,7 @@ var import_zod3 = require("zod");
6370
6704
  // src/server/utils.ts
6371
6705
  var DEFAULT_MAX_BYTES = 1048576;
6372
6706
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6373
- return new Promise((resolve7, reject) => {
6707
+ return new Promise((resolve8, reject) => {
6374
6708
  let body = "";
6375
6709
  let bytes = 0;
6376
6710
  req.on("data", (chunk) => {
@@ -6382,7 +6716,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6382
6716
  }
6383
6717
  body += String(chunk);
6384
6718
  });
6385
- req.on("end", () => resolve7(body));
6719
+ req.on("end", () => resolve8(body));
6386
6720
  req.on("error", reject);
6387
6721
  });
6388
6722
  }
@@ -6503,8 +6837,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6503
6837
 
6504
6838
  // src/server/routes/plans.ts
6505
6839
  var import_zod5 = require("zod");
6506
- var fs9 = __toESM(require("fs/promises"));
6507
- var path9 = __toESM(require("path"));
6840
+ var fs10 = __toESM(require("fs/promises"));
6841
+ var path11 = __toESM(require("path"));
6508
6842
  var PlanWriteSchema = import_zod5.z.object({
6509
6843
  filename: import_zod5.z.string().min(1),
6510
6844
  content: import_zod5.z.string().min(1)
@@ -6524,7 +6858,7 @@ function handlePlansRoute(req, res, plansDir) {
6524
6858
  return;
6525
6859
  }
6526
6860
  const parsed = result.data;
6527
- const basename3 = path9.basename(parsed.filename);
6861
+ const basename3 = path11.basename(parsed.filename);
6528
6862
  if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
6529
6863
  res.writeHead(400, { "Content-Type": "application/json" });
6530
6864
  res.end(
@@ -6532,9 +6866,9 @@ function handlePlansRoute(req, res, plansDir) {
6532
6866
  );
6533
6867
  return;
6534
6868
  }
6535
- await fs9.mkdir(plansDir, { recursive: true });
6536
- const filePath = path9.join(plansDir, basename3);
6537
- await fs9.writeFile(filePath, parsed.content, "utf-8");
6869
+ await fs10.mkdir(plansDir, { recursive: true });
6870
+ const filePath = path11.join(plansDir, basename3);
6871
+ await fs10.writeFile(filePath, parsed.content, "utf-8");
6538
6872
  res.writeHead(201, { "Content-Type": "application/json" });
6539
6873
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6540
6874
  } catch {
@@ -6909,8 +7243,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
6909
7243
  }
6910
7244
 
6911
7245
  // src/server/routes/roadmap-actions.ts
6912
- var fs10 = __toESM(require("fs/promises"));
6913
- var path10 = __toESM(require("path"));
7246
+ var fs11 = __toESM(require("fs/promises"));
7247
+ var path12 = __toESM(require("path"));
6914
7248
  var import_core7 = require("@harness-engineering/core");
6915
7249
  var import_zod8 = require("zod");
6916
7250
  var AppendRoadmapRequestSchema = import_zod8.z.object({
@@ -6938,7 +7272,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6938
7272
  sendJSON2(res, 503, { error: "Roadmap path not configured" });
6939
7273
  return;
6940
7274
  }
6941
- const projectRoot = path10.dirname(path10.dirname(roadmapPath));
7275
+ const projectRoot = path12.dirname(path12.dirname(roadmapPath));
6942
7276
  const mode = (0, import_core7.loadProjectRoadmapMode)(projectRoot);
6943
7277
  if (mode === "file-less") {
6944
7278
  const trackerCfg = (0, import_core7.loadTrackerClientConfigFromProject)(projectRoot);
@@ -6991,7 +7325,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6991
7325
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
6992
7326
  return;
6993
7327
  }
6994
- const content = await fs10.readFile(roadmapPath, "utf-8");
7328
+ const content = await fs11.readFile(roadmapPath, "utf-8");
6995
7329
  const roadmapResult = (0, import_core7.parseRoadmap)(content);
6996
7330
  if (!roadmapResult.ok) {
6997
7331
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
@@ -7022,8 +7356,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7022
7356
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7023
7357
  const tmpPath = roadmapPath + ".tmp";
7024
7358
  const serialized = (0, import_core7.serializeRoadmap)(roadmap);
7025
- await fs10.writeFile(tmpPath, serialized, "utf-8");
7026
- await fs10.rename(tmpPath, roadmapPath);
7359
+ await fs11.writeFile(tmpPath, serialized, "utf-8");
7360
+ await fs11.rename(tmpPath, roadmapPath);
7027
7361
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7028
7362
  } catch (err) {
7029
7363
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7516,7 +7850,7 @@ var import_core10 = require("@harness-engineering/core");
7516
7850
  var import_types24 = require("@harness-engineering/types");
7517
7851
 
7518
7852
  // src/proposals/gate.ts
7519
- var import_yaml2 = require("yaml");
7853
+ var import_yaml3 = require("yaml");
7520
7854
  var import_core8 = require("@harness-engineering/core");
7521
7855
  var GateRunError = class extends Error {
7522
7856
  constructor(message) {
@@ -7529,7 +7863,7 @@ function checkSkillYaml(yaml) {
7529
7863
  const findings = [];
7530
7864
  let doc;
7531
7865
  try {
7532
- doc = (0, import_yaml2.parse)(yaml);
7866
+ doc = (0, import_yaml3.parse)(yaml);
7533
7867
  } catch (err) {
7534
7868
  findings.push({
7535
7869
  severity: "error",
@@ -7652,9 +7986,9 @@ async function runGate(projectPath, proposalId) {
7652
7986
  }
7653
7987
 
7654
7988
  // src/proposals/promote.ts
7655
- var fs11 = __toESM(require("fs"));
7656
- var path11 = __toESM(require("path"));
7657
- var import_yaml3 = require("yaml");
7989
+ var fs12 = __toESM(require("fs"));
7990
+ var path13 = __toESM(require("path"));
7991
+ var import_yaml4 = require("yaml");
7658
7992
  var import_core9 = require("@harness-engineering/core");
7659
7993
  var GateNotReadyError = class extends Error {
7660
7994
  constructor(message) {
@@ -7670,11 +8004,11 @@ var PromotionError = class extends Error {
7670
8004
  };
7671
8005
  var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7672
8006
  function skillDir(projectPath, name) {
7673
- return path11.join(projectPath, "agents", "skills", "claude-code", name);
8007
+ return path13.join(projectPath, "agents", "skills", "claude-code", name);
7674
8008
  }
7675
8009
  function readIfExists(p) {
7676
8010
  try {
7677
- return fs11.readFileSync(p, "utf-8");
8011
+ return fs12.readFileSync(p, "utf-8");
7678
8012
  } catch {
7679
8013
  return null;
7680
8014
  }
@@ -7682,7 +8016,7 @@ function readIfExists(p) {
7682
8016
  function injectProvenanceIntoYaml(yamlText, proposalId) {
7683
8017
  let doc;
7684
8018
  try {
7685
- doc = (0, import_yaml3.parse)(yamlText);
8019
+ doc = (0, import_yaml4.parse)(yamlText);
7686
8020
  } catch (err) {
7687
8021
  throw new PromotionError(
7688
8022
  `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
@@ -7694,7 +8028,7 @@ function injectProvenanceIntoYaml(yamlText, proposalId) {
7694
8028
  const obj = doc;
7695
8029
  obj["provenance"] = "agent-proposed";
7696
8030
  obj["originatingProposalId"] = proposalId;
7697
- return (0, import_yaml3.stringify)(obj);
8031
+ return (0, import_yaml4.stringify)(obj);
7698
8032
  }
7699
8033
  function assertGateReady(proposal) {
7700
8034
  if (proposal.status !== "gate-running") {
@@ -7720,15 +8054,15 @@ function assertGateReady(proposal) {
7720
8054
  }
7721
8055
  async function promoteNewSkill(projectPath, proposal) {
7722
8056
  const target = skillDir(projectPath, proposal.content.name);
7723
- if (fs11.existsSync(target)) {
8057
+ if (fs12.existsSync(target)) {
7724
8058
  throw new PromotionError(
7725
8059
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7726
8060
  );
7727
8061
  }
7728
- fs11.mkdirSync(target, { recursive: true });
8062
+ fs12.mkdirSync(target, { recursive: true });
7729
8063
  const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
7730
- fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
7731
- fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
8064
+ fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8065
+ fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7732
8066
  return { skillPath: target };
7733
8067
  }
7734
8068
  async function promoteRefinement(projectPath, proposal) {
@@ -7736,12 +8070,12 @@ async function promoteRefinement(projectPath, proposal) {
7736
8070
  throw new PromotionError("refinement proposal is missing targetSkill");
7737
8071
  }
7738
8072
  const target = skillDir(projectPath, proposal.targetSkill);
7739
- if (!fs11.existsSync(target)) {
8073
+ if (!fs12.existsSync(target)) {
7740
8074
  throw new PromotionError(
7741
8075
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7742
8076
  );
7743
8077
  }
7744
- const yamlPath = path11.join(target, "skill.yaml");
8078
+ const yamlPath = path13.join(target, "skill.yaml");
7745
8079
  const before = readIfExists(yamlPath) ?? "";
7746
8080
  const after = injectProvenanceIntoYaml(before, proposal.id);
7747
8081
  if (after === before) {
@@ -7749,7 +8083,7 @@ async function promoteRefinement(projectPath, proposal) {
7749
8083
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7750
8084
  );
7751
8085
  }
7752
- fs11.writeFileSync(yamlPath, after);
8086
+ fs12.writeFileSync(yamlPath, after);
7753
8087
  return { skillPath: target };
7754
8088
  }
7755
8089
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8036,35 +8370,185 @@ function handleV1ProposalsRoute(req, res, deps) {
8036
8370
  return false;
8037
8371
  }
8038
8372
 
8039
- // src/server/routes/sessions.ts
8040
- var fs12 = __toESM(require("fs/promises"));
8041
- var path12 = __toESM(require("path"));
8373
+ // src/server/routes/v1/routing.ts
8042
8374
  var import_zod14 = require("zod");
8043
- var SessionCreateSchema = import_zod14.z.object({
8044
- sessionId: import_zod14.z.string().min(1)
8375
+ var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
8376
+ var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
8377
+ var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
8378
+ function sendJSON9(res, status, body) {
8379
+ res.writeHead(status, { "Content-Type": "application/json" });
8380
+ res.end(JSON.stringify(body));
8381
+ }
8382
+ function unavailable(res) {
8383
+ sendJSON9(res, 503, { error: "BackendRouter not available" });
8384
+ return true;
8385
+ }
8386
+ function resolveChain(value, backends) {
8387
+ return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
8388
+ }
8389
+ function buildResolvedChains(routing, backends) {
8390
+ const out = {};
8391
+ out["default"] = resolveChain(routing.default, backends);
8392
+ for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
8393
+ const v = routing[tier];
8394
+ if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
8395
+ }
8396
+ if (routing.intelligence) {
8397
+ for (const [layer, v] of Object.entries(routing.intelligence)) {
8398
+ if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
8399
+ }
8400
+ }
8401
+ if (routing.isolation) {
8402
+ for (const [tier, v] of Object.entries(routing.isolation)) {
8403
+ if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
8404
+ }
8405
+ }
8406
+ if (routing.skills) {
8407
+ for (const [name, v] of Object.entries(routing.skills)) {
8408
+ if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
8409
+ }
8410
+ }
8411
+ if (routing.modes) {
8412
+ for (const [mode, v] of Object.entries(routing.modes)) {
8413
+ if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
8414
+ }
8415
+ }
8416
+ return out;
8417
+ }
8418
+ function handleConfig(res, deps) {
8419
+ if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
8420
+ sendJSON9(res, 200, {
8421
+ routing: deps.routing,
8422
+ resolvedChains: buildResolvedChains(deps.routing, deps.backends),
8423
+ backends: Object.keys(deps.backends)
8424
+ });
8425
+ return true;
8426
+ }
8427
+ function parseDecisionsQuery(url) {
8428
+ const qIdx = url.indexOf("?");
8429
+ if (qIdx === -1) return {};
8430
+ const p = new URLSearchParams(url.slice(qIdx + 1));
8431
+ const filter = {};
8432
+ const skill = p.get("skill");
8433
+ const mode = p.get("mode");
8434
+ const backend = p.get("backend");
8435
+ const limit = p.get("limit");
8436
+ if (skill) filter.skillName = skill;
8437
+ if (mode) filter.mode = mode;
8438
+ if (backend) filter.backendName = backend;
8439
+ if (limit) {
8440
+ const n = Number(limit);
8441
+ if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
8442
+ }
8443
+ return filter;
8444
+ }
8445
+ function handleDecisions(req, res, deps) {
8446
+ if (!deps.bus) return unavailable(res);
8447
+ const filter = parseDecisionsQuery(req.url ?? "");
8448
+ sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
8449
+ return true;
8450
+ }
8451
+ var UseCaseSchema = import_zod14.z.discriminatedUnion("kind", [
8452
+ import_zod14.z.object({
8453
+ kind: import_zod14.z.literal("tier"),
8454
+ tier: import_zod14.z.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
8455
+ }),
8456
+ import_zod14.z.object({ kind: import_zod14.z.literal("intelligence"), layer: import_zod14.z.enum(["sel", "pesl"]) }),
8457
+ import_zod14.z.object({ kind: import_zod14.z.literal("isolation"), tier: import_zod14.z.string() }),
8458
+ import_zod14.z.object({ kind: import_zod14.z.literal("maintenance") }),
8459
+ import_zod14.z.object({ kind: import_zod14.z.literal("chat") }),
8460
+ import_zod14.z.object({
8461
+ kind: import_zod14.z.literal("skill"),
8462
+ skillName: import_zod14.z.string().min(1),
8463
+ cognitiveMode: import_zod14.z.string().optional()
8464
+ }),
8465
+ import_zod14.z.object({ kind: import_zod14.z.literal("mode"), cognitiveMode: import_zod14.z.string().min(1) })
8466
+ ]);
8467
+ var TraceBodySchema = import_zod14.z.object({
8468
+ useCase: UseCaseSchema,
8469
+ invocationOverride: import_zod14.z.string().min(1).optional()
8470
+ });
8471
+ async function handleTrace(req, res, deps) {
8472
+ if (!deps.routing || !deps.backends) {
8473
+ unavailable(res);
8474
+ return true;
8475
+ }
8476
+ let raw;
8477
+ try {
8478
+ raw = await readBody(req);
8479
+ } catch {
8480
+ sendJSON9(res, 400, { error: "body read failed" });
8481
+ return true;
8482
+ }
8483
+ let parsed;
8484
+ try {
8485
+ parsed = JSON.parse(raw);
8486
+ } catch {
8487
+ sendJSON9(res, 400, { error: "invalid JSON body" });
8488
+ return true;
8489
+ }
8490
+ const r = TraceBodySchema.safeParse(parsed);
8491
+ if (!r.success) {
8492
+ sendJSON9(res, 400, { error: r.error.message });
8493
+ return true;
8494
+ }
8495
+ const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
8496
+ try {
8497
+ const dryRunRouter = new BackendRouter({
8498
+ backends: deps.backends,
8499
+ routing: deps.routing
8500
+ });
8501
+ const { decision, def } = dryRunRouter.resolveDecisionAndDef(
8502
+ r.data.useCase,
8503
+ opts
8504
+ );
8505
+ sendJSON9(res, 200, { decision, def: { type: def.type } });
8506
+ } catch (err) {
8507
+ sendJSON9(res, 500, { error: String(err) });
8508
+ }
8509
+ return true;
8510
+ }
8511
+ function handleV1RoutingRoute(req, res, deps) {
8512
+ const url = req.url ?? "";
8513
+ const method = req.method ?? "GET";
8514
+ if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
8515
+ if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
8516
+ if (method === "POST" && TRACE_RE.test(url)) {
8517
+ void handleTrace(req, res, deps);
8518
+ return true;
8519
+ }
8520
+ return false;
8521
+ }
8522
+
8523
+ // src/server/routes/sessions.ts
8524
+ var fs13 = __toESM(require("fs/promises"));
8525
+ var path14 = __toESM(require("path"));
8526
+ var import_zod15 = require("zod");
8527
+ var SessionCreateSchema = import_zod15.z.object({
8528
+ sessionId: import_zod15.z.string().min(1)
8045
8529
  }).passthrough();
8046
8530
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8047
8531
  function isSafeId(id) {
8048
- return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
8532
+ return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
8049
8533
  }
8050
8534
  function jsonResponse(res, status, data) {
8051
8535
  res.writeHead(status, { "Content-Type": "application/json" });
8052
8536
  res.end(JSON.stringify(data));
8053
8537
  }
8054
8538
  function extractSessionId(url) {
8055
- const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
8539
+ const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
8056
8540
  const id = segments.pop();
8057
8541
  return id && id !== "sessions" ? id : null;
8058
8542
  }
8059
8543
  async function handleList2(res, sessionsDir) {
8060
8544
  try {
8061
- const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
8545
+ const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8062
8546
  const sessions = [];
8063
8547
  for (const entry of entries) {
8064
8548
  if (!entry.isDirectory()) continue;
8065
8549
  try {
8066
- const content = await fs12.readFile(
8067
- path12.join(sessionsDir, entry.name, "session.json"),
8550
+ const content = await fs13.readFile(
8551
+ path14.join(sessionsDir, entry.name, "session.json"),
8068
8552
  "utf-8"
8069
8553
  );
8070
8554
  sessions.push(JSON.parse(content));
@@ -8089,7 +8573,7 @@ async function handleGet2(res, id, sessionsDir) {
8089
8573
  return;
8090
8574
  }
8091
8575
  try {
8092
- const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
8576
+ const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8093
8577
  jsonResponse(res, 200, JSON.parse(content));
8094
8578
  } catch (err) {
8095
8579
  if (err.code === "ENOENT") {
@@ -8112,9 +8596,9 @@ async function handleCreate(req, res, sessionsDir) {
8112
8596
  jsonResponse(res, 400, { error: "Invalid sessionId" });
8113
8597
  return;
8114
8598
  }
8115
- const sessionDir = path12.join(sessionsDir, session.sessionId);
8116
- await fs12.mkdir(sessionDir, { recursive: true });
8117
- await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8599
+ const sessionDir = path14.join(sessionsDir, session.sessionId);
8600
+ await fs13.mkdir(sessionDir, { recursive: true });
8601
+ await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8118
8602
  jsonResponse(res, 200, { ok: true });
8119
8603
  } catch {
8120
8604
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8128,10 +8612,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
8128
8612
  return;
8129
8613
  }
8130
8614
  const body = await readBody(req);
8131
- const updates = import_zod14.z.record(import_zod14.z.unknown()).parse(JSON.parse(body));
8132
- const sessionFilePath = path12.join(sessionsDir, id, "session.json");
8133
- const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
8134
- await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8615
+ const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
8616
+ const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8617
+ const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8618
+ await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8135
8619
  jsonResponse(res, 200, { ok: true });
8136
8620
  } catch {
8137
8621
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8144,7 +8628,7 @@ async function handleDelete(res, url, sessionsDir) {
8144
8628
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8145
8629
  return;
8146
8630
  }
8147
- await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
8631
+ await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8148
8632
  jsonResponse(res, 200, { ok: true });
8149
8633
  } catch {
8150
8634
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8248,16 +8732,16 @@ function handleStreamsRoute(req, res, recorder) {
8248
8732
  }
8249
8733
 
8250
8734
  // src/server/routes/auth.ts
8251
- var import_zod15 = require("zod");
8735
+ var import_zod16 = require("zod");
8252
8736
  var import_types25 = require("@harness-engineering/types");
8253
- var CreateBodySchema = import_zod15.z.object({
8254
- name: import_zod15.z.string().min(1).max(100),
8255
- scopes: import_zod15.z.array(import_types25.TokenScopeSchema).min(1),
8737
+ var CreateBodySchema = import_zod16.z.object({
8738
+ name: import_zod16.z.string().min(1).max(100),
8739
+ scopes: import_zod16.z.array(import_types25.TokenScopeSchema).min(1),
8256
8740
  bridgeKind: import_types25.BridgeKindSchema.optional(),
8257
- tenantId: import_zod15.z.string().optional(),
8258
- expiresAt: import_zod15.z.string().datetime().optional()
8741
+ tenantId: import_zod16.z.string().optional(),
8742
+ expiresAt: import_zod16.z.string().datetime().optional()
8259
8743
  });
8260
- function sendJSON9(res, status, body) {
8744
+ function sendJSON10(res, status, body) {
8261
8745
  res.writeHead(status, { "Content-Type": "application/json" });
8262
8746
  res.end(JSON.stringify(body));
8263
8747
  }
@@ -8267,19 +8751,19 @@ async function handlePost(req, res, store) {
8267
8751
  raw = await readBody(req);
8268
8752
  } catch (err) {
8269
8753
  const msg = err instanceof Error ? err.message : "Failed to read body";
8270
- sendJSON9(res, 413, { error: msg });
8754
+ sendJSON10(res, 413, { error: msg });
8271
8755
  return;
8272
8756
  }
8273
8757
  let json;
8274
8758
  try {
8275
8759
  json = JSON.parse(raw);
8276
8760
  } catch {
8277
- sendJSON9(res, 400, { error: "Invalid JSON body" });
8761
+ sendJSON10(res, 400, { error: "Invalid JSON body" });
8278
8762
  return;
8279
8763
  }
8280
8764
  const parsed = CreateBodySchema.safeParse(json);
8281
8765
  if (!parsed.success) {
8282
- sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8766
+ sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8283
8767
  return;
8284
8768
  }
8285
8769
  try {
@@ -8292,37 +8776,37 @@ async function handlePost(req, res, store) {
8292
8776
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
8293
8777
  const result = await store.create(input);
8294
8778
  const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
8295
- sendJSON9(res, 200, {
8779
+ sendJSON10(res, 200, {
8296
8780
  ...publicRecord,
8297
8781
  token: result.token
8298
8782
  });
8299
8783
  } catch (err) {
8300
8784
  const msg = err instanceof Error ? err.message : "Failed to create token";
8301
8785
  if (msg.includes("already exists")) {
8302
- sendJSON9(res, 409, { error: msg });
8786
+ sendJSON10(res, 409, { error: msg });
8303
8787
  return;
8304
8788
  }
8305
- sendJSON9(res, 500, { error: "Internal error creating token" });
8789
+ sendJSON10(res, 500, { error: "Internal error creating token" });
8306
8790
  }
8307
8791
  }
8308
8792
  async function handleList3(res, store) {
8309
8793
  try {
8310
8794
  const list = await store.list();
8311
- sendJSON9(res, 200, list);
8795
+ sendJSON10(res, 200, list);
8312
8796
  } catch {
8313
- sendJSON9(res, 500, { error: "Internal error listing tokens" });
8797
+ sendJSON10(res, 500, { error: "Internal error listing tokens" });
8314
8798
  }
8315
8799
  }
8316
8800
  async function handleDelete2(res, store, id) {
8317
8801
  try {
8318
8802
  const ok = await store.revoke(id);
8319
8803
  if (!ok) {
8320
- sendJSON9(res, 404, { error: "Token not found" });
8804
+ sendJSON10(res, 404, { error: "Token not found" });
8321
8805
  return;
8322
8806
  }
8323
- sendJSON9(res, 200, { deleted: true });
8807
+ sendJSON10(res, 200, { deleted: true });
8324
8808
  } catch {
8325
- sendJSON9(res, 500, { error: "Internal error revoking token" });
8809
+ sendJSON10(res, 500, { error: "Internal error revoking token" });
8326
8810
  }
8327
8811
  }
8328
8812
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -8347,12 +8831,12 @@ function handleAuthRoute(req, res, store) {
8347
8831
  return true;
8348
8832
  }
8349
8833
  }
8350
- sendJSON9(res, 405, { error: "Method not allowed" });
8834
+ sendJSON10(res, 405, { error: "Method not allowed" });
8351
8835
  return true;
8352
8836
  }
8353
8837
 
8354
8838
  // src/server/routes/local-model.ts
8355
- function sendJSON10(res, status, body) {
8839
+ function sendJSON11(res, status, body) {
8356
8840
  res.writeHead(status, { "Content-Type": "application/json" });
8357
8841
  res.end(JSON.stringify(body));
8358
8842
  }
@@ -8360,36 +8844,36 @@ function handleLocalModelRoute(req, res, getStatus) {
8360
8844
  const { method, url } = req;
8361
8845
  if (url !== "/api/v1/local-model/status") return false;
8362
8846
  if (method !== "GET") {
8363
- sendJSON10(res, 405, { error: "Method not allowed" });
8847
+ sendJSON11(res, 405, { error: "Method not allowed" });
8364
8848
  return true;
8365
8849
  }
8366
8850
  if (!getStatus) {
8367
- sendJSON10(res, 503, { error: "Local backend not configured" });
8851
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8368
8852
  return true;
8369
8853
  }
8370
8854
  const status = getStatus();
8371
8855
  if (!status) {
8372
- sendJSON10(res, 503, { error: "Local backend not configured" });
8856
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8373
8857
  return true;
8374
8858
  }
8375
- sendJSON10(res, 200, status);
8859
+ sendJSON11(res, 200, status);
8376
8860
  return true;
8377
8861
  }
8378
8862
  function handleLocalModelsRoute(req, res, getStatuses) {
8379
8863
  const { method, url } = req;
8380
8864
  if (url !== "/api/v1/local-models/status") return false;
8381
8865
  if (method !== "GET") {
8382
- sendJSON10(res, 405, { error: "Method not allowed" });
8866
+ sendJSON11(res, 405, { error: "Method not allowed" });
8383
8867
  return true;
8384
8868
  }
8385
8869
  const statuses = getStatuses ? getStatuses() : [];
8386
- sendJSON10(res, 200, statuses);
8870
+ sendJSON11(res, 200, statuses);
8387
8871
  return true;
8388
8872
  }
8389
8873
 
8390
8874
  // src/server/static.ts
8391
- var fs13 = __toESM(require("fs"));
8392
- var path13 = __toESM(require("path"));
8875
+ var fs14 = __toESM(require("fs"));
8876
+ var path15 = __toESM(require("path"));
8393
8877
  var MIME_TYPES = {
8394
8878
  ".html": "text/html; charset=utf-8",
8395
8879
  ".js": "application/javascript; charset=utf-8",
@@ -8409,29 +8893,29 @@ var MIME_TYPES = {
8409
8893
  function handleStaticFile(req, res, dashboardDir) {
8410
8894
  const { method, url } = req;
8411
8895
  if (method !== "GET") return false;
8412
- const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
8413
- const wsPath = path13.posix.join(path13.posix.sep, "ws");
8896
+ const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
8897
+ const wsPath = path15.posix.join(path15.posix.sep, "ws");
8414
8898
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
8415
8899
  const urlPath = new URL(url ?? "/", "http://localhost").pathname;
8416
- const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8417
- const resolved = path13.resolve(requestedPath);
8418
- if (!resolved.startsWith(path13.resolve(dashboardDir))) {
8419
- return serveFile(path13.join(dashboardDir, "index.html"), res);
8900
+ const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8901
+ const resolved = path15.resolve(requestedPath);
8902
+ if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8903
+ return serveFile(path15.join(dashboardDir, "index.html"), res);
8420
8904
  }
8421
- if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
8905
+ if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
8422
8906
  return serveFile(resolved, res);
8423
8907
  }
8424
- const indexPath = path13.join(dashboardDir, "index.html");
8425
- if (fs13.existsSync(indexPath)) {
8908
+ const indexPath = path15.join(dashboardDir, "index.html");
8909
+ if (fs14.existsSync(indexPath)) {
8426
8910
  return serveFile(indexPath, res);
8427
8911
  }
8428
8912
  return false;
8429
8913
  }
8430
8914
  function serveFile(filePath, res) {
8431
- const ext = path13.extname(filePath).toLowerCase();
8915
+ const ext = path15.extname(filePath).toLowerCase();
8432
8916
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8433
8917
  try {
8434
- const content = fs13.readFileSync(filePath);
8918
+ const content = fs14.readFileSync(filePath);
8435
8919
  res.writeHead(200, { "Content-Type": contentType });
8436
8920
  res.end(content);
8437
8921
  return true;
@@ -8441,8 +8925,8 @@ function serveFile(filePath, res) {
8441
8925
  }
8442
8926
 
8443
8927
  // src/server/plan-watcher.ts
8444
- var fs14 = __toESM(require("fs"));
8445
- var path14 = __toESM(require("path"));
8928
+ var fs15 = __toESM(require("fs"));
8929
+ var path16 = __toESM(require("path"));
8446
8930
  var PlanWatcher = class {
8447
8931
  plansDir;
8448
8932
  queue;
@@ -8456,11 +8940,11 @@ var PlanWatcher = class {
8456
8940
  * Creates the directory if it does not exist.
8457
8941
  */
8458
8942
  start() {
8459
- fs14.mkdirSync(this.plansDir, { recursive: true });
8460
- this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
8943
+ fs15.mkdirSync(this.plansDir, { recursive: true });
8944
+ this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
8461
8945
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
8462
- const filePath = path14.join(this.plansDir, filename);
8463
- if (fs14.existsSync(filePath)) {
8946
+ const filePath = path16.join(this.plansDir, filename);
8947
+ if (fs15.existsSync(filePath)) {
8464
8948
  void this.handleNewPlan(filename);
8465
8949
  }
8466
8950
  }
@@ -8510,8 +8994,8 @@ function parseToken(raw) {
8510
8994
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
8511
8995
  }
8512
8996
  var TokenStore = class {
8513
- constructor(path22) {
8514
- this.path = path22;
8997
+ constructor(path24) {
8998
+ this.path = path24;
8515
8999
  }
8516
9000
  path;
8517
9001
  cache = null;
@@ -8618,8 +9102,8 @@ var import_promises2 = require("fs/promises");
8618
9102
  var import_node_path2 = require("path");
8619
9103
  var import_types27 = require("@harness-engineering/types");
8620
9104
  var AuditLogger = class {
8621
- constructor(path22, opts = {}) {
8622
- this.path = path22;
9105
+ constructor(path24, opts = {}) {
9106
+ this.path = path24;
8623
9107
  this.opts = opts;
8624
9108
  }
8625
9109
  path;
@@ -8746,14 +9230,36 @@ var V1_BRIDGE_ROUTES = [
8746
9230
  pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
8747
9231
  scope: "read-telemetry",
8748
9232
  description: "Prompt-cache hit/miss snapshot (rolling window)."
9233
+ },
9234
+ // ── Spec B Phase 5 routing observability ──
9235
+ // D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
9236
+ // precedent (read-only observability). A dedicated `read-routing`
9237
+ // scope was rejected to avoid a TokenScopeSchema + ADR cascade.
9238
+ {
9239
+ method: "GET",
9240
+ pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
9241
+ scope: "read-telemetry",
9242
+ description: "Current routing config + resolved fallback chains + known backends."
9243
+ },
9244
+ {
9245
+ method: "GET",
9246
+ pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
9247
+ scope: "read-telemetry",
9248
+ description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
9249
+ },
9250
+ {
9251
+ method: "POST",
9252
+ pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
9253
+ scope: "read-telemetry",
9254
+ description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
8749
9255
  }
8750
9256
  ];
8751
9257
  function isV1Bridge(method, url) {
8752
9258
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
8753
9259
  }
8754
- function requiredBridgeScope(method, path22) {
9260
+ function requiredBridgeScope(method, path24) {
8755
9261
  for (const r of V1_BRIDGE_ROUTES) {
8756
- if (r.method === method && r.pattern.test(path22)) return r.scope;
9262
+ if (r.method === method && r.pattern.test(path24)) return r.scope;
8757
9263
  }
8758
9264
  return null;
8759
9265
  }
@@ -8763,24 +9269,24 @@ function hasScope(held, required) {
8763
9269
  if (held.includes("admin")) return true;
8764
9270
  return held.includes(required);
8765
9271
  }
8766
- function requiredScopeForRoute(method, path22) {
8767
- const bridgeScope = requiredBridgeScope(method, path22);
9272
+ function requiredScopeForRoute(method, path24) {
9273
+ const bridgeScope = requiredBridgeScope(method, path24);
8768
9274
  if (bridgeScope) return bridgeScope;
8769
- if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
8770
- if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
8771
- if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
8772
- if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
8773
- if (path22.startsWith("/api/interactions")) return "resolve-interaction";
8774
- if (path22.startsWith("/api/plans")) return "read-status";
8775
- if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
8776
- if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
8777
- if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
8778
- if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
9275
+ if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9276
+ if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9277
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9278
+ if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9279
+ if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9280
+ if (path24.startsWith("/api/plans")) return "read-status";
9281
+ if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9282
+ if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9283
+ if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9284
+ if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
8779
9285
  return "read-status";
8780
- if (path22.startsWith("/api/maintenance")) return "trigger-job";
8781
- if (path22.startsWith("/api/streams")) return "read-status";
8782
- if (path22.startsWith("/api/sessions")) return "read-status";
8783
- if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
9286
+ if (path24.startsWith("/api/maintenance")) return "trigger-job";
9287
+ if (path24.startsWith("/api/streams")) return "read-status";
9288
+ if (path24.startsWith("/api/sessions")) return "read-status";
9289
+ if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
8784
9290
  return null;
8785
9291
  }
8786
9292
 
@@ -8844,6 +9350,15 @@ var OrchestratorServer = class {
8844
9350
  getLocalModelStatuses = null;
8845
9351
  webhooks;
8846
9352
  cacheMetrics;
9353
+ // Spec B Phase 5 — routing observability accessor closures + the WS
9354
+ // broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
9355
+ // calls it explicitly; clearListeners in Orchestrator.stop() is the
9356
+ // belt-and-suspenders second line).
9357
+ getBackendRouterFn = null;
9358
+ getRoutingDecisionBusFn = null;
9359
+ getRoutingConfigFn = null;
9360
+ getBackendsFn = null;
9361
+ routingDecisionUnsubscribe = null;
8847
9362
  recorder = null;
8848
9363
  planWatcher = null;
8849
9364
  tokenStore;
@@ -8856,8 +9371,8 @@ var OrchestratorServer = class {
8856
9371
  this.orchestrator = orchestrator;
8857
9372
  this.port = port;
8858
9373
  this.initDependencies(deps);
8859
- const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
8860
- const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
9374
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
9375
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
8861
9376
  this.tokenStore = new TokenStore(tokensPath);
8862
9377
  this.auditLogger = new AuditLogger(auditPath);
8863
9378
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -8870,20 +9385,24 @@ var OrchestratorServer = class {
8870
9385
  }
8871
9386
  initDependencies(deps) {
8872
9387
  this.interactionQueue = deps?.interactionQueue;
8873
- this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8874
- this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
9388
+ this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
9389
+ this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
8875
9390
  this.claudeCommand = deps?.claudeCommand ?? "claude";
8876
9391
  this.pipeline = deps?.pipeline ?? null;
8877
9392
  this.analysisArchive = deps?.analysisArchive;
8878
9393
  this.roadmapPath = deps?.roadmapPath ?? null;
8879
9394
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
8880
- this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
9395
+ this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
8881
9396
  this.projectPath = deps?.projectPath ?? process.cwd();
8882
9397
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
8883
9398
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
8884
9399
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
8885
9400
  this.webhooks = deps?.webhooks;
8886
9401
  this.cacheMetrics = deps?.cacheMetrics;
9402
+ this.getBackendRouterFn = deps?.getBackendRouter ?? null;
9403
+ this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
9404
+ this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
9405
+ this.getBackendsFn = deps?.getBackends ?? null;
8887
9406
  }
8888
9407
  wireEvents() {
8889
9408
  this.stateChangeListener = (snapshot) => {
@@ -8894,6 +9413,12 @@ var OrchestratorServer = class {
8894
9413
  };
8895
9414
  this.orchestrator.on("state_change", this.stateChangeListener);
8896
9415
  this.orchestrator.on("agent_event", this.agentEventListener);
9416
+ const bus = this.getRoutingDecisionBusFn?.() ?? null;
9417
+ if (bus) {
9418
+ this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
9419
+ this.broadcaster.broadcast("routing:decision", decision);
9420
+ });
9421
+ }
8897
9422
  }
8898
9423
  /**
8899
9424
  * Broadcast a new interaction to all WebSocket clients.
@@ -9049,6 +9574,14 @@ var OrchestratorServer = class {
9049
9574
  (req, res) => handleV1TelemetryRoute(req, res, {
9050
9575
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
9051
9576
  }),
9577
+ // Spec B Phase 5 — routing observability. Returns 503 when the
9578
+ // backendFactory is null (legacy single-backend configs).
9579
+ (req, res) => handleV1RoutingRoute(req, res, {
9580
+ router: this.getBackendRouterFn?.() ?? null,
9581
+ bus: this.getRoutingDecisionBusFn?.() ?? null,
9582
+ routing: this.getRoutingConfigFn?.() ?? null,
9583
+ backends: this.getBackendsFn?.() ?? null
9584
+ }),
9052
9585
  // Hermes Phase 4 — skill proposal review queue. Read scopes
9053
9586
  // (`read-status`) and write scopes (`manage-proposals`) are enforced
9054
9587
  // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
@@ -9145,17 +9678,21 @@ var OrchestratorServer = class {
9145
9678
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
9146
9679
  this.planWatcher.start();
9147
9680
  }
9148
- return new Promise((resolve7) => {
9681
+ return new Promise((resolve8) => {
9149
9682
  const host = getBindHost();
9150
9683
  this.httpServer.listen(this.port, host, () => {
9151
9684
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
9152
- resolve7();
9685
+ resolve8();
9153
9686
  });
9154
9687
  });
9155
9688
  }
9156
9689
  stop() {
9157
9690
  this.orchestrator.removeListener("state_change", this.stateChangeListener);
9158
9691
  this.orchestrator.removeListener("agent_event", this.agentEventListener);
9692
+ if (this.routingDecisionUnsubscribe) {
9693
+ this.routingDecisionUnsubscribe();
9694
+ this.routingDecisionUnsubscribe = null;
9695
+ }
9159
9696
  if (this.planWatcher) {
9160
9697
  this.planWatcher.stop();
9161
9698
  this.planWatcher = null;
@@ -9199,8 +9736,8 @@ function genSecret2() {
9199
9736
  return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
9200
9737
  }
9201
9738
  var WebhookStore = class {
9202
- constructor(path22) {
9203
- this.path = path22;
9739
+ constructor(path24) {
9740
+ this.path = path24;
9204
9741
  }
9205
9742
  path;
9206
9743
  cache = null;
@@ -10745,19 +11282,19 @@ var SingleProcessLeaderElector = class {
10745
11282
  };
10746
11283
 
10747
11284
  // src/maintenance/reporter.ts
10748
- var fs15 = __toESM(require("fs"));
10749
- var path16 = __toESM(require("path"));
10750
- var import_zod16 = require("zod");
10751
- var RunResultSchema = import_zod16.z.object({
10752
- taskId: import_zod16.z.string(),
10753
- startedAt: import_zod16.z.string(),
10754
- completedAt: import_zod16.z.string(),
10755
- status: import_zod16.z.enum(["success", "failure", "skipped", "no-issues"]),
10756
- findings: import_zod16.z.number(),
10757
- fixed: import_zod16.z.number(),
10758
- prUrl: import_zod16.z.string().nullable(),
10759
- prUpdated: import_zod16.z.boolean(),
10760
- error: import_zod16.z.string().optional()
11285
+ var fs16 = __toESM(require("fs"));
11286
+ var path18 = __toESM(require("path"));
11287
+ var import_zod17 = require("zod");
11288
+ var RunResultSchema = import_zod17.z.object({
11289
+ taskId: import_zod17.z.string(),
11290
+ startedAt: import_zod17.z.string(),
11291
+ completedAt: import_zod17.z.string(),
11292
+ status: import_zod17.z.enum(["success", "failure", "skipped", "no-issues"]),
11293
+ findings: import_zod17.z.number(),
11294
+ fixed: import_zod17.z.number(),
11295
+ prUrl: import_zod17.z.string().nullable(),
11296
+ prUpdated: import_zod17.z.boolean(),
11297
+ error: import_zod17.z.string().optional()
10761
11298
  });
10762
11299
  var MAX_HISTORY = 500;
10763
11300
  var fallbackLogger = {
@@ -10781,10 +11318,10 @@ var MaintenanceReporter = class {
10781
11318
  */
10782
11319
  async load() {
10783
11320
  try {
10784
- await fs15.promises.mkdir(this.persistDir, { recursive: true });
10785
- const filePath = path16.join(this.persistDir, "history.json");
10786
- const data = await fs15.promises.readFile(filePath, "utf-8");
10787
- const parsed = import_zod16.z.array(RunResultSchema).safeParse(JSON.parse(data));
11321
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11322
+ const filePath = path18.join(this.persistDir, "history.json");
11323
+ const data = await fs16.promises.readFile(filePath, "utf-8");
11324
+ const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
10788
11325
  if (parsed.success) {
10789
11326
  this.history = parsed.data.slice(0, MAX_HISTORY);
10790
11327
  }
@@ -10817,9 +11354,9 @@ var MaintenanceReporter = class {
10817
11354
  */
10818
11355
  async persist() {
10819
11356
  try {
10820
- await fs15.promises.mkdir(this.persistDir, { recursive: true });
10821
- const filePath = path16.join(this.persistDir, "history.json");
10822
- await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
11357
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11358
+ const filePath = path18.join(this.persistDir, "history.json");
11359
+ await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10823
11360
  } catch (err) {
10824
11361
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
10825
11362
  }
@@ -11306,7 +11843,7 @@ function parseStatusLine(output) {
11306
11843
  // src/maintenance/check-script-runner.ts
11307
11844
  var import_node_child_process11 = require("child_process");
11308
11845
  var import_node_util3 = require("util");
11309
- var path17 = __toESM(require("path"));
11846
+ var path19 = __toESM(require("path"));
11310
11847
  var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process11.execFile);
11311
11848
  var CheckScriptRunner = class {
11312
11849
  constructor(cwd) {
@@ -11325,7 +11862,7 @@ var CheckScriptRunner = class {
11325
11862
  }
11326
11863
  };
11327
11864
  async function captureScript(spec, projectRoot) {
11328
- const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11865
+ const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
11329
11866
  const args = spec.args ?? [];
11330
11867
  const timeoutMs = spec.timeoutMs ?? 12e4;
11331
11868
  try {
@@ -11415,8 +11952,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
11415
11952
  }
11416
11953
 
11417
11954
  // src/maintenance/output-store.ts
11418
- var fs16 = __toESM(require("fs"));
11419
- var path18 = __toESM(require("path"));
11955
+ var fs17 = __toESM(require("fs"));
11956
+ var path20 = __toESM(require("path"));
11420
11957
  var DEFAULT_RETENTION = {
11421
11958
  runs: 50,
11422
11959
  maxAgeDays: 30
@@ -11456,13 +11993,13 @@ var TaskOutputStore = class {
11456
11993
  async write(taskId, entry, retention) {
11457
11994
  this.ensureSafeTaskId(taskId);
11458
11995
  const dir = this.dirFor(taskId);
11459
- await fs16.promises.mkdir(dir, { recursive: true });
11996
+ await fs17.promises.mkdir(dir, { recursive: true });
11460
11997
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11461
- const filePath = path18.join(dir, fileName);
11998
+ const filePath = path20.join(dir, fileName);
11462
11999
  const tmpPath = `${filePath}.tmp`;
11463
12000
  const payload = JSON.stringify(entry, null, 2);
11464
- await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11465
- await fs16.promises.rename(tmpPath, filePath);
12001
+ await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12002
+ await fs17.promises.rename(tmpPath, filePath);
11466
12003
  try {
11467
12004
  await this.applyRetention(taskId, retention);
11468
12005
  } catch (err) {
@@ -11486,7 +12023,7 @@ var TaskOutputStore = class {
11486
12023
  const slice = fileNames.slice(offset, offset + limit);
11487
12024
  const out = [];
11488
12025
  for (const name of slice) {
11489
- const entry = await this.readEntry(path18.join(dir, name));
12026
+ const entry = await this.readEntry(path20.join(dir, name));
11490
12027
  if (entry) out.push(entry);
11491
12028
  }
11492
12029
  return out;
@@ -11502,18 +12039,18 @@ var TaskOutputStore = class {
11502
12039
  }
11503
12040
  const dir = this.dirFor(taskId);
11504
12041
  const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11505
- return this.readEntry(path18.join(dir, fileName));
12042
+ return this.readEntry(path20.join(dir, fileName));
11506
12043
  }
11507
12044
  /**
11508
12045
  * The on-disk root for a given task. Exposed for tooling that needs to walk
11509
12046
  * outputs from outside the store API.
11510
12047
  */
11511
12048
  dirFor(taskId) {
11512
- return path18.join(this.rootDir, taskId, "outputs");
12049
+ return path20.join(this.rootDir, taskId, "outputs");
11513
12050
  }
11514
12051
  async readEntry(filePath) {
11515
12052
  try {
11516
- const buf = await fs16.promises.readFile(filePath, "utf-8");
12053
+ const buf = await fs17.promises.readFile(filePath, "utf-8");
11517
12054
  const parsed = JSON.parse(buf);
11518
12055
  return parsed;
11519
12056
  } catch {
@@ -11535,7 +12072,7 @@ var TaskOutputStore = class {
11535
12072
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11536
12073
  for (const name of toRemove) {
11537
12074
  try {
11538
- await fs16.promises.unlink(path18.join(dir, name));
12075
+ await fs17.promises.unlink(path20.join(dir, name));
11539
12076
  } catch {
11540
12077
  }
11541
12078
  }
@@ -11544,7 +12081,7 @@ var TaskOutputStore = class {
11544
12081
  async function listJsonFilesDescending(dir) {
11545
12082
  let names;
11546
12083
  try {
11547
- names = await fs16.promises.readdir(dir);
12084
+ names = await fs17.promises.readdir(dir);
11548
12085
  } catch {
11549
12086
  return [];
11550
12087
  }
@@ -11718,8 +12255,8 @@ function validateCheckShape(prefix, task, errors) {
11718
12255
  });
11719
12256
  }
11720
12257
  if (hasScript) {
11721
- const path22 = task.checkScript?.path;
11722
- if (!path22 || path22.trim().length === 0) {
12258
+ const path24 = task.checkScript?.path;
12259
+ if (!path24 || path24.trim().length === 0) {
11723
12260
  errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
11724
12261
  }
11725
12262
  if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
@@ -11845,9 +12382,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
11845
12382
  stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11846
12383
  }
11847
12384
  }
11848
- function reportCycle(path22, next, errors, reported) {
11849
- const cycleStart = path22.indexOf(next);
11850
- const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
12385
+ function reportCycle(path24, next, errors, reported) {
12386
+ const cycleStart = path24.indexOf(next);
12387
+ const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
11851
12388
  const key = cyclePath.join("\u2192");
11852
12389
  if (reported.has(key)) return;
11853
12390
  reported.add(key);
@@ -11858,11 +12395,6 @@ function reportCycle(path22, next, errors, reported) {
11858
12395
  }
11859
12396
 
11860
12397
  // src/orchestrator.ts
11861
- function useCaseForBackendParam(issue, backendParam) {
11862
- if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
11863
- const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
11864
- return { kind: "tier", tier };
11865
- }
11866
12398
  var Orchestrator = class extends import_node_events.EventEmitter {
11867
12399
  state;
11868
12400
  config;
@@ -11887,6 +12419,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11887
12419
  * construction time. Eliminating this fallback is autopilot Phase 4+.
11888
12420
  */
11889
12421
  backendFactory;
12422
+ /**
12423
+ * Spec B Phase 4 (D8): per-orchestrator in-process bus for
12424
+ * `RoutingDecision` events. Constructed alongside backendFactory when
12425
+ * agent.backends synthesis succeeds; null when legacy single-backend
12426
+ * config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
12427
+ * subscribe via `getRoutingDecisionBus()`.
12428
+ */
12429
+ routingDecisionBus;
11890
12430
  /**
11891
12431
  * Test-only: when overrides.backend is provided, dispatch uses this
11892
12432
  * instance directly (bypassing the factory). Mirrors Phase 1
@@ -11909,6 +12449,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11909
12449
  * so this map is the single source of truth post-migration.
11910
12450
  */
11911
12451
  localResolvers = /* @__PURE__ */ new Map();
12452
+ /**
12453
+ * Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
12454
+ * construction from `projectRoot/agents/skills/`. Consulted by
12455
+ * `buildRoutingUseCase` at dispatch start to construct
12456
+ * `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
12457
+ * Empty when the orchestrator runs outside a harness project root
12458
+ * (then dispatch falls through to per-tier, preserving F11/N2).
12459
+ */
12460
+ skillCatalog;
11912
12461
  /**
11913
12462
  * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
11914
12463
  * (SC39): each local/pi resolver gets its own listener emitting a
@@ -11957,7 +12506,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11957
12506
  completionHandler;
11958
12507
  /** Project root directory, derived from workspace root. */
11959
12508
  get projectRoot() {
11960
- return path19.resolve(this.config.workspace.root, "..", "..");
12509
+ return path21.resolve(this.config.workspace.root, "..", "..");
11961
12510
  }
11962
12511
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
11963
12512
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -12001,6 +12550,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12001
12550
  `migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
12002
12551
  );
12003
12552
  }
12553
+ const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
12554
+ this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
12555
+ if (this.skillCatalog.length === 0) {
12556
+ this.logger.warn(
12557
+ `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")}.`
12558
+ );
12559
+ }
12004
12560
  this.tracker = overrides?.tracker || this.createTracker();
12005
12561
  this.workspace = new WorkspaceManager(config.workspace, {
12006
12562
  emitEvent: (event) => {
@@ -12012,10 +12568,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12012
12568
  this.renderer = new PromptRenderer();
12013
12569
  this.overrideBackend = overrides?.backend ?? null;
12014
12570
  this.interactionQueue = new InteractionQueue(
12015
- path19.join(config.workspace.root, "..", "interactions"),
12571
+ path21.join(config.workspace.root, "..", "interactions"),
12016
12572
  this
12017
12573
  );
12018
- this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
12574
+ this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
12019
12575
  const backendsMap = this.config.agent.backends ?? {};
12020
12576
  for (const [name, def] of Object.entries(backendsMap)) {
12021
12577
  if (def.type === "local" || def.type === "pi") {
@@ -12036,6 +12592,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12036
12592
  const routing = this.config.agent.routing ?? {
12037
12593
  default: firstBackendName ?? "primary"
12038
12594
  };
12595
+ this.routingDecisionBus = new RoutingDecisionBus({
12596
+ capacity: 500,
12597
+ logger: this.logger
12598
+ });
12039
12599
  this.backendFactory = new OrchestratorBackendFactory({
12040
12600
  backends: this.config.agent.backends,
12041
12601
  routing,
@@ -12043,6 +12603,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12043
12603
  ...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
12044
12604
  ...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
12045
12605
  cacheMetrics: this.cacheMetrics,
12606
+ decisionBus: this.routingDecisionBus,
12046
12607
  getResolverModelFor: (name) => {
12047
12608
  const resolver = this.localResolvers.get(name);
12048
12609
  return resolver ? () => resolver.resolveModel() : void 0;
@@ -12050,6 +12611,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12050
12611
  });
12051
12612
  } else {
12052
12613
  this.backendFactory = null;
12614
+ this.routingDecisionBus = null;
12053
12615
  }
12054
12616
  this.pipeline = null;
12055
12617
  this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
@@ -12059,7 +12621,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12059
12621
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
12060
12622
  });
12061
12623
  this.recorder = new StreamRecorder(
12062
- path19.resolve(config.workspace.root, "..", "streams"),
12624
+ path21.resolve(config.workspace.root, "..", "streams"),
12063
12625
  this.logger
12064
12626
  );
12065
12627
  const self = this;
@@ -12090,10 +12652,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12090
12652
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
12091
12653
  if (config.server?.port) {
12092
12654
  const webhookStore = new WebhookStore(
12093
- path19.join(this.projectRoot, ".harness", "webhooks.json")
12655
+ path21.join(this.projectRoot, ".harness", "webhooks.json")
12094
12656
  );
12095
12657
  this.webhookQueue = new WebhookQueue(
12096
- path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12658
+ path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12097
12659
  );
12098
12660
  const webhookDelivery = new WebhookDelivery({
12099
12661
  queue: this.webhookQueue,
@@ -12131,7 +12693,16 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12131
12693
  queue: this.webhookQueue
12132
12694
  },
12133
12695
  cacheMetrics: this.cacheMetrics,
12134
- plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
12696
+ // Spec B Phase 5: routing observability accessors. Closures so the
12697
+ // server re-reads on every request — stop() / start() do not
12698
+ // require server reconstruction. Returns null if no backendFactory
12699
+ // (legacy single-backend configs), and the route handler renders
12700
+ // 503 in that case.
12701
+ getBackendRouter: () => this.getBackendRouter(),
12702
+ getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
12703
+ getRoutingConfig: () => this.getRoutingConfig(),
12704
+ getBackends: () => this.getBackends(),
12705
+ plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
12135
12706
  pipeline: this.pipeline,
12136
12707
  analysisArchive: this.analysisArchive,
12137
12708
  roadmapPath: config.tracker.filePath ?? null,
@@ -12241,7 +12812,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12241
12812
  }
12242
12813
  };
12243
12814
  const outputStore = new TaskOutputStore({
12244
- rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12815
+ rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12245
12816
  logger: this.logger
12246
12817
  });
12247
12818
  const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
@@ -12282,7 +12853,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12282
12853
  ${messages}`);
12283
12854
  }
12284
12855
  this.maintenanceReporter = new MaintenanceReporter({
12285
- persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12856
+ persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12286
12857
  logger: this.logger
12287
12858
  });
12288
12859
  await this.maintenanceReporter.load();
@@ -12333,10 +12904,17 @@ ${messages}`);
12333
12904
  }
12334
12905
  }
12335
12906
  createIntelligencePipeline() {
12907
+ if (!this.backendFactory) {
12908
+ this.logger.warn(
12909
+ "intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
12910
+ );
12911
+ return null;
12912
+ }
12336
12913
  const bundle = buildIntelligencePipeline({
12337
12914
  config: this.config,
12338
12915
  localResolvers: this.localResolvers,
12339
- logger: this.logger
12916
+ logger: this.logger,
12917
+ router: this.backendFactory.getRouter()
12340
12918
  });
12341
12919
  if (!bundle) return null;
12342
12920
  this.graphStore = bundle.graphStore;
@@ -12387,11 +12965,13 @@ ${messages}`);
12387
12965
  simulationResults,
12388
12966
  personaRecommendations
12389
12967
  } = pipelineResult ?? {};
12968
+ const selfAssignee = await this.orchestratorIdPromise;
12390
12969
  const tickEvent = {
12391
12970
  type: "tick",
12392
12971
  candidates,
12393
12972
  runningStates: runningStatesResult.value,
12394
12973
  nowMs,
12974
+ selfAssignee,
12395
12975
  ...concernSignals !== void 0 && { concernSignals },
12396
12976
  ...enrichedSpecs !== void 0 && { enrichedSpecs },
12397
12977
  ...complexityScores !== void 0 && { complexityScores },
@@ -12815,14 +13395,24 @@ ${messages}`);
12815
13395
  issue,
12816
13396
  attempt: attempt || 1
12817
13397
  });
12818
- const useCase = useCaseForBackendParam(issue, backend);
13398
+ const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
13399
+ const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
13400
+ const routerOpts = invocationOverride ? { invocationOverride } : void 0;
13401
+ if (invocationOverride) {
13402
+ this.logger.info(
13403
+ `Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
13404
+ { issueId: issue.id }
13405
+ );
13406
+ }
12819
13407
  let routedBackendName;
12820
13408
  if (this.overrideBackend !== null) {
12821
13409
  routedBackendName = this.overrideBackend.name;
12822
13410
  } else if (this.backendFactory !== null) {
12823
- routedBackendName = this.backendFactory.resolveName(useCase);
13411
+ routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
12824
13412
  } else {
12825
- routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
13413
+ const routingDefault = this.config.agent.routing?.default;
13414
+ const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
13415
+ routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
12826
13416
  }
12827
13417
  const session = {
12828
13418
  sessionId: `pending-${Date.now()}`,
@@ -12861,7 +13451,7 @@ ${messages}`);
12861
13451
  if (this.overrideBackend !== null) {
12862
13452
  agentBackend = this.overrideBackend;
12863
13453
  } else if (this.backendFactory !== null) {
12864
- agentBackend = this.backendFactory.forUseCase(useCase);
13454
+ agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
12865
13455
  } else {
12866
13456
  throw new Error(
12867
13457
  `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.`
@@ -13151,6 +13741,8 @@ ${messages}`);
13151
13741
  unsub();
13152
13742
  }
13153
13743
  this.localModelStatusUnsubscribes = [];
13744
+ this.routingDecisionBus?.clearListeners();
13745
+ this.routingDecisionBus = null;
13154
13746
  for (const resolver of this.localResolvers.values()) {
13155
13747
  resolver.stop();
13156
13748
  }
@@ -13224,6 +13816,42 @@ ${messages}`);
13224
13816
  tickActivity: this.tickActivity
13225
13817
  };
13226
13818
  }
13819
+ /**
13820
+ * Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
13821
+ * Phase 7 (dashboard WS broadcast). Returns null when the legacy
13822
+ * single-backend config bypassed agent.backends synthesis.
13823
+ */
13824
+ getRoutingDecisionBus() {
13825
+ return this.routingDecisionBus;
13826
+ }
13827
+ /**
13828
+ * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
13829
+ * dispatch path uses the factory-owned router directly; observability
13830
+ * routes (config / decisions) reach it through this accessor. Returns
13831
+ * null when the legacy single-backend config bypassed agent.backends
13832
+ * synthesis (no backendFactory built).
13833
+ */
13834
+ getBackendRouter() {
13835
+ return this.backendFactory?.getRouter() ?? null;
13836
+ }
13837
+ /**
13838
+ * Spec B Phase 5: snapshot of the active RoutingConfig for the config
13839
+ * route and the trace route's bus-less router construction. Returns
13840
+ * null when the operator's harness.config.json carries no
13841
+ * `agent.routing` block.
13842
+ */
13843
+ getRoutingConfig() {
13844
+ return this.config.agent.routing ?? null;
13845
+ }
13846
+ /**
13847
+ * Spec B Phase 5: snapshot of `agent.backends` for the config route
13848
+ * (existence annotations) and the trace route (bus-less router
13849
+ * construction). Returns null when no synthesized backends map exists
13850
+ * (legacy single-backend configs).
13851
+ */
13852
+ getBackends() {
13853
+ return this.config.agent.backends ?? null;
13854
+ }
13227
13855
  /** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
13228
13856
  getMaintenanceStatus() {
13229
13857
  return this.maintenanceScheduler?.getStatus() ?? null;
@@ -13602,8 +14230,8 @@ async function syncMain(repoRoot, opts = {}) {
13602
14230
  }
13603
14231
 
13604
14232
  // src/sessions/search-index.ts
13605
- var fs17 = __toESM(require("fs"));
13606
- var path20 = __toESM(require("path"));
14233
+ var fs18 = __toESM(require("fs"));
14234
+ var path22 = __toESM(require("path"));
13607
14235
  var import_better_sqlite32 = __toESM(require("better-sqlite3"));
13608
14236
  var import_types31 = require("@harness-engineering/types");
13609
14237
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -13648,7 +14276,7 @@ function normalizeFts5Query(query) {
13648
14276
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
13649
14277
  }
13650
14278
  function searchIndexPath(projectPath) {
13651
- return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
14279
+ return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13652
14280
  }
13653
14281
  var FILE_KIND_TO_FILENAME = {
13654
14282
  summary: "summary.md",
@@ -13663,7 +14291,7 @@ var SqliteSearchIndex = class {
13663
14291
  removeSessionStmt;
13664
14292
  totalStmt;
13665
14293
  constructor(dbPath) {
13666
- fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
14294
+ fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
13667
14295
  this.db = new import_better_sqlite32.default(dbPath);
13668
14296
  this.db.pragma("journal_mode = WAL");
13669
14297
  this.db.pragma("synchronous = NORMAL");
@@ -13768,14 +14396,14 @@ function indexSessionDirectory(idx, args) {
13768
14396
  let docsWritten = 0;
13769
14397
  for (const kind of kinds) {
13770
14398
  const fileName = FILE_KIND_TO_FILENAME[kind];
13771
- const filePath = path20.join(args.sessionDir, fileName);
13772
- if (!fs17.existsSync(filePath)) continue;
13773
- let body = fs17.readFileSync(filePath, "utf8");
14399
+ const filePath = path22.join(args.sessionDir, fileName);
14400
+ if (!fs18.existsSync(filePath)) continue;
14401
+ let body = fs18.readFileSync(filePath, "utf8");
13774
14402
  if (Buffer.byteLength(body, "utf8") > cap) {
13775
14403
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
13776
14404
  }
13777
- const stat = fs17.statSync(filePath);
13778
- const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
14405
+ const stat = fs18.statSync(filePath);
14406
+ const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
13779
14407
  idx.upsertSessionDoc({
13780
14408
  sessionId: args.sessionId,
13781
14409
  archived: args.archived,
@@ -13790,17 +14418,17 @@ function indexSessionDirectory(idx, args) {
13790
14418
  }
13791
14419
  function reindexFromArchive(projectPath, opts = {}) {
13792
14420
  const start = Date.now();
13793
- const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
14421
+ const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
13794
14422
  const idx = openSearchIndex(projectPath);
13795
14423
  try {
13796
14424
  idx.resetArchived();
13797
14425
  let sessionsIndexed = 0;
13798
14426
  let docsWritten = 0;
13799
- if (fs17.existsSync(archiveBase)) {
13800
- const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
14427
+ if (fs18.existsSync(archiveBase)) {
14428
+ const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
13801
14429
  for (const entry of entries) {
13802
14430
  if (!entry.isDirectory()) continue;
13803
- const sessionDir = path20.join(archiveBase, entry.name);
14431
+ const sessionDir = path22.join(archiveBase, entry.name);
13804
14432
  const result = indexSessionDirectory(idx, {
13805
14433
  sessionId: entry.name,
13806
14434
  sessionDir,
@@ -13820,8 +14448,8 @@ function reindexFromArchive(projectPath, opts = {}) {
13820
14448
  }
13821
14449
 
13822
14450
  // src/sessions/summarize.ts
13823
- var fs18 = __toESM(require("fs"));
13824
- var path21 = __toESM(require("path"));
14451
+ var fs19 = __toESM(require("fs"));
14452
+ var path23 = __toESM(require("path"));
13825
14453
  var import_types32 = require("@harness-engineering/types");
13826
14454
  var import_types33 = require("@harness-engineering/types");
13827
14455
  var LLM_SUMMARY_FILE = "llm-summary.md";
@@ -13849,10 +14477,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
13849
14477
  function readInputCorpus(archiveDir) {
13850
14478
  const parts = [];
13851
14479
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
13852
- const p = path21.join(archiveDir, filename);
13853
- if (!fs18.existsSync(p)) continue;
14480
+ const p = path23.join(archiveDir, filename);
14481
+ if (!fs19.existsSync(p)) continue;
13854
14482
  try {
13855
- const content = fs18.readFileSync(p, "utf8");
14483
+ const content = fs19.readFileSync(p, "utf8");
13856
14484
  if (content.trim().length === 0) continue;
13857
14485
  parts.push(`## FILE: ${kind}
13858
14486
 
@@ -13903,7 +14531,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
13903
14531
  return lines.join("\n");
13904
14532
  }
13905
14533
  function writeStubMarkdown(archiveDir, reason) {
13906
- const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
14534
+ const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
13907
14535
  const body = `---
13908
14536
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
13909
14537
  schemaVersion: 1
@@ -13914,12 +14542,12 @@ status: failed
13914
14542
 
13915
14543
  - reason: ${reason}
13916
14544
  `;
13917
- fs18.writeFileSync(filePath, body, "utf8");
14545
+ fs19.writeFileSync(filePath, body, "utf8");
13918
14546
  return filePath;
13919
14547
  }
13920
14548
  async function summarizeArchivedSession(ctx) {
13921
14549
  const writeStubOnError = ctx.writeStubOnError ?? true;
13922
- if (!fs18.existsSync(ctx.archiveDir)) {
14550
+ if (!fs19.existsSync(ctx.archiveDir)) {
13923
14551
  return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
13924
14552
  }
13925
14553
  const corpus = readInputCorpus(ctx.archiveDir);
@@ -13980,9 +14608,9 @@ async function summarizeArchivedSession(ctx) {
13980
14608
  outputTokens: response.tokenUsage.outputTokens,
13981
14609
  schemaVersion: 1
13982
14610
  };
13983
- const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14611
+ const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13984
14612
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
13985
- fs18.writeFileSync(filePath, body, "utf8");
14613
+ fs19.writeFileSync(filePath, body, "utf8");
13986
14614
  return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
13987
14615
  }
13988
14616
  function isSummaryEnabled(config) {
@@ -14096,7 +14724,10 @@ function buildArchiveHooks(opts) {
14096
14724
  computeRateLimitDelay,
14097
14725
  createBackend,
14098
14726
  createEmptyState,
14727
+ crossFieldRoutingIssues,
14099
14728
  detectScopeTier,
14729
+ discoverSkillCatalog,
14730
+ discoverSkillCatalogNames,
14100
14731
  emitProposalApproved,
14101
14732
  emitProposalCreated,
14102
14733
  emitProposalRejected,
@@ -14122,6 +14753,7 @@ function buildArchiveHooks(opts) {
14122
14753
  resolveEscalationConfig,
14123
14754
  resolveOrchestratorId,
14124
14755
  routeIssue,
14756
+ routingWarnings,
14125
14757
  runGate,
14126
14758
  savePublishedIndex,
14127
14759
  searchIndexPath,