@harness-engineering/orchestrator 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,12 +32,14 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AnalysisArchive: () => AnalysisArchive,
34
34
  BUILT_IN_TASKS: () => BUILT_IN_TASKS,
35
+ BackendDefSchema: () => BackendDefSchema,
35
36
  BackendRouter: () => BackendRouter,
36
37
  ClaimManager: () => ClaimManager,
37
38
  GateNotReadyError: () => GateNotReadyError,
38
39
  GateRunError: () => GateRunError,
39
40
  InteractionQueue: () => InteractionQueue,
40
41
  LinearGraphQLStub: () => LinearGraphQLStub,
42
+ LocalModelResolver: () => LocalModelResolver,
41
43
  MAX_ATTEMPTS: () => MAX_ATTEMPTS,
42
44
  MockBackend: () => MockBackend,
43
45
  ORCHESTRATOR_IDENTITY_FILE: () => ORCHESTRATOR_IDENTITY_FILE,
@@ -48,6 +50,8 @@ __export(index_exports, {
48
50
  PromptRenderer: () => PromptRenderer,
49
51
  RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
50
52
  RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
53
+ RoutingConfigSchema: () => RoutingConfigSchema,
54
+ RoutingValueSchema: () => RoutingValueSchema,
51
55
  SinkConfigError: () => SinkConfigError,
52
56
  SinkRegistry: () => SinkRegistry,
53
57
  SlackSink: () => SlackSink,
@@ -67,7 +71,11 @@ __export(index_exports, {
67
71
  computeRateLimitDelay: () => computeRateLimitDelay,
68
72
  createBackend: () => createBackend,
69
73
  createEmptyState: () => createEmptyState,
74
+ crossFieldRoutingIssues: () => crossFieldRoutingIssues,
75
+ defaultFetchModels: () => defaultFetchModels,
70
76
  detectScopeTier: () => detectScopeTier,
77
+ discoverSkillCatalog: () => discoverSkillCatalog,
78
+ discoverSkillCatalogNames: () => discoverSkillCatalogNames,
71
79
  emitProposalApproved: () => emitProposalApproved,
72
80
  emitProposalCreated: () => emitProposalCreated,
73
81
  emitProposalRejected: () => emitProposalRejected,
@@ -83,6 +91,7 @@ __export(index_exports, {
83
91
  loadPublishedIndex: () => loadPublishedIndex,
84
92
  migrateAgentConfig: () => migrateAgentConfig,
85
93
  normalizeFts5Query: () => normalizeFts5Query,
94
+ normalizeLocalModel: () => normalizeLocalModel,
86
95
  openSearchIndex: () => openSearchIndex,
87
96
  promote: () => promote,
88
97
  reconcile: () => reconcile,
@@ -93,6 +102,7 @@ __export(index_exports, {
93
102
  resolveEscalationConfig: () => resolveEscalationConfig,
94
103
  resolveOrchestratorId: () => resolveOrchestratorId,
95
104
  routeIssue: () => routeIssue,
105
+ routingWarnings: () => routingWarnings,
96
106
  runGate: () => runGate,
97
107
  savePublishedIndex: () => savePublishedIndex,
98
108
  searchIndexPath: () => searchIndexPath,
@@ -138,7 +148,7 @@ function sortCandidates(issues) {
138
148
  return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
139
149
  });
140
150
  }
141
- function isEligible(issue, state, activeStates, terminalStates) {
151
+ function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
142
152
  if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
143
153
  return false;
144
154
  }
@@ -160,6 +170,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
160
170
  if (state.completed.has(issue.id)) {
161
171
  return false;
162
172
  }
173
+ if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
174
+ return false;
175
+ }
163
176
  if (normalizedState === "todo" && issue.blockedBy.length > 0) {
164
177
  const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
165
178
  if (blocker.state === null) return true;
@@ -171,9 +184,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
171
184
  }
172
185
  return true;
173
186
  }
174
- function selectCandidates(issues, state, activeStates, terminalStates) {
187
+ function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
175
188
  const sorted = sortCandidates(issues);
176
- return sorted.filter((issue) => isEligible(issue, state, activeStates, terminalStates));
189
+ return sorted.filter(
190
+ (issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
191
+ );
177
192
  }
178
193
 
179
194
  // src/core/concurrency.ts
@@ -769,7 +784,8 @@ function handleTick(state, event, config) {
769
784
  candidates,
770
785
  next,
771
786
  config.tracker.activeStates,
772
- config.tracker.terminalStates
787
+ config.tracker.terminalStates,
788
+ event.selfAssignee
773
789
  );
774
790
  const escalationConfig = resolveEscalationConfig(config);
775
791
  for (const issue of eligible) {
@@ -1255,7 +1271,7 @@ var ClaimManager = class {
1255
1271
  const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
1256
1272
  if (!claimResult.ok) return claimResult;
1257
1273
  if (this.verifyDelayMs > 0) {
1258
- await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
1274
+ await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
1259
1275
  }
1260
1276
  const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
1261
1277
  if (!statesResult.ok) return statesResult;
@@ -1907,8 +1923,9 @@ function formatFilesList(files) {
1907
1923
  }
1908
1924
 
1909
1925
  // src/workflow/loader.ts
1910
- var fs6 = __toESM(require("fs/promises"));
1911
- var import_yaml = require("yaml");
1926
+ var fs7 = __toESM(require("fs/promises"));
1927
+ var path7 = __toESM(require("path"));
1928
+ var import_yaml2 = require("yaml");
1912
1929
  var import_types3 = require("@harness-engineering/types");
1913
1930
 
1914
1931
  // src/workflow/config.ts
@@ -1960,16 +1977,29 @@ var BackendDefSchema = import_zod.z.discriminatedUnion("type", [
1960
1977
  probeIntervalMs: import_zod.z.number().int().min(1e3).optional()
1961
1978
  }).strict()
1962
1979
  ]);
1980
+ var RoutingValueSchema = import_zod.z.union([
1981
+ import_zod.z.string().min(1),
1982
+ import_zod.z.array(import_zod.z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
1983
+ ]);
1963
1984
  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(),
1985
+ default: RoutingValueSchema,
1986
+ "quick-fix": RoutingValueSchema.optional(),
1987
+ "guided-change": RoutingValueSchema.optional(),
1988
+ "full-exploration": RoutingValueSchema.optional(),
1989
+ diagnostic: RoutingValueSchema.optional(),
1969
1990
  intelligence: import_zod.z.object({
1970
- sel: import_zod.z.string().optional(),
1971
- pesl: import_zod.z.string().optional()
1972
- }).strict().optional()
1991
+ sel: RoutingValueSchema.optional(),
1992
+ pesl: RoutingValueSchema.optional()
1993
+ }).strict().optional(),
1994
+ // --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
1995
+ isolation: import_zod.z.object({
1996
+ none: RoutingValueSchema.optional(),
1997
+ container: RoutingValueSchema.optional(),
1998
+ "remote-sandbox": RoutingValueSchema.optional()
1999
+ }).strict().optional(),
2000
+ // --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
2001
+ skills: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional(),
2002
+ modes: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional()
1973
2003
  }).strict();
1974
2004
 
1975
2005
  // src/workflow/config.ts
@@ -1978,13 +2008,17 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
1978
2008
  function crossFieldRoutingIssues(backends, routing) {
1979
2009
  const issues = [];
1980
2010
  const names = new Set(Object.keys(backends));
1981
- const checkRef = (path22, name) => {
1982
- if (name !== void 0 && !names.has(name)) {
2011
+ const checkRef = (path24, value) => {
2012
+ if (value === void 0) return;
2013
+ const entries = Array.isArray(value) ? value : [value];
2014
+ entries.forEach((name, idx) => {
2015
+ if (names.has(name)) return;
2016
+ const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
1983
2017
  issues.push({
1984
- path: path22,
1985
- message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
2018
+ path: pathWithIdx,
2019
+ message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1986
2020
  });
1987
- }
2021
+ });
1988
2022
  };
1989
2023
  checkRef(["default"], routing.default);
1990
2024
  checkRef(["quick-fix"], routing["quick-fix"]);
@@ -1993,9 +2027,44 @@ function crossFieldRoutingIssues(backends, routing) {
1993
2027
  checkRef(["diagnostic"], routing.diagnostic);
1994
2028
  checkRef(["intelligence", "sel"], routing.intelligence?.sel);
1995
2029
  checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
2030
+ checkRef(["isolation", "none"], routing.isolation?.none);
2031
+ checkRef(["isolation", "container"], routing.isolation?.container);
2032
+ checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
2033
+ if (routing.skills) {
2034
+ for (const [skill, value] of Object.entries(routing.skills)) {
2035
+ checkRef(["skills", skill], value);
2036
+ }
2037
+ }
2038
+ if (routing.modes) {
2039
+ for (const [mode, value] of Object.entries(routing.modes)) {
2040
+ checkRef(["modes", mode], value);
2041
+ }
2042
+ }
1996
2043
  return issues;
1997
2044
  }
1998
- function validateWorkflowConfig(config) {
2045
+ function routingWarnings(routing, knownSkillNames) {
2046
+ const warnings = [];
2047
+ if (knownSkillNames.length > 0 && routing.skills) {
2048
+ const known = new Set(knownSkillNames);
2049
+ for (const name of Object.keys(routing.skills)) {
2050
+ if (known.has(name)) continue;
2051
+ warnings.push(
2052
+ `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.`
2053
+ );
2054
+ }
2055
+ }
2056
+ if (routing.modes) {
2057
+ const standardModes = new Set(import_types2.STANDARD_COGNITIVE_MODES);
2058
+ for (const mode of Object.keys(routing.modes)) {
2059
+ if (standardModes.has(mode)) continue;
2060
+ warnings.push(
2061
+ `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.`
2062
+ );
2063
+ }
2064
+ }
2065
+ return warnings;
2066
+ }
2067
+ function validateWorkflowConfig(config, options = {}) {
1999
2068
  if (!config || typeof config !== "object")
2000
2069
  return (0, import_types2.Err)(new Error("Config is missing or not an object"));
2001
2070
  const c = config;
@@ -2011,6 +2080,7 @@ function validateWorkflowConfig(config) {
2011
2080
  if (!hasLegacyBackend && !hasModernBackends) {
2012
2081
  return (0, import_types2.Err)(new Error("Config must define agent.backend or agent.backends."));
2013
2082
  }
2083
+ const warnings = [];
2014
2084
  if (hasModernBackends) {
2015
2085
  const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
2016
2086
  if (!backendsParsed.success) {
@@ -2021,9 +2091,10 @@ function validateWorkflowConfig(config) {
2021
2091
  return (0, import_types2.Err)(new Error(`agent.routing: ${routingParsed.error.message}`));
2022
2092
  }
2023
2093
  if (routingParsed.data) {
2094
+ const routingData = routingParsed.data;
2024
2095
  const cross = crossFieldRoutingIssues(
2025
2096
  backendsParsed.data,
2026
- routingParsed.data
2097
+ routingData
2027
2098
  );
2028
2099
  if (cross.length > 0) {
2029
2100
  return (0, import_types2.Err)(
@@ -2032,9 +2103,10 @@ function validateWorkflowConfig(config) {
2032
2103
  )
2033
2104
  );
2034
2105
  }
2106
+ warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
2035
2107
  }
2036
2108
  }
2037
- return (0, import_types2.Ok)(config);
2109
+ return (0, import_types2.Ok)({ config, warnings });
2038
2110
  }
2039
2111
  function getDefaultConfig() {
2040
2112
  return {
@@ -2087,11 +2159,55 @@ function getDefaultConfig() {
2087
2159
  };
2088
2160
  }
2089
2161
 
2162
+ // src/workflow/skill-catalog.ts
2163
+ var fs6 = __toESM(require("fs"));
2164
+ var path6 = __toESM(require("path"));
2165
+ var import_yaml = require("yaml");
2166
+ function discoverSkillCatalog(projectRoot) {
2167
+ const skillsRoot = path6.join(projectRoot, "agents", "skills");
2168
+ if (!fs6.existsSync(skillsRoot)) return [];
2169
+ const byName = /* @__PURE__ */ new Map();
2170
+ let hosts;
2171
+ try {
2172
+ hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
2173
+ } catch {
2174
+ return [];
2175
+ }
2176
+ for (const host of hosts) {
2177
+ if (!host.isDirectory()) continue;
2178
+ const hostDir = path6.join(skillsRoot, host.name);
2179
+ let skills;
2180
+ try {
2181
+ skills = fs6.readdirSync(hostDir, { withFileTypes: true });
2182
+ } catch {
2183
+ continue;
2184
+ }
2185
+ for (const skill of skills) {
2186
+ if (!skill.isDirectory()) continue;
2187
+ const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
2188
+ if (!fs6.existsSync(skillYamlPath)) continue;
2189
+ try {
2190
+ const content = fs6.readFileSync(skillYamlPath, "utf-8");
2191
+ const parsed = (0, import_yaml.parse)(content);
2192
+ if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
2193
+ const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
2194
+ byName.set(parsed.name, entry);
2195
+ }
2196
+ } catch {
2197
+ }
2198
+ }
2199
+ }
2200
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
2201
+ }
2202
+ function discoverSkillCatalogNames(projectRoot) {
2203
+ return discoverSkillCatalog(projectRoot).map((e) => e.name);
2204
+ }
2205
+
2090
2206
  // src/workflow/loader.ts
2091
2207
  var WorkflowLoader = class {
2092
2208
  async loadWorkflow(filePath) {
2093
2209
  try {
2094
- const content = await fs6.readFile(filePath, "utf-8");
2210
+ const content = await fs7.readFile(filePath, "utf-8");
2095
2211
  const parts = content.split("---");
2096
2212
  if (parts.length < 3) {
2097
2213
  return (0, import_types3.Err)(
@@ -2102,14 +2218,17 @@ var WorkflowLoader = class {
2102
2218
  }
2103
2219
  const yamlContent = parts[1].trim();
2104
2220
  const promptTemplate = parts.slice(2).join("---").trim();
2105
- const configData = (0, import_yaml.parse)(yamlContent);
2106
- const configResult = validateWorkflowConfig(configData);
2221
+ const configData = (0, import_yaml2.parse)(yamlContent);
2222
+ const projectRoot = path7.dirname(path7.resolve(filePath));
2223
+ const knownSkillNames = discoverSkillCatalogNames(projectRoot);
2224
+ const configResult = validateWorkflowConfig(configData, { knownSkillNames });
2107
2225
  if (!configResult.ok) {
2108
2226
  return (0, import_types3.Err)(configResult.error);
2109
2227
  }
2110
2228
  return (0, import_types3.Ok)({
2111
- config: configResult.value,
2112
- promptTemplate
2229
+ config: configResult.value.config,
2230
+ promptTemplate,
2231
+ warnings: configResult.value.warnings
2113
2232
  });
2114
2233
  } catch (error) {
2115
2234
  return (0, import_types3.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2118,7 +2237,7 @@ var WorkflowLoader = class {
2118
2237
  };
2119
2238
 
2120
2239
  // src/tracker/adapters/roadmap.ts
2121
- var fs7 = __toESM(require("fs/promises"));
2240
+ var fs8 = __toESM(require("fs/promises"));
2122
2241
  var import_node_crypto2 = require("crypto");
2123
2242
  var import_core = require("@harness-engineering/core");
2124
2243
  var import_types4 = require("@harness-engineering/types");
@@ -2149,7 +2268,7 @@ var RoadmapTrackerAdapter = class {
2149
2268
  async fetchIssuesByStates(stateNames) {
2150
2269
  try {
2151
2270
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2152
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2271
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2153
2272
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2154
2273
  if (!roadmapResult.ok) return roadmapResult;
2155
2274
  const issues = [];
@@ -2181,7 +2300,7 @@ var RoadmapTrackerAdapter = class {
2181
2300
  if (!terminal) {
2182
2301
  return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
2183
2302
  }
2184
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2303
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2185
2304
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2186
2305
  if (!roadmapResult.ok) return roadmapResult;
2187
2306
  const roadmap = roadmapResult.value;
@@ -2190,7 +2309,7 @@ var RoadmapTrackerAdapter = class {
2190
2309
  const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
2191
2310
  if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
2192
2311
  target.status = terminal;
2193
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2312
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2194
2313
  return (0, import_types4.Ok)(void 0);
2195
2314
  } catch (error) {
2196
2315
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2204,19 +2323,22 @@ var RoadmapTrackerAdapter = class {
2204
2323
  async claimIssue(issueId, orchestratorId) {
2205
2324
  try {
2206
2325
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2207
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2326
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2208
2327
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2209
2328
  if (!roadmapResult.ok) return roadmapResult;
2210
2329
  const roadmap = roadmapResult.value;
2211
2330
  const target = this.findFeatureById(roadmap.milestones, issueId);
2212
2331
  if (!target) return (0, import_types4.Ok)(void 0);
2332
+ if (target.assignee != null && target.assignee !== orchestratorId) {
2333
+ return (0, import_types4.Ok)(void 0);
2334
+ }
2213
2335
  if (target.status === "in-progress" && target.assignee === orchestratorId) {
2214
2336
  return (0, import_types4.Ok)(void 0);
2215
2337
  }
2216
2338
  target.status = "in-progress";
2217
2339
  target.assignee = orchestratorId;
2218
2340
  target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2219
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2341
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2220
2342
  return (0, import_types4.Ok)(void 0);
2221
2343
  } catch (error) {
2222
2344
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2233,7 +2355,7 @@ var RoadmapTrackerAdapter = class {
2233
2355
  if (!activeState) {
2234
2356
  return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
2235
2357
  }
2236
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2358
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2237
2359
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2238
2360
  if (!roadmapResult.ok) return roadmapResult;
2239
2361
  const roadmap = roadmapResult.value;
@@ -2245,7 +2367,7 @@ var RoadmapTrackerAdapter = class {
2245
2367
  target.status = activeState;
2246
2368
  target.assignee = null;
2247
2369
  target.updatedAt = null;
2248
- await fs7.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2370
+ await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
2249
2371
  return (0, import_types4.Ok)(void 0);
2250
2372
  } catch (error) {
2251
2373
  return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
@@ -2267,7 +2389,7 @@ var RoadmapTrackerAdapter = class {
2267
2389
  async fetchIssueStatesByIds(issueIds) {
2268
2390
  try {
2269
2391
  if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
2270
- const content = await fs7.readFile(this.config.filePath, "utf-8");
2392
+ const content = await fs8.readFile(this.config.filePath, "utf-8");
2271
2393
  const roadmapResult = (0, import_core.parseRoadmap)(content);
2272
2394
  if (!roadmapResult.ok) return roadmapResult;
2273
2395
  const issueMap = /* @__PURE__ */ new Map();
@@ -2332,8 +2454,8 @@ var LinearGraphQLStub = class {
2332
2454
  };
2333
2455
 
2334
2456
  // src/workspace/manager.ts
2335
- var fs8 = __toESM(require("fs/promises"));
2336
- var path6 = __toESM(require("path"));
2457
+ var fs9 = __toESM(require("fs/promises"));
2458
+ var path8 = __toESM(require("path"));
2337
2459
  var import_node_child_process2 = require("child_process");
2338
2460
  var import_node_util2 = require("util");
2339
2461
  var import_types6 = require("@harness-engineering/types");
@@ -2364,15 +2486,15 @@ var WorkspaceManager = class {
2364
2486
  */
2365
2487
  resolvePath(identifier) {
2366
2488
  const sanitized = this.sanitizeIdentifier(identifier);
2367
- return path6.join(this.config.root, sanitized);
2489
+ return path8.join(this.config.root, sanitized);
2368
2490
  }
2369
2491
  /**
2370
2492
  * Discovers the git repository root from the workspace root directory.
2371
2493
  */
2372
2494
  async getRepoRoot() {
2373
2495
  if (this.repoRoot) return this.repoRoot;
2374
- const root = path6.resolve(this.config.root);
2375
- await fs8.mkdir(root, { recursive: true });
2496
+ const root = path8.resolve(this.config.root);
2497
+ await fs9.mkdir(root, { recursive: true });
2376
2498
  const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
2377
2499
  this.repoRoot = stdout.trim();
2378
2500
  return this.repoRoot;
@@ -2383,23 +2505,23 @@ var WorkspaceManager = class {
2383
2505
  */
2384
2506
  async ensureWorkspace(identifier) {
2385
2507
  try {
2386
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2508
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2387
2509
  try {
2388
- await fs8.access(path6.join(workspacePath, ".git"));
2510
+ await fs9.access(path8.join(workspacePath, ".git"));
2389
2511
  const repoRoot2 = await this.getRepoRoot();
2390
2512
  try {
2391
2513
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2392
2514
  } catch {
2393
- await fs8.rm(workspacePath, { recursive: true, force: true });
2515
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2394
2516
  }
2395
2517
  } catch {
2396
2518
  try {
2397
- await fs8.access(workspacePath);
2519
+ await fs9.access(workspacePath);
2398
2520
  const repoRoot2 = await this.getRepoRoot();
2399
2521
  try {
2400
2522
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
2401
2523
  } catch {
2402
- await fs8.rm(workspacePath, { recursive: true, force: true });
2524
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2403
2525
  }
2404
2526
  } catch {
2405
2527
  }
@@ -2495,7 +2617,7 @@ var WorkspaceManager = class {
2495
2617
  async exists(identifier) {
2496
2618
  try {
2497
2619
  const workspacePath = this.resolvePath(identifier);
2498
- await fs8.access(workspacePath);
2620
+ await fs9.access(workspacePath);
2499
2621
  return true;
2500
2622
  } catch {
2501
2623
  return false;
@@ -2508,9 +2630,9 @@ var WorkspaceManager = class {
2508
2630
  */
2509
2631
  async findPushedBranch(identifier) {
2510
2632
  try {
2511
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2633
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2512
2634
  try {
2513
- await fs8.access(path6.join(workspacePath, ".git"));
2635
+ await fs9.access(path8.join(workspacePath, ".git"));
2514
2636
  } catch {
2515
2637
  return null;
2516
2638
  }
@@ -2616,12 +2738,12 @@ var WorkspaceManager = class {
2616
2738
  */
2617
2739
  async removeWorkspace(identifier) {
2618
2740
  try {
2619
- const workspacePath = path6.resolve(this.resolvePath(identifier));
2741
+ const workspacePath = path8.resolve(this.resolvePath(identifier));
2620
2742
  try {
2621
2743
  const repoRoot = await this.getRepoRoot();
2622
2744
  await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
2623
2745
  } catch {
2624
- await fs8.rm(workspacePath, { recursive: true, force: true });
2746
+ await fs9.rm(workspacePath, { recursive: true, force: true });
2625
2747
  }
2626
2748
  return (0, import_types6.Ok)(void 0);
2627
2749
  } catch (error) {
@@ -2646,7 +2768,7 @@ var WorkspaceHooks = class {
2646
2768
  if (!command) {
2647
2769
  return (0, import_types7.Ok)(void 0);
2648
2770
  }
2649
- return new Promise((resolve7) => {
2771
+ return new Promise((resolve8) => {
2650
2772
  const filteredEnv = {};
2651
2773
  for (const [k, v] of Object.entries(process.env)) {
2652
2774
  if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
@@ -2659,19 +2781,19 @@ var WorkspaceHooks = class {
2659
2781
  });
2660
2782
  const timeout = setTimeout(() => {
2661
2783
  child.kill();
2662
- resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2784
+ resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2663
2785
  }, this.config.timeoutMs);
2664
2786
  child.on("exit", (code) => {
2665
2787
  clearTimeout(timeout);
2666
2788
  if (code === 0) {
2667
- resolve7((0, import_types7.Ok)(void 0));
2789
+ resolve8((0, import_types7.Ok)(void 0));
2668
2790
  } else {
2669
- resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2791
+ resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2670
2792
  }
2671
2793
  });
2672
2794
  child.on("error", (error) => {
2673
2795
  clearTimeout(timeout);
2674
- resolve7((0, import_types7.Err)(error));
2796
+ resolve8((0, import_types7.Err)(error));
2675
2797
  });
2676
2798
  });
2677
2799
  }
@@ -2709,7 +2831,7 @@ var MockBackend = class {
2709
2831
  content: "Thinking...",
2710
2832
  sessionId: session.sessionId
2711
2833
  };
2712
- await new Promise((resolve7) => setTimeout(resolve7, 100));
2834
+ await new Promise((resolve8) => setTimeout(resolve8, 100));
2713
2835
  yield {
2714
2836
  type: "thought",
2715
2837
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2761,12 +2883,12 @@ var PromptRenderer = class {
2761
2883
 
2762
2884
  // src/orchestrator.ts
2763
2885
  var import_node_events = require("events");
2764
- var path19 = __toESM(require("path"));
2886
+ var path21 = __toESM(require("path"));
2765
2887
  var import_node_crypto16 = require("crypto");
2766
2888
  var import_core14 = require("@harness-engineering/core");
2767
2889
 
2768
2890
  // src/intelligence/pipeline-runner.ts
2769
- var path7 = __toESM(require("path"));
2891
+ var path9 = __toESM(require("path"));
2770
2892
  var import_intelligence = require("@harness-engineering/intelligence");
2771
2893
  var import_core2 = require("@harness-engineering/core");
2772
2894
  var CONNECTION_ERROR_PATTERNS = [
@@ -2885,7 +3007,7 @@ var IntelligencePipelineRunner = class {
2885
3007
  }
2886
3008
  async loadGraphStore() {
2887
3009
  try {
2888
- const graphDir = path7.join(this.ctx.config.workspace.root, "..", "graph");
3010
+ const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
2889
3011
  const loaded = await this.ctx.graphStore.load(graphDir);
2890
3012
  if (loaded) {
2891
3013
  this.ctx.logger.info("Graph store loaded from disk");
@@ -3163,7 +3285,7 @@ var IntelligencePipelineRunner = class {
3163
3285
  };
3164
3286
 
3165
3287
  // src/completion/handler.ts
3166
- var path8 = __toESM(require("path"));
3288
+ var path10 = __toESM(require("path"));
3167
3289
  var import_core3 = require("@harness-engineering/core");
3168
3290
  var CompletionHandler = class {
3169
3291
  ctx;
@@ -3246,7 +3368,7 @@ var CompletionHandler = class {
3246
3368
  result: outcome.result
3247
3369
  });
3248
3370
  if (this.ctx.graphStore) {
3249
- const graphDir = path8.join(this.ctx.config.workspace.root, "..", "graph");
3371
+ const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
3250
3372
  await this.ctx.graphStore.save(graphDir);
3251
3373
  }
3252
3374
  } catch (err) {
@@ -3524,6 +3646,14 @@ var DEFAULT_PROBE_INTERVAL_MS = 3e4;
3524
3646
  var MIN_PROBE_INTERVAL_MS = 1e3;
3525
3647
  var DEFAULT_API_KEY = "lm-studio";
3526
3648
  var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
3649
+ function normalizeLocalModel(input) {
3650
+ if (input === void 0) return [];
3651
+ if (typeof input === "string") return [input];
3652
+ if (input.length === 0) {
3653
+ throw new Error("localModel array must be non-empty when provided");
3654
+ }
3655
+ return [...input];
3656
+ }
3527
3657
  var noopLogger = {
3528
3658
  info: () => void 0,
3529
3659
  warn: () => void 0
@@ -3728,11 +3858,11 @@ function detectLegacyFields(agent) {
3728
3858
  }
3729
3859
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3730
3860
  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;
3861
+ for (const path24 of presentLegacy) {
3862
+ if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
3863
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
3734
3864
  warnings.push(
3735
- `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3865
+ `Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3736
3866
  );
3737
3867
  }
3738
3868
  return warnings;
@@ -3760,7 +3890,7 @@ function migrateAgentConfig(agent) {
3760
3890
  }
3761
3891
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3762
3892
  const warnings = presentLegacy.map(
3763
- (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3893
+ (path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3764
3894
  );
3765
3895
  return {
3766
3896
  config: { ...agent, backends, routing },
@@ -3823,61 +3953,160 @@ function synthesizeLocal(agent) {
3823
3953
  }
3824
3954
 
3825
3955
  // src/agent/backend-router.ts
3956
+ function toArray(value) {
3957
+ return Array.isArray(value) ? value : [value];
3958
+ }
3826
3959
  var BackendRouter = class {
3827
3960
  backends;
3828
3961
  routing;
3962
+ decisionBus;
3829
3963
  constructor(opts) {
3830
3964
  this.backends = opts.backends;
3831
3965
  this.routing = opts.routing;
3966
+ this.decisionBus = opts.decisionBus;
3832
3967
  this.validateReferences();
3833
3968
  }
3834
3969
  /**
3835
- * Returns the backend name for a given use case.
3970
+ * Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
3836
3971
  *
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`.
3972
+ * @param useCase the routing query
3973
+ * @param opts.invocationOverride if set and the named backend exists,
3974
+ * beats all other sources (D7 — the `--backend <name>` escape hatch)
3975
+ */
3976
+ resolve(useCase, opts) {
3977
+ const startedAt = performance.now();
3978
+ const path24 = [];
3979
+ const tryChain = (source, value) => {
3980
+ if (value === void 0) return void 0;
3981
+ for (const name of toArray(value)) {
3982
+ const step = { source, candidate: name, outcome: "considered" };
3983
+ path24.push(step);
3984
+ if (this.backends[name]) {
3985
+ step.outcome = "chosen";
3986
+ return name;
3987
+ }
3988
+ step.outcome = "unknown-backend";
3989
+ }
3990
+ return void 0;
3991
+ };
3992
+ const decide = (backendName) => {
3993
+ const def = this.backends[backendName];
3994
+ if (!def) {
3995
+ throw new Error(
3996
+ `BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
3997
+ );
3998
+ }
3999
+ return {
4000
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4001
+ useCase,
4002
+ resolutionPath: path24,
4003
+ backendName,
4004
+ backendType: def.type,
4005
+ durationMs: performance.now() - startedAt
4006
+ };
4007
+ };
4008
+ const emitAndReturn = (decision) => {
4009
+ this.decisionBus?.emit(decision);
4010
+ return decision;
4011
+ };
4012
+ const fromInvocation = tryChain(
4013
+ "invocation",
4014
+ opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
4015
+ );
4016
+ if (fromInvocation) return emitAndReturn(decide(fromInvocation));
4017
+ if (useCase.kind === "skill") {
4018
+ const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
4019
+ if (fromSkill) return emitAndReturn(decide(fromSkill));
4020
+ }
4021
+ const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
4022
+ if (mode !== void 0) {
4023
+ const fromMode = tryChain("mode", this.routing.modes?.[mode]);
4024
+ if (fromMode) return emitAndReturn(decide(fromMode));
4025
+ }
4026
+ const fromExisting = this.resolveExistingUseCase(useCase);
4027
+ if (fromExisting !== void 0) {
4028
+ const chained = tryChain("tier", fromExisting);
4029
+ if (chained) return emitAndReturn(decide(chained));
4030
+ }
4031
+ const fromDefault = tryChain("default", this.routing.default);
4032
+ if (fromDefault) return emitAndReturn(decide(fromDefault));
4033
+ const knownList = Object.keys(this.backends).join(", ") || "(none)";
4034
+ throw new Error(
4035
+ `BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
4036
+ );
4037
+ }
4038
+ /**
4039
+ * Returns the {@link BackendDef} reference for the resolved name.
4040
+ * Identity-equal to the entry in `backends` (no copy) so callers
4041
+ * relying on reference equality (SC21) continue to work.
4042
+ */
4043
+ resolveDefinition(useCase, opts) {
4044
+ const decision = this.resolve(useCase, opts);
4045
+ const def = this.backends[decision.backendName];
4046
+ if (!def) {
4047
+ throw new Error(
4048
+ `BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
4049
+ );
4050
+ }
4051
+ return def;
4052
+ }
4053
+ /**
4054
+ * Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
4055
+ * for callers that need both. Replaces the previous pattern of
4056
+ * `resolveDefinition(useCase) + resolve(useCase)` which produced two
4057
+ * RoutingDecision emissions per dispatch — doubling routing-decision
4058
+ * log volume now that Phase 4 emits.
4059
+ *
4060
+ * Identity-equal `BackendDef` (no copy) so callers relying on
4061
+ * reference equality (SC21) continue to work.
3841
4062
  */
3842
- resolve(useCase) {
4063
+ resolveDecisionAndDef(useCase, opts) {
4064
+ const decision = this.resolve(useCase, opts);
4065
+ const def = this.backends[decision.backendName];
4066
+ if (!def) {
4067
+ throw new Error(
4068
+ `BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
4069
+ );
4070
+ }
4071
+ return { decision, def };
4072
+ }
4073
+ /**
4074
+ * The pre-Spec-B resolution helper: returns the configured
4075
+ * {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
4076
+ * use cases (or `undefined` for skill/mode use cases, which are owned
4077
+ * by the per-skill / per-mode steps in {@link resolve}). Returning
4078
+ * `undefined` lets the caller fall through to `routing.default`.
4079
+ */
4080
+ resolveExistingUseCase(useCase) {
3843
4081
  switch (useCase.kind) {
3844
4082
  case "tier": {
3845
- const named = this.routing[useCase.tier];
3846
- return named ?? this.routing.default;
4083
+ const tierMap = this.routing;
4084
+ return tierMap[useCase.tier];
3847
4085
  }
3848
4086
  case "intelligence": {
3849
4087
  const intel = this.routing.intelligence;
3850
- return intel?.[useCase.layer] ?? this.routing.default;
4088
+ return intel?.[useCase.layer];
3851
4089
  }
3852
4090
  case "isolation": {
3853
4091
  const iso = this.routing.isolation;
3854
- return iso?.[useCase.tier] ?? this.routing.default;
4092
+ return iso?.[useCase.tier];
3855
4093
  }
3856
4094
  case "maintenance":
3857
4095
  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
- );
4096
+ return void 0;
4097
+ case "skill":
4098
+ case "mode":
4099
+ return void 0;
3873
4100
  }
3874
- return def;
3875
4101
  }
3876
4102
  validateReferences() {
3877
4103
  const known = new Set(Object.keys(this.backends));
3878
4104
  const missing = [];
3879
- const check = (path22, name) => {
3880
- if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
4105
+ const check = (label, value) => {
4106
+ if (value === void 0) return;
4107
+ for (const name of toArray(value)) {
4108
+ if (!known.has(name)) missing.push({ path: label, name });
4109
+ }
3881
4110
  };
3882
4111
  check("default", this.routing.default);
3883
4112
  check("quick-fix", this.routing["quick-fix"]);
@@ -3889,8 +4118,14 @@ var BackendRouter = class {
3889
4118
  check("isolation.none", this.routing.isolation?.none);
3890
4119
  check("isolation.container", this.routing.isolation?.container);
3891
4120
  check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
4121
+ for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
4122
+ check(`skills.${skill}`, value);
4123
+ }
4124
+ for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
4125
+ check(`modes.${mode}`, value);
4126
+ }
3892
4127
  if (missing.length > 0) {
3893
- const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
4128
+ const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
3894
4129
  const known_ = [...known].join(", ") || "(none)";
3895
4130
  throw new Error(
3896
4131
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3904,11 +4139,11 @@ var import_node_child_process4 = require("child_process");
3904
4139
  var readline = __toESM(require("readline"));
3905
4140
  var import_node_crypto3 = require("crypto");
3906
4141
  var import_types10 = require("@harness-engineering/types");
3907
- function resolveExitCode(code, command, resolve7) {
4142
+ function resolveExitCode(code, command, resolve8) {
3908
4143
  if (code === 0) {
3909
- resolve7((0, import_types10.Ok)(void 0));
4144
+ resolve8((0, import_types10.Ok)(void 0));
3910
4145
  } else {
3911
- resolve7(
4146
+ resolve8(
3912
4147
  (0, import_types10.Err)({
3913
4148
  category: "agent_not_found",
3914
4149
  message: `Claude command '${command}' not found or failed`
@@ -3916,8 +4151,8 @@ function resolveExitCode(code, command, resolve7) {
3916
4151
  );
3917
4152
  }
3918
4153
  }
3919
- function resolveSpawnError(command, resolve7) {
3920
- resolve7((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
4154
+ function resolveSpawnError(command, resolve8) {
4155
+ resolve8((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3921
4156
  }
3922
4157
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3923
4158
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4230,10 +4465,10 @@ var ClaudeBackend = class {
4230
4465
  errRl.close();
4231
4466
  }
4232
4467
  if (exitCode === null) {
4233
- await new Promise((resolve7) => {
4468
+ await new Promise((resolve8) => {
4234
4469
  child.on("exit", (code) => {
4235
4470
  exitCode = code;
4236
- resolve7(null);
4471
+ resolve8(null);
4237
4472
  });
4238
4473
  });
4239
4474
  }
@@ -4255,10 +4490,10 @@ var ClaudeBackend = class {
4255
4490
  return (0, import_types10.Ok)(void 0);
4256
4491
  }
4257
4492
  async healthCheck() {
4258
- return new Promise((resolve7) => {
4493
+ return new Promise((resolve8) => {
4259
4494
  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));
4495
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
4496
+ child.on("error", () => resolveSpawnError(this.command, resolve8));
4262
4497
  });
4263
4498
  }
4264
4499
  };
@@ -5172,14 +5407,14 @@ var SshBackend = class {
5172
5407
  async healthCheck() {
5173
5408
  const args = [...this.buildSshArgs()];
5174
5409
  args[args.length - 1] = "true";
5175
- return new Promise((resolve7) => {
5410
+ return new Promise((resolve8) => {
5176
5411
  let child;
5177
5412
  try {
5178
5413
  child = this.spawnImpl(this.config.sshBinary, args, {
5179
5414
  stdio: ["ignore", "ignore", "pipe"]
5180
5415
  });
5181
5416
  } catch (err) {
5182
- resolve7(
5417
+ resolve8(
5183
5418
  (0, import_types16.Err)({
5184
5419
  category: "agent_not_found",
5185
5420
  message: err instanceof Error ? err.message : "failed to spawn ssh"
@@ -5200,9 +5435,9 @@ var SshBackend = class {
5200
5435
  child.on("close", (code) => {
5201
5436
  clearTimeout(timer);
5202
5437
  if (code === 0) {
5203
- resolve7((0, import_types16.Ok)(void 0));
5438
+ resolve8((0, import_types16.Ok)(void 0));
5204
5439
  } else {
5205
- resolve7(
5440
+ resolve8(
5206
5441
  (0, import_types16.Err)({
5207
5442
  category: "agent_not_found",
5208
5443
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
@@ -5212,7 +5447,7 @@ var SshBackend = class {
5212
5447
  });
5213
5448
  child.on("error", (err) => {
5214
5449
  clearTimeout(timer);
5215
- resolve7((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
5450
+ resolve8((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
5216
5451
  });
5217
5452
  });
5218
5453
  }
@@ -5260,13 +5495,13 @@ async function* readLines(stream) {
5260
5495
  if (buffer.length > 0) yield buffer;
5261
5496
  }
5262
5497
  function waitForExit(child) {
5263
- return new Promise((resolve7) => {
5498
+ return new Promise((resolve8) => {
5264
5499
  if (child.exitCode !== null) {
5265
- resolve7(child.exitCode);
5500
+ resolve8(child.exitCode);
5266
5501
  return;
5267
5502
  }
5268
- child.once("close", (code) => resolve7(code));
5269
- child.once("error", () => resolve7(null));
5503
+ child.once("close", (code) => resolve8(code));
5504
+ child.once("error", () => resolve8(null));
5270
5505
  });
5271
5506
  }
5272
5507
 
@@ -5453,14 +5688,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
5453
5688
  return out;
5454
5689
  }
5455
5690
  runOneShot(binary, args) {
5456
- return new Promise((resolve7) => {
5691
+ return new Promise((resolve8) => {
5457
5692
  let child;
5458
5693
  try {
5459
5694
  child = this.spawnImpl(binary, args, {
5460
5695
  stdio: ["ignore", "pipe", "pipe"]
5461
5696
  });
5462
5697
  } catch (err) {
5463
- resolve7(
5698
+ resolve8(
5464
5699
  (0, import_types17.Err)({
5465
5700
  category: "agent_not_found",
5466
5701
  message: err instanceof Error ? err.message : "failed to spawn runtime"
@@ -5485,9 +5720,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
5485
5720
  child.on("close", (code) => {
5486
5721
  clearTimeout(timer);
5487
5722
  if (code === 0) {
5488
- resolve7((0, import_types17.Ok)(stdout));
5723
+ resolve8((0, import_types17.Ok)(stdout));
5489
5724
  } else {
5490
- resolve7(
5725
+ resolve8(
5491
5726
  (0, import_types17.Err)({
5492
5727
  category: "response_error",
5493
5728
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
@@ -5497,7 +5732,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5497
5732
  });
5498
5733
  child.on("error", (err) => {
5499
5734
  clearTimeout(timer);
5500
- resolve7((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
5735
+ resolve8((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
5501
5736
  });
5502
5737
  });
5503
5738
  }
@@ -5557,13 +5792,13 @@ async function* readLines2(stream) {
5557
5792
  if (buffer.length > 0) yield buffer;
5558
5793
  }
5559
5794
  function waitForExit2(child) {
5560
- return new Promise((resolve7) => {
5795
+ return new Promise((resolve8) => {
5561
5796
  if (child.exitCode !== null) {
5562
- resolve7(child.exitCode);
5797
+ resolve8(child.exitCode);
5563
5798
  return;
5564
5799
  }
5565
- child.once("close", (code) => resolve7(code));
5566
- child.once("error", () => resolve7(null));
5800
+ child.once("close", (code) => resolve8(code));
5801
+ child.once("error", () => resolve8(null));
5567
5802
  });
5568
5803
  }
5569
5804
 
@@ -5761,13 +5996,13 @@ var ContainerBackend = class {
5761
5996
  var import_node_child_process7 = require("child_process");
5762
5997
  var import_types19 = require("@harness-engineering/types");
5763
5998
  function dockerExec(args) {
5764
- return new Promise((resolve7, reject) => {
5999
+ return new Promise((resolve8, reject) => {
5765
6000
  (0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
5766
6001
  if (error) {
5767
6002
  reject(error);
5768
6003
  return;
5769
6004
  }
5770
- resolve7(stdout.trim());
6005
+ resolve8(stdout.trim());
5771
6006
  });
5772
6007
  });
5773
6008
  }
@@ -5826,11 +6061,11 @@ var DockerRuntime = class {
5826
6061
  } finally {
5827
6062
  rl.close();
5828
6063
  }
5829
- const exitCode = await new Promise((resolve7) => {
6064
+ const exitCode = await new Promise((resolve8) => {
5830
6065
  if (child.exitCode !== null) {
5831
- resolve7(child.exitCode);
6066
+ resolve8(child.exitCode);
5832
6067
  } else {
5833
- child.on("exit", (code) => resolve7(code ?? 1));
6068
+ child.on("exit", (code) => resolve8(code ?? 1));
5834
6069
  }
5835
6070
  });
5836
6071
  return exitCode;
@@ -5889,13 +6124,13 @@ var EnvSecretBackend = class {
5889
6124
  var import_node_child_process8 = require("child_process");
5890
6125
  var import_types21 = require("@harness-engineering/types");
5891
6126
  function opExec(args) {
5892
- return new Promise((resolve7, reject) => {
6127
+ return new Promise((resolve8, reject) => {
5893
6128
  (0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
5894
6129
  if (error) {
5895
6130
  reject(error);
5896
6131
  return;
5897
6132
  }
5898
- resolve7(stdout.trim());
6133
+ resolve8(stdout.trim());
5899
6134
  });
5900
6135
  });
5901
6136
  }
@@ -5938,13 +6173,13 @@ var OnePasswordSecretBackend = class {
5938
6173
  var import_node_child_process9 = require("child_process");
5939
6174
  var import_types22 = require("@harness-engineering/types");
5940
6175
  function vaultExec(args, env) {
5941
- return new Promise((resolve7, reject) => {
6176
+ return new Promise((resolve8, reject) => {
5942
6177
  (0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5943
6178
  if (error) {
5944
6179
  reject(error);
5945
6180
  return;
5946
6181
  }
5947
- resolve7(stdout.trim());
6182
+ resolve8(stdout.trim());
5948
6183
  });
5949
6184
  });
5950
6185
  }
@@ -6019,7 +6254,11 @@ var OrchestratorBackendFactory = class {
6019
6254
  opts;
6020
6255
  constructor(opts) {
6021
6256
  this.opts = opts;
6022
- this.router = new BackendRouter({ backends: opts.backends, routing: opts.routing });
6257
+ this.router = new BackendRouter({
6258
+ backends: opts.backends,
6259
+ routing: opts.routing,
6260
+ ...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
6261
+ });
6023
6262
  }
6024
6263
  /**
6025
6264
  * Resolve `useCase` to a backend name, materialize a fresh
@@ -6038,12 +6277,21 @@ var OrchestratorBackendFactory = class {
6038
6277
  * is `undefined` for pure-modern configs. Threading the routed name
6039
6278
  * through dispatch eliminates that gap.
6040
6279
  */
6041
- resolveName(useCase) {
6042
- return this.router.resolve(useCase);
6280
+ resolveName(useCase, opts) {
6281
+ return this.router.resolve(useCase, opts).backendName;
6043
6282
  }
6044
- forUseCase(useCase) {
6045
- const def = this.router.resolveDefinition(useCase);
6046
- const name = this.router.resolve(useCase);
6283
+ /**
6284
+ * Spec B Phase 1: expose the underlying router for callers that need
6285
+ * it directly (e.g., {@link buildIntelligencePipeline} for the
6286
+ * I1 SEL/PESL comparison fix). Read-only access; consumers must not
6287
+ * mutate router state.
6288
+ */
6289
+ getRouter() {
6290
+ return this.router;
6291
+ }
6292
+ forUseCase(useCase, opts) {
6293
+ const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
6294
+ const name = decision.backendName;
6047
6295
  let backend;
6048
6296
  const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
6049
6297
  if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
@@ -6207,15 +6455,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
6207
6455
 
6208
6456
  // src/agent/intelligence-factory.ts
6209
6457
  function buildIntelligencePipeline(deps) {
6210
- const { config } = deps;
6458
+ const { config, router } = deps;
6211
6459
  const intel = config.intelligence;
6212
6460
  if (!intel?.enabled) return null;
6213
6461
  const selProvider = buildAnalysisProviderForLayer("sel", deps);
6214
6462
  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;
6463
+ const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
6464
+ const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
6465
+ const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
6219
6466
  const peslModel = intel.models?.pesl ?? config.agent.model;
6220
6467
  const graphStore = new import_graph.GraphStore();
6221
6468
  const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
@@ -6232,7 +6479,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
6232
6479
  const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
6233
6480
  return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
6234
6481
  }
6235
- const routed = resolveRoutedBackend(layer, config, logger);
6482
+ const routed = resolveRoutedBackend(layer, deps);
6236
6483
  if (!routed) return null;
6237
6484
  const { name, def } = routed;
6238
6485
  const resolver = localResolvers.get(name);
@@ -6257,20 +6504,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
6257
6504
  logger
6258
6505
  });
6259
6506
  }
6260
- function resolveRoutedBackend(layer, config, logger) {
6261
- const routing = config.agent.routing;
6507
+ function resolveRoutedBackend(layer, deps) {
6508
+ const { config, router, logger } = deps;
6262
6509
  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) {
6510
+ if (!backends || !router) return null;
6511
+ try {
6512
+ const decision = router.resolve({ kind: "intelligence", layer });
6513
+ const def = backends[decision.backendName];
6514
+ if (!def) {
6515
+ logger.warn(
6516
+ `Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
6517
+ );
6518
+ return null;
6519
+ }
6520
+ return { name: decision.backendName, def };
6521
+ } catch (err) {
6268
6522
  logger.warn(
6269
- `Intelligence pipeline: routed backend '${name}' for layer '${layer}' is not in agent.backends.`
6523
+ `Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
6270
6524
  );
6271
6525
  return null;
6272
6526
  }
6273
- return { name, def };
6274
6527
  }
6275
6528
  function buildExplicitProvider(provider, selModel, config) {
6276
6529
  if (provider.kind === "anthropic") {
@@ -6305,9 +6558,104 @@ function buildExplicitProvider(provider, selModel, config) {
6305
6558
  });
6306
6559
  }
6307
6560
 
6561
+ // src/routing/decision-bus.ts
6562
+ var RoutingDecisionBus = class {
6563
+ ringBuffer = [];
6564
+ listeners = /* @__PURE__ */ new Set();
6565
+ capacity;
6566
+ logger;
6567
+ constructor(opts) {
6568
+ this.capacity = opts?.capacity ?? 500;
6569
+ this.logger = opts?.logger;
6570
+ }
6571
+ emit(decision) {
6572
+ this.ringBuffer.push(decision);
6573
+ if (this.ringBuffer.length > this.capacity) {
6574
+ this.ringBuffer.shift();
6575
+ }
6576
+ if (this.logger) {
6577
+ this.logger.info("routing-decision", {
6578
+ useCase: decision.useCase,
6579
+ backendName: decision.backendName,
6580
+ resolutionPathLength: decision.resolutionPath.length,
6581
+ durationMs: decision.durationMs
6582
+ });
6583
+ }
6584
+ for (const listener of this.listeners) {
6585
+ try {
6586
+ listener(decision);
6587
+ } catch (err) {
6588
+ if (this.logger) {
6589
+ this.logger.warn("RoutingDecisionBus subscriber threw", {
6590
+ error: String(err)
6591
+ });
6592
+ }
6593
+ }
6594
+ }
6595
+ }
6596
+ recent(filter) {
6597
+ let out = this.ringBuffer.slice();
6598
+ if (filter?.skillName !== void 0) {
6599
+ out = out.filter(
6600
+ (d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
6601
+ );
6602
+ }
6603
+ if (filter?.mode !== void 0) {
6604
+ const m = filter.mode;
6605
+ out = out.filter(
6606
+ (d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
6607
+ );
6608
+ }
6609
+ if (filter?.backendName !== void 0) {
6610
+ out = out.filter((d) => d.backendName === filter.backendName);
6611
+ }
6612
+ if (filter?.limit !== void 0) {
6613
+ out = out.slice(-filter.limit).reverse();
6614
+ } else {
6615
+ out = out.reverse();
6616
+ }
6617
+ return out;
6618
+ }
6619
+ subscribe(listener) {
6620
+ this.listeners.add(listener);
6621
+ return () => {
6622
+ this.listeners.delete(listener);
6623
+ };
6624
+ }
6625
+ /**
6626
+ * Spec B Phase 5 (review-S2 fix): release all subscriber references so
6627
+ * teardown can complete without anchoring closures. Called from
6628
+ * `Orchestrator.stop()` before nulling the bus reference. The bus
6629
+ * remains usable after clear — `subscribe()` works as normal.
6630
+ */
6631
+ clearListeners() {
6632
+ this.listeners.clear();
6633
+ }
6634
+ };
6635
+
6636
+ // src/agent/triage-skill-mapping.ts
6637
+ function resolveSkillForTriage(triageSkill, catalog) {
6638
+ const expected = `harness-${triageSkill}`;
6639
+ const match = catalog.find((e) => e.name === expected);
6640
+ if (!match) return void 0;
6641
+ return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
6642
+ }
6643
+
6644
+ // src/agent/use-case-builder.ts
6645
+ function buildRoutingUseCase(issue, backendParam, catalog) {
6646
+ if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
6647
+ const decision = triageIssue(issue, {});
6648
+ const resolved = resolveSkillForTriage(decision.skill, catalog);
6649
+ if (resolved) {
6650
+ return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
6651
+ }
6652
+ const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
6653
+ return { kind: "tier", tier };
6654
+ }
6655
+
6308
6656
  // src/server/http.ts
6309
6657
  var http = __toESM(require("http"));
6310
- var path15 = __toESM(require("path"));
6658
+ var path17 = __toESM(require("path"));
6311
6659
  var import_core11 = require("@harness-engineering/core");
6312
6660
 
6313
6661
  // src/server/websocket.ts
@@ -6370,7 +6718,7 @@ var import_zod3 = require("zod");
6370
6718
  // src/server/utils.ts
6371
6719
  var DEFAULT_MAX_BYTES = 1048576;
6372
6720
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6373
- return new Promise((resolve7, reject) => {
6721
+ return new Promise((resolve8, reject) => {
6374
6722
  let body = "";
6375
6723
  let bytes = 0;
6376
6724
  req.on("data", (chunk) => {
@@ -6382,7 +6730,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6382
6730
  }
6383
6731
  body += String(chunk);
6384
6732
  });
6385
- req.on("end", () => resolve7(body));
6733
+ req.on("end", () => resolve8(body));
6386
6734
  req.on("error", reject);
6387
6735
  });
6388
6736
  }
@@ -6503,8 +6851,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
6503
6851
 
6504
6852
  // src/server/routes/plans.ts
6505
6853
  var import_zod5 = require("zod");
6506
- var fs9 = __toESM(require("fs/promises"));
6507
- var path9 = __toESM(require("path"));
6854
+ var fs10 = __toESM(require("fs/promises"));
6855
+ var path11 = __toESM(require("path"));
6508
6856
  var PlanWriteSchema = import_zod5.z.object({
6509
6857
  filename: import_zod5.z.string().min(1),
6510
6858
  content: import_zod5.z.string().min(1)
@@ -6524,7 +6872,7 @@ function handlePlansRoute(req, res, plansDir) {
6524
6872
  return;
6525
6873
  }
6526
6874
  const parsed = result.data;
6527
- const basename3 = path9.basename(parsed.filename);
6875
+ const basename3 = path11.basename(parsed.filename);
6528
6876
  if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
6529
6877
  res.writeHead(400, { "Content-Type": "application/json" });
6530
6878
  res.end(
@@ -6532,9 +6880,9 @@ function handlePlansRoute(req, res, plansDir) {
6532
6880
  );
6533
6881
  return;
6534
6882
  }
6535
- await fs9.mkdir(plansDir, { recursive: true });
6536
- const filePath = path9.join(plansDir, basename3);
6537
- await fs9.writeFile(filePath, parsed.content, "utf-8");
6883
+ await fs10.mkdir(plansDir, { recursive: true });
6884
+ const filePath = path11.join(plansDir, basename3);
6885
+ await fs10.writeFile(filePath, parsed.content, "utf-8");
6538
6886
  res.writeHead(201, { "Content-Type": "application/json" });
6539
6887
  res.end(JSON.stringify({ ok: true, filename: basename3 }));
6540
6888
  } catch {
@@ -6909,8 +7257,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
6909
7257
  }
6910
7258
 
6911
7259
  // src/server/routes/roadmap-actions.ts
6912
- var fs10 = __toESM(require("fs/promises"));
6913
- var path10 = __toESM(require("path"));
7260
+ var fs11 = __toESM(require("fs/promises"));
7261
+ var path12 = __toESM(require("path"));
6914
7262
  var import_core7 = require("@harness-engineering/core");
6915
7263
  var import_zod8 = require("zod");
6916
7264
  var AppendRoadmapRequestSchema = import_zod8.z.object({
@@ -6938,7 +7286,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6938
7286
  sendJSON2(res, 503, { error: "Roadmap path not configured" });
6939
7287
  return;
6940
7288
  }
6941
- const projectRoot = path10.dirname(path10.dirname(roadmapPath));
7289
+ const projectRoot = path12.dirname(path12.dirname(roadmapPath));
6942
7290
  const mode = (0, import_core7.loadProjectRoadmapMode)(projectRoot);
6943
7291
  if (mode === "file-less") {
6944
7292
  const trackerCfg = (0, import_core7.loadTrackerClientConfigFromProject)(projectRoot);
@@ -6991,7 +7339,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
6991
7339
  sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
6992
7340
  return;
6993
7341
  }
6994
- const content = await fs10.readFile(roadmapPath, "utf-8");
7342
+ const content = await fs11.readFile(roadmapPath, "utf-8");
6995
7343
  const roadmapResult = (0, import_core7.parseRoadmap)(content);
6996
7344
  if (!roadmapResult.ok) {
6997
7345
  sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
@@ -7022,8 +7370,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
7022
7370
  roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
7023
7371
  const tmpPath = roadmapPath + ".tmp";
7024
7372
  const serialized = (0, import_core7.serializeRoadmap)(roadmap);
7025
- await fs10.writeFile(tmpPath, serialized, "utf-8");
7026
- await fs10.rename(tmpPath, roadmapPath);
7373
+ await fs11.writeFile(tmpPath, serialized, "utf-8");
7374
+ await fs11.rename(tmpPath, roadmapPath);
7027
7375
  sendJSON2(res, 201, { ok: true, featureName: parsed.title });
7028
7376
  } catch (err) {
7029
7377
  const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
@@ -7516,7 +7864,7 @@ var import_core10 = require("@harness-engineering/core");
7516
7864
  var import_types24 = require("@harness-engineering/types");
7517
7865
 
7518
7866
  // src/proposals/gate.ts
7519
- var import_yaml2 = require("yaml");
7867
+ var import_yaml3 = require("yaml");
7520
7868
  var import_core8 = require("@harness-engineering/core");
7521
7869
  var GateRunError = class extends Error {
7522
7870
  constructor(message) {
@@ -7529,7 +7877,7 @@ function checkSkillYaml(yaml) {
7529
7877
  const findings = [];
7530
7878
  let doc;
7531
7879
  try {
7532
- doc = (0, import_yaml2.parse)(yaml);
7880
+ doc = (0, import_yaml3.parse)(yaml);
7533
7881
  } catch (err) {
7534
7882
  findings.push({
7535
7883
  severity: "error",
@@ -7652,9 +8000,9 @@ async function runGate(projectPath, proposalId) {
7652
8000
  }
7653
8001
 
7654
8002
  // src/proposals/promote.ts
7655
- var fs11 = __toESM(require("fs"));
7656
- var path11 = __toESM(require("path"));
7657
- var import_yaml3 = require("yaml");
8003
+ var fs12 = __toESM(require("fs"));
8004
+ var path13 = __toESM(require("path"));
8005
+ var import_yaml4 = require("yaml");
7658
8006
  var import_core9 = require("@harness-engineering/core");
7659
8007
  var GateNotReadyError = class extends Error {
7660
8008
  constructor(message) {
@@ -7670,11 +8018,11 @@ var PromotionError = class extends Error {
7670
8018
  };
7671
8019
  var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7672
8020
  function skillDir(projectPath, name) {
7673
- return path11.join(projectPath, "agents", "skills", "claude-code", name);
8021
+ return path13.join(projectPath, "agents", "skills", "claude-code", name);
7674
8022
  }
7675
8023
  function readIfExists(p) {
7676
8024
  try {
7677
- return fs11.readFileSync(p, "utf-8");
8025
+ return fs12.readFileSync(p, "utf-8");
7678
8026
  } catch {
7679
8027
  return null;
7680
8028
  }
@@ -7682,7 +8030,7 @@ function readIfExists(p) {
7682
8030
  function injectProvenanceIntoYaml(yamlText, proposalId) {
7683
8031
  let doc;
7684
8032
  try {
7685
- doc = (0, import_yaml3.parse)(yamlText);
8033
+ doc = (0, import_yaml4.parse)(yamlText);
7686
8034
  } catch (err) {
7687
8035
  throw new PromotionError(
7688
8036
  `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
@@ -7694,7 +8042,7 @@ function injectProvenanceIntoYaml(yamlText, proposalId) {
7694
8042
  const obj = doc;
7695
8043
  obj["provenance"] = "agent-proposed";
7696
8044
  obj["originatingProposalId"] = proposalId;
7697
- return (0, import_yaml3.stringify)(obj);
8045
+ return (0, import_yaml4.stringify)(obj);
7698
8046
  }
7699
8047
  function assertGateReady(proposal) {
7700
8048
  if (proposal.status !== "gate-running") {
@@ -7720,15 +8068,15 @@ function assertGateReady(proposal) {
7720
8068
  }
7721
8069
  async function promoteNewSkill(projectPath, proposal) {
7722
8070
  const target = skillDir(projectPath, proposal.content.name);
7723
- if (fs11.existsSync(target)) {
8071
+ if (fs12.existsSync(target)) {
7724
8072
  throw new PromotionError(
7725
8073
  `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7726
8074
  );
7727
8075
  }
7728
- fs11.mkdirSync(target, { recursive: true });
8076
+ fs12.mkdirSync(target, { recursive: true });
7729
8077
  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 ?? "");
8078
+ fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
8079
+ fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7732
8080
  return { skillPath: target };
7733
8081
  }
7734
8082
  async function promoteRefinement(projectPath, proposal) {
@@ -7736,12 +8084,12 @@ async function promoteRefinement(projectPath, proposal) {
7736
8084
  throw new PromotionError("refinement proposal is missing targetSkill");
7737
8085
  }
7738
8086
  const target = skillDir(projectPath, proposal.targetSkill);
7739
- if (!fs11.existsSync(target)) {
8087
+ if (!fs12.existsSync(target)) {
7740
8088
  throw new PromotionError(
7741
8089
  `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7742
8090
  );
7743
8091
  }
7744
- const yamlPath = path11.join(target, "skill.yaml");
8092
+ const yamlPath = path13.join(target, "skill.yaml");
7745
8093
  const before = readIfExists(yamlPath) ?? "";
7746
8094
  const after = injectProvenanceIntoYaml(before, proposal.id);
7747
8095
  if (after === before) {
@@ -7749,7 +8097,7 @@ async function promoteRefinement(projectPath, proposal) {
7749
8097
  "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7750
8098
  );
7751
8099
  }
7752
- fs11.writeFileSync(yamlPath, after);
8100
+ fs12.writeFileSync(yamlPath, after);
7753
8101
  return { skillPath: target };
7754
8102
  }
7755
8103
  async function promote(projectPath, proposalId, decidedBy) {
@@ -8036,35 +8384,185 @@ function handleV1ProposalsRoute(req, res, deps) {
8036
8384
  return false;
8037
8385
  }
8038
8386
 
8039
- // src/server/routes/sessions.ts
8040
- var fs12 = __toESM(require("fs/promises"));
8041
- var path12 = __toESM(require("path"));
8387
+ // src/server/routes/v1/routing.ts
8042
8388
  var import_zod14 = require("zod");
8043
- var SessionCreateSchema = import_zod14.z.object({
8044
- sessionId: import_zod14.z.string().min(1)
8389
+ var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
8390
+ var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
8391
+ var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
8392
+ function sendJSON9(res, status, body) {
8393
+ res.writeHead(status, { "Content-Type": "application/json" });
8394
+ res.end(JSON.stringify(body));
8395
+ }
8396
+ function unavailable(res) {
8397
+ sendJSON9(res, 503, { error: "BackendRouter not available" });
8398
+ return true;
8399
+ }
8400
+ function resolveChain(value, backends) {
8401
+ return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
8402
+ }
8403
+ function buildResolvedChains(routing, backends) {
8404
+ const out = {};
8405
+ out["default"] = resolveChain(routing.default, backends);
8406
+ for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
8407
+ const v = routing[tier];
8408
+ if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
8409
+ }
8410
+ if (routing.intelligence) {
8411
+ for (const [layer, v] of Object.entries(routing.intelligence)) {
8412
+ if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
8413
+ }
8414
+ }
8415
+ if (routing.isolation) {
8416
+ for (const [tier, v] of Object.entries(routing.isolation)) {
8417
+ if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
8418
+ }
8419
+ }
8420
+ if (routing.skills) {
8421
+ for (const [name, v] of Object.entries(routing.skills)) {
8422
+ if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
8423
+ }
8424
+ }
8425
+ if (routing.modes) {
8426
+ for (const [mode, v] of Object.entries(routing.modes)) {
8427
+ if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
8428
+ }
8429
+ }
8430
+ return out;
8431
+ }
8432
+ function handleConfig(res, deps) {
8433
+ if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
8434
+ sendJSON9(res, 200, {
8435
+ routing: deps.routing,
8436
+ resolvedChains: buildResolvedChains(deps.routing, deps.backends),
8437
+ backends: Object.keys(deps.backends)
8438
+ });
8439
+ return true;
8440
+ }
8441
+ function parseDecisionsQuery(url) {
8442
+ const qIdx = url.indexOf("?");
8443
+ if (qIdx === -1) return {};
8444
+ const p = new URLSearchParams(url.slice(qIdx + 1));
8445
+ const filter = {};
8446
+ const skill = p.get("skill");
8447
+ const mode = p.get("mode");
8448
+ const backend = p.get("backend");
8449
+ const limit = p.get("limit");
8450
+ if (skill) filter.skillName = skill;
8451
+ if (mode) filter.mode = mode;
8452
+ if (backend) filter.backendName = backend;
8453
+ if (limit) {
8454
+ const n = Number(limit);
8455
+ if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
8456
+ }
8457
+ return filter;
8458
+ }
8459
+ function handleDecisions(req, res, deps) {
8460
+ if (!deps.bus) return unavailable(res);
8461
+ const filter = parseDecisionsQuery(req.url ?? "");
8462
+ sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
8463
+ return true;
8464
+ }
8465
+ var UseCaseSchema = import_zod14.z.discriminatedUnion("kind", [
8466
+ import_zod14.z.object({
8467
+ kind: import_zod14.z.literal("tier"),
8468
+ tier: import_zod14.z.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
8469
+ }),
8470
+ import_zod14.z.object({ kind: import_zod14.z.literal("intelligence"), layer: import_zod14.z.enum(["sel", "pesl"]) }),
8471
+ import_zod14.z.object({ kind: import_zod14.z.literal("isolation"), tier: import_zod14.z.string() }),
8472
+ import_zod14.z.object({ kind: import_zod14.z.literal("maintenance") }),
8473
+ import_zod14.z.object({ kind: import_zod14.z.literal("chat") }),
8474
+ import_zod14.z.object({
8475
+ kind: import_zod14.z.literal("skill"),
8476
+ skillName: import_zod14.z.string().min(1),
8477
+ cognitiveMode: import_zod14.z.string().optional()
8478
+ }),
8479
+ import_zod14.z.object({ kind: import_zod14.z.literal("mode"), cognitiveMode: import_zod14.z.string().min(1) })
8480
+ ]);
8481
+ var TraceBodySchema = import_zod14.z.object({
8482
+ useCase: UseCaseSchema,
8483
+ invocationOverride: import_zod14.z.string().min(1).optional()
8484
+ });
8485
+ async function handleTrace(req, res, deps) {
8486
+ if (!deps.routing || !deps.backends) {
8487
+ unavailable(res);
8488
+ return true;
8489
+ }
8490
+ let raw;
8491
+ try {
8492
+ raw = await readBody(req);
8493
+ } catch {
8494
+ sendJSON9(res, 400, { error: "body read failed" });
8495
+ return true;
8496
+ }
8497
+ let parsed;
8498
+ try {
8499
+ parsed = JSON.parse(raw);
8500
+ } catch {
8501
+ sendJSON9(res, 400, { error: "invalid JSON body" });
8502
+ return true;
8503
+ }
8504
+ const r = TraceBodySchema.safeParse(parsed);
8505
+ if (!r.success) {
8506
+ sendJSON9(res, 400, { error: r.error.message });
8507
+ return true;
8508
+ }
8509
+ const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
8510
+ try {
8511
+ const dryRunRouter = new BackendRouter({
8512
+ backends: deps.backends,
8513
+ routing: deps.routing
8514
+ });
8515
+ const { decision, def } = dryRunRouter.resolveDecisionAndDef(
8516
+ r.data.useCase,
8517
+ opts
8518
+ );
8519
+ sendJSON9(res, 200, { decision, def: { type: def.type } });
8520
+ } catch (err) {
8521
+ sendJSON9(res, 500, { error: String(err) });
8522
+ }
8523
+ return true;
8524
+ }
8525
+ function handleV1RoutingRoute(req, res, deps) {
8526
+ const url = req.url ?? "";
8527
+ const method = req.method ?? "GET";
8528
+ if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
8529
+ if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
8530
+ if (method === "POST" && TRACE_RE.test(url)) {
8531
+ void handleTrace(req, res, deps);
8532
+ return true;
8533
+ }
8534
+ return false;
8535
+ }
8536
+
8537
+ // src/server/routes/sessions.ts
8538
+ var fs13 = __toESM(require("fs/promises"));
8539
+ var path14 = __toESM(require("path"));
8540
+ var import_zod15 = require("zod");
8541
+ var SessionCreateSchema = import_zod15.z.object({
8542
+ sessionId: import_zod15.z.string().min(1)
8045
8543
  }).passthrough();
8046
8544
  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
8545
  function isSafeId(id) {
8048
- return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
8546
+ return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
8049
8547
  }
8050
8548
  function jsonResponse(res, status, data) {
8051
8549
  res.writeHead(status, { "Content-Type": "application/json" });
8052
8550
  res.end(JSON.stringify(data));
8053
8551
  }
8054
8552
  function extractSessionId(url) {
8055
- const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
8553
+ const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
8056
8554
  const id = segments.pop();
8057
8555
  return id && id !== "sessions" ? id : null;
8058
8556
  }
8059
8557
  async function handleList2(res, sessionsDir) {
8060
8558
  try {
8061
- const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
8559
+ const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
8062
8560
  const sessions = [];
8063
8561
  for (const entry of entries) {
8064
8562
  if (!entry.isDirectory()) continue;
8065
8563
  try {
8066
- const content = await fs12.readFile(
8067
- path12.join(sessionsDir, entry.name, "session.json"),
8564
+ const content = await fs13.readFile(
8565
+ path14.join(sessionsDir, entry.name, "session.json"),
8068
8566
  "utf-8"
8069
8567
  );
8070
8568
  sessions.push(JSON.parse(content));
@@ -8089,7 +8587,7 @@ async function handleGet2(res, id, sessionsDir) {
8089
8587
  return;
8090
8588
  }
8091
8589
  try {
8092
- const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
8590
+ const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
8093
8591
  jsonResponse(res, 200, JSON.parse(content));
8094
8592
  } catch (err) {
8095
8593
  if (err.code === "ENOENT") {
@@ -8112,9 +8610,9 @@ async function handleCreate(req, res, sessionsDir) {
8112
8610
  jsonResponse(res, 400, { error: "Invalid sessionId" });
8113
8611
  return;
8114
8612
  }
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));
8613
+ const sessionDir = path14.join(sessionsDir, session.sessionId);
8614
+ await fs13.mkdir(sessionDir, { recursive: true });
8615
+ await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8118
8616
  jsonResponse(res, 200, { ok: true });
8119
8617
  } catch {
8120
8618
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -8128,10 +8626,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
8128
8626
  return;
8129
8627
  }
8130
8628
  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));
8629
+ const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
8630
+ const sessionFilePath = path14.join(sessionsDir, id, "session.json");
8631
+ const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
8632
+ await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8135
8633
  jsonResponse(res, 200, { ok: true });
8136
8634
  } catch {
8137
8635
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -8144,7 +8642,7 @@ async function handleDelete(res, url, sessionsDir) {
8144
8642
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
8145
8643
  return;
8146
8644
  }
8147
- await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
8645
+ await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
8148
8646
  jsonResponse(res, 200, { ok: true });
8149
8647
  } catch {
8150
8648
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -8248,16 +8746,16 @@ function handleStreamsRoute(req, res, recorder) {
8248
8746
  }
8249
8747
 
8250
8748
  // src/server/routes/auth.ts
8251
- var import_zod15 = require("zod");
8749
+ var import_zod16 = require("zod");
8252
8750
  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),
8751
+ var CreateBodySchema = import_zod16.z.object({
8752
+ name: import_zod16.z.string().min(1).max(100),
8753
+ scopes: import_zod16.z.array(import_types25.TokenScopeSchema).min(1),
8256
8754
  bridgeKind: import_types25.BridgeKindSchema.optional(),
8257
- tenantId: import_zod15.z.string().optional(),
8258
- expiresAt: import_zod15.z.string().datetime().optional()
8755
+ tenantId: import_zod16.z.string().optional(),
8756
+ expiresAt: import_zod16.z.string().datetime().optional()
8259
8757
  });
8260
- function sendJSON9(res, status, body) {
8758
+ function sendJSON10(res, status, body) {
8261
8759
  res.writeHead(status, { "Content-Type": "application/json" });
8262
8760
  res.end(JSON.stringify(body));
8263
8761
  }
@@ -8267,19 +8765,19 @@ async function handlePost(req, res, store) {
8267
8765
  raw = await readBody(req);
8268
8766
  } catch (err) {
8269
8767
  const msg = err instanceof Error ? err.message : "Failed to read body";
8270
- sendJSON9(res, 413, { error: msg });
8768
+ sendJSON10(res, 413, { error: msg });
8271
8769
  return;
8272
8770
  }
8273
8771
  let json;
8274
8772
  try {
8275
8773
  json = JSON.parse(raw);
8276
8774
  } catch {
8277
- sendJSON9(res, 400, { error: "Invalid JSON body" });
8775
+ sendJSON10(res, 400, { error: "Invalid JSON body" });
8278
8776
  return;
8279
8777
  }
8280
8778
  const parsed = CreateBodySchema.safeParse(json);
8281
8779
  if (!parsed.success) {
8282
- sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8780
+ sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8283
8781
  return;
8284
8782
  }
8285
8783
  try {
@@ -8292,37 +8790,37 @@ async function handlePost(req, res, store) {
8292
8790
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
8293
8791
  const result = await store.create(input);
8294
8792
  const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
8295
- sendJSON9(res, 200, {
8793
+ sendJSON10(res, 200, {
8296
8794
  ...publicRecord,
8297
8795
  token: result.token
8298
8796
  });
8299
8797
  } catch (err) {
8300
8798
  const msg = err instanceof Error ? err.message : "Failed to create token";
8301
8799
  if (msg.includes("already exists")) {
8302
- sendJSON9(res, 409, { error: msg });
8800
+ sendJSON10(res, 409, { error: msg });
8303
8801
  return;
8304
8802
  }
8305
- sendJSON9(res, 500, { error: "Internal error creating token" });
8803
+ sendJSON10(res, 500, { error: "Internal error creating token" });
8306
8804
  }
8307
8805
  }
8308
8806
  async function handleList3(res, store) {
8309
8807
  try {
8310
8808
  const list = await store.list();
8311
- sendJSON9(res, 200, list);
8809
+ sendJSON10(res, 200, list);
8312
8810
  } catch {
8313
- sendJSON9(res, 500, { error: "Internal error listing tokens" });
8811
+ sendJSON10(res, 500, { error: "Internal error listing tokens" });
8314
8812
  }
8315
8813
  }
8316
8814
  async function handleDelete2(res, store, id) {
8317
8815
  try {
8318
8816
  const ok = await store.revoke(id);
8319
8817
  if (!ok) {
8320
- sendJSON9(res, 404, { error: "Token not found" });
8818
+ sendJSON10(res, 404, { error: "Token not found" });
8321
8819
  return;
8322
8820
  }
8323
- sendJSON9(res, 200, { deleted: true });
8821
+ sendJSON10(res, 200, { deleted: true });
8324
8822
  } catch {
8325
- sendJSON9(res, 500, { error: "Internal error revoking token" });
8823
+ sendJSON10(res, 500, { error: "Internal error revoking token" });
8326
8824
  }
8327
8825
  }
8328
8826
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -8347,12 +8845,12 @@ function handleAuthRoute(req, res, store) {
8347
8845
  return true;
8348
8846
  }
8349
8847
  }
8350
- sendJSON9(res, 405, { error: "Method not allowed" });
8848
+ sendJSON10(res, 405, { error: "Method not allowed" });
8351
8849
  return true;
8352
8850
  }
8353
8851
 
8354
8852
  // src/server/routes/local-model.ts
8355
- function sendJSON10(res, status, body) {
8853
+ function sendJSON11(res, status, body) {
8356
8854
  res.writeHead(status, { "Content-Type": "application/json" });
8357
8855
  res.end(JSON.stringify(body));
8358
8856
  }
@@ -8360,36 +8858,36 @@ function handleLocalModelRoute(req, res, getStatus) {
8360
8858
  const { method, url } = req;
8361
8859
  if (url !== "/api/v1/local-model/status") return false;
8362
8860
  if (method !== "GET") {
8363
- sendJSON10(res, 405, { error: "Method not allowed" });
8861
+ sendJSON11(res, 405, { error: "Method not allowed" });
8364
8862
  return true;
8365
8863
  }
8366
8864
  if (!getStatus) {
8367
- sendJSON10(res, 503, { error: "Local backend not configured" });
8865
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8368
8866
  return true;
8369
8867
  }
8370
8868
  const status = getStatus();
8371
8869
  if (!status) {
8372
- sendJSON10(res, 503, { error: "Local backend not configured" });
8870
+ sendJSON11(res, 503, { error: "Local backend not configured" });
8373
8871
  return true;
8374
8872
  }
8375
- sendJSON10(res, 200, status);
8873
+ sendJSON11(res, 200, status);
8376
8874
  return true;
8377
8875
  }
8378
8876
  function handleLocalModelsRoute(req, res, getStatuses) {
8379
8877
  const { method, url } = req;
8380
8878
  if (url !== "/api/v1/local-models/status") return false;
8381
8879
  if (method !== "GET") {
8382
- sendJSON10(res, 405, { error: "Method not allowed" });
8880
+ sendJSON11(res, 405, { error: "Method not allowed" });
8383
8881
  return true;
8384
8882
  }
8385
8883
  const statuses = getStatuses ? getStatuses() : [];
8386
- sendJSON10(res, 200, statuses);
8884
+ sendJSON11(res, 200, statuses);
8387
8885
  return true;
8388
8886
  }
8389
8887
 
8390
8888
  // src/server/static.ts
8391
- var fs13 = __toESM(require("fs"));
8392
- var path13 = __toESM(require("path"));
8889
+ var fs14 = __toESM(require("fs"));
8890
+ var path15 = __toESM(require("path"));
8393
8891
  var MIME_TYPES = {
8394
8892
  ".html": "text/html; charset=utf-8",
8395
8893
  ".js": "application/javascript; charset=utf-8",
@@ -8409,29 +8907,29 @@ var MIME_TYPES = {
8409
8907
  function handleStaticFile(req, res, dashboardDir) {
8410
8908
  const { method, url } = req;
8411
8909
  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");
8910
+ const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
8911
+ const wsPath = path15.posix.join(path15.posix.sep, "ws");
8414
8912
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
8415
8913
  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);
8914
+ const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8915
+ const resolved = path15.resolve(requestedPath);
8916
+ if (!resolved.startsWith(path15.resolve(dashboardDir))) {
8917
+ return serveFile(path15.join(dashboardDir, "index.html"), res);
8420
8918
  }
8421
- if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
8919
+ if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
8422
8920
  return serveFile(resolved, res);
8423
8921
  }
8424
- const indexPath = path13.join(dashboardDir, "index.html");
8425
- if (fs13.existsSync(indexPath)) {
8922
+ const indexPath = path15.join(dashboardDir, "index.html");
8923
+ if (fs14.existsSync(indexPath)) {
8426
8924
  return serveFile(indexPath, res);
8427
8925
  }
8428
8926
  return false;
8429
8927
  }
8430
8928
  function serveFile(filePath, res) {
8431
- const ext = path13.extname(filePath).toLowerCase();
8929
+ const ext = path15.extname(filePath).toLowerCase();
8432
8930
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
8433
8931
  try {
8434
- const content = fs13.readFileSync(filePath);
8932
+ const content = fs14.readFileSync(filePath);
8435
8933
  res.writeHead(200, { "Content-Type": contentType });
8436
8934
  res.end(content);
8437
8935
  return true;
@@ -8441,8 +8939,8 @@ function serveFile(filePath, res) {
8441
8939
  }
8442
8940
 
8443
8941
  // src/server/plan-watcher.ts
8444
- var fs14 = __toESM(require("fs"));
8445
- var path14 = __toESM(require("path"));
8942
+ var fs15 = __toESM(require("fs"));
8943
+ var path16 = __toESM(require("path"));
8446
8944
  var PlanWatcher = class {
8447
8945
  plansDir;
8448
8946
  queue;
@@ -8456,11 +8954,11 @@ var PlanWatcher = class {
8456
8954
  * Creates the directory if it does not exist.
8457
8955
  */
8458
8956
  start() {
8459
- fs14.mkdirSync(this.plansDir, { recursive: true });
8460
- this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
8957
+ fs15.mkdirSync(this.plansDir, { recursive: true });
8958
+ this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
8461
8959
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
8462
- const filePath = path14.join(this.plansDir, filename);
8463
- if (fs14.existsSync(filePath)) {
8960
+ const filePath = path16.join(this.plansDir, filename);
8961
+ if (fs15.existsSync(filePath)) {
8464
8962
  void this.handleNewPlan(filename);
8465
8963
  }
8466
8964
  }
@@ -8510,8 +9008,8 @@ function parseToken(raw) {
8510
9008
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
8511
9009
  }
8512
9010
  var TokenStore = class {
8513
- constructor(path22) {
8514
- this.path = path22;
9011
+ constructor(path24) {
9012
+ this.path = path24;
8515
9013
  }
8516
9014
  path;
8517
9015
  cache = null;
@@ -8618,8 +9116,8 @@ var import_promises2 = require("fs/promises");
8618
9116
  var import_node_path2 = require("path");
8619
9117
  var import_types27 = require("@harness-engineering/types");
8620
9118
  var AuditLogger = class {
8621
- constructor(path22, opts = {}) {
8622
- this.path = path22;
9119
+ constructor(path24, opts = {}) {
9120
+ this.path = path24;
8623
9121
  this.opts = opts;
8624
9122
  }
8625
9123
  path;
@@ -8746,14 +9244,36 @@ var V1_BRIDGE_ROUTES = [
8746
9244
  pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
8747
9245
  scope: "read-telemetry",
8748
9246
  description: "Prompt-cache hit/miss snapshot (rolling window)."
9247
+ },
9248
+ // ── Spec B Phase 5 routing observability ──
9249
+ // D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
9250
+ // precedent (read-only observability). A dedicated `read-routing`
9251
+ // scope was rejected to avoid a TokenScopeSchema + ADR cascade.
9252
+ {
9253
+ method: "GET",
9254
+ pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
9255
+ scope: "read-telemetry",
9256
+ description: "Current routing config + resolved fallback chains + known backends."
9257
+ },
9258
+ {
9259
+ method: "GET",
9260
+ pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
9261
+ scope: "read-telemetry",
9262
+ description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
9263
+ },
9264
+ {
9265
+ method: "POST",
9266
+ pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
9267
+ scope: "read-telemetry",
9268
+ description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
8749
9269
  }
8750
9270
  ];
8751
9271
  function isV1Bridge(method, url) {
8752
9272
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
8753
9273
  }
8754
- function requiredBridgeScope(method, path22) {
9274
+ function requiredBridgeScope(method, path24) {
8755
9275
  for (const r of V1_BRIDGE_ROUTES) {
8756
- if (r.method === method && r.pattern.test(path22)) return r.scope;
9276
+ if (r.method === method && r.pattern.test(path24)) return r.scope;
8757
9277
  }
8758
9278
  return null;
8759
9279
  }
@@ -8763,24 +9283,24 @@ function hasScope(held, required) {
8763
9283
  if (held.includes("admin")) return true;
8764
9284
  return held.includes(required);
8765
9285
  }
8766
- function requiredScopeForRoute(method, path22) {
8767
- const bridgeScope = requiredBridgeScope(method, path22);
9286
+ function requiredScopeForRoute(method, path24) {
9287
+ const bridgeScope = requiredBridgeScope(method, path24);
8768
9288
  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"))
9289
+ if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
9290
+ if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
9291
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
9292
+ if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
9293
+ if (path24.startsWith("/api/interactions")) return "resolve-interaction";
9294
+ if (path24.startsWith("/api/plans")) return "read-status";
9295
+ if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
9296
+ if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
9297
+ if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
9298
+ if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
8779
9299
  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";
9300
+ if (path24.startsWith("/api/maintenance")) return "trigger-job";
9301
+ if (path24.startsWith("/api/streams")) return "read-status";
9302
+ if (path24.startsWith("/api/sessions")) return "read-status";
9303
+ if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
8784
9304
  return null;
8785
9305
  }
8786
9306
 
@@ -8844,6 +9364,15 @@ var OrchestratorServer = class {
8844
9364
  getLocalModelStatuses = null;
8845
9365
  webhooks;
8846
9366
  cacheMetrics;
9367
+ // Spec B Phase 5 — routing observability accessor closures + the WS
9368
+ // broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
9369
+ // calls it explicitly; clearListeners in Orchestrator.stop() is the
9370
+ // belt-and-suspenders second line).
9371
+ getBackendRouterFn = null;
9372
+ getRoutingDecisionBusFn = null;
9373
+ getRoutingConfigFn = null;
9374
+ getBackendsFn = null;
9375
+ routingDecisionUnsubscribe = null;
8847
9376
  recorder = null;
8848
9377
  planWatcher = null;
8849
9378
  tokenStore;
@@ -8856,8 +9385,8 @@ var OrchestratorServer = class {
8856
9385
  this.orchestrator = orchestrator;
8857
9386
  this.port = port;
8858
9387
  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");
9388
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
9389
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
8861
9390
  this.tokenStore = new TokenStore(tokensPath);
8862
9391
  this.auditLogger = new AuditLogger(auditPath);
8863
9392
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -8870,20 +9399,24 @@ var OrchestratorServer = class {
8870
9399
  }
8871
9400
  initDependencies(deps) {
8872
9401
  this.interactionQueue = deps?.interactionQueue;
8873
- this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8874
- this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
9402
+ this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
9403
+ this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
8875
9404
  this.claudeCommand = deps?.claudeCommand ?? "claude";
8876
9405
  this.pipeline = deps?.pipeline ?? null;
8877
9406
  this.analysisArchive = deps?.analysisArchive;
8878
9407
  this.roadmapPath = deps?.roadmapPath ?? null;
8879
9408
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
8880
- this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
9409
+ this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
8881
9410
  this.projectPath = deps?.projectPath ?? process.cwd();
8882
9411
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
8883
9412
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
8884
9413
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
8885
9414
  this.webhooks = deps?.webhooks;
8886
9415
  this.cacheMetrics = deps?.cacheMetrics;
9416
+ this.getBackendRouterFn = deps?.getBackendRouter ?? null;
9417
+ this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
9418
+ this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
9419
+ this.getBackendsFn = deps?.getBackends ?? null;
8887
9420
  }
8888
9421
  wireEvents() {
8889
9422
  this.stateChangeListener = (snapshot) => {
@@ -8894,6 +9427,12 @@ var OrchestratorServer = class {
8894
9427
  };
8895
9428
  this.orchestrator.on("state_change", this.stateChangeListener);
8896
9429
  this.orchestrator.on("agent_event", this.agentEventListener);
9430
+ const bus = this.getRoutingDecisionBusFn?.() ?? null;
9431
+ if (bus) {
9432
+ this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
9433
+ this.broadcaster.broadcast("routing:decision", decision);
9434
+ });
9435
+ }
8897
9436
  }
8898
9437
  /**
8899
9438
  * Broadcast a new interaction to all WebSocket clients.
@@ -9049,6 +9588,14 @@ var OrchestratorServer = class {
9049
9588
  (req, res) => handleV1TelemetryRoute(req, res, {
9050
9589
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
9051
9590
  }),
9591
+ // Spec B Phase 5 — routing observability. Returns 503 when the
9592
+ // backendFactory is null (legacy single-backend configs).
9593
+ (req, res) => handleV1RoutingRoute(req, res, {
9594
+ router: this.getBackendRouterFn?.() ?? null,
9595
+ bus: this.getRoutingDecisionBusFn?.() ?? null,
9596
+ routing: this.getRoutingConfigFn?.() ?? null,
9597
+ backends: this.getBackendsFn?.() ?? null
9598
+ }),
9052
9599
  // Hermes Phase 4 — skill proposal review queue. Read scopes
9053
9600
  // (`read-status`) and write scopes (`manage-proposals`) are enforced
9054
9601
  // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
@@ -9145,17 +9692,21 @@ var OrchestratorServer = class {
9145
9692
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
9146
9693
  this.planWatcher.start();
9147
9694
  }
9148
- return new Promise((resolve7) => {
9695
+ return new Promise((resolve8) => {
9149
9696
  const host = getBindHost();
9150
9697
  this.httpServer.listen(this.port, host, () => {
9151
9698
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
9152
- resolve7();
9699
+ resolve8();
9153
9700
  });
9154
9701
  });
9155
9702
  }
9156
9703
  stop() {
9157
9704
  this.orchestrator.removeListener("state_change", this.stateChangeListener);
9158
9705
  this.orchestrator.removeListener("agent_event", this.agentEventListener);
9706
+ if (this.routingDecisionUnsubscribe) {
9707
+ this.routingDecisionUnsubscribe();
9708
+ this.routingDecisionUnsubscribe = null;
9709
+ }
9159
9710
  if (this.planWatcher) {
9160
9711
  this.planWatcher.stop();
9161
9712
  this.planWatcher = null;
@@ -9199,8 +9750,8 @@ function genSecret2() {
9199
9750
  return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
9200
9751
  }
9201
9752
  var WebhookStore = class {
9202
- constructor(path22) {
9203
- this.path = path22;
9753
+ constructor(path24) {
9754
+ this.path = path24;
9204
9755
  }
9205
9756
  path;
9206
9757
  cache = null;
@@ -10745,19 +11296,19 @@ var SingleProcessLeaderElector = class {
10745
11296
  };
10746
11297
 
10747
11298
  // 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()
11299
+ var fs16 = __toESM(require("fs"));
11300
+ var path18 = __toESM(require("path"));
11301
+ var import_zod17 = require("zod");
11302
+ var RunResultSchema = import_zod17.z.object({
11303
+ taskId: import_zod17.z.string(),
11304
+ startedAt: import_zod17.z.string(),
11305
+ completedAt: import_zod17.z.string(),
11306
+ status: import_zod17.z.enum(["success", "failure", "skipped", "no-issues"]),
11307
+ findings: import_zod17.z.number(),
11308
+ fixed: import_zod17.z.number(),
11309
+ prUrl: import_zod17.z.string().nullable(),
11310
+ prUpdated: import_zod17.z.boolean(),
11311
+ error: import_zod17.z.string().optional()
10761
11312
  });
10762
11313
  var MAX_HISTORY = 500;
10763
11314
  var fallbackLogger = {
@@ -10781,10 +11332,10 @@ var MaintenanceReporter = class {
10781
11332
  */
10782
11333
  async load() {
10783
11334
  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));
11335
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11336
+ const filePath = path18.join(this.persistDir, "history.json");
11337
+ const data = await fs16.promises.readFile(filePath, "utf-8");
11338
+ const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
10788
11339
  if (parsed.success) {
10789
11340
  this.history = parsed.data.slice(0, MAX_HISTORY);
10790
11341
  }
@@ -10817,9 +11368,9 @@ var MaintenanceReporter = class {
10817
11368
  */
10818
11369
  async persist() {
10819
11370
  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");
11371
+ await fs16.promises.mkdir(this.persistDir, { recursive: true });
11372
+ const filePath = path18.join(this.persistDir, "history.json");
11373
+ await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10823
11374
  } catch (err) {
10824
11375
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
10825
11376
  }
@@ -11306,7 +11857,7 @@ function parseStatusLine(output) {
11306
11857
  // src/maintenance/check-script-runner.ts
11307
11858
  var import_node_child_process11 = require("child_process");
11308
11859
  var import_node_util3 = require("util");
11309
- var path17 = __toESM(require("path"));
11860
+ var path19 = __toESM(require("path"));
11310
11861
  var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process11.execFile);
11311
11862
  var CheckScriptRunner = class {
11312
11863
  constructor(cwd) {
@@ -11325,7 +11876,7 @@ var CheckScriptRunner = class {
11325
11876
  }
11326
11877
  };
11327
11878
  async function captureScript(spec, projectRoot) {
11328
- const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11879
+ const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
11329
11880
  const args = spec.args ?? [];
11330
11881
  const timeoutMs = spec.timeoutMs ?? 12e4;
11331
11882
  try {
@@ -11415,8 +11966,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
11415
11966
  }
11416
11967
 
11417
11968
  // src/maintenance/output-store.ts
11418
- var fs16 = __toESM(require("fs"));
11419
- var path18 = __toESM(require("path"));
11969
+ var fs17 = __toESM(require("fs"));
11970
+ var path20 = __toESM(require("path"));
11420
11971
  var DEFAULT_RETENTION = {
11421
11972
  runs: 50,
11422
11973
  maxAgeDays: 30
@@ -11456,13 +12007,13 @@ var TaskOutputStore = class {
11456
12007
  async write(taskId, entry, retention) {
11457
12008
  this.ensureSafeTaskId(taskId);
11458
12009
  const dir = this.dirFor(taskId);
11459
- await fs16.promises.mkdir(dir, { recursive: true });
12010
+ await fs17.promises.mkdir(dir, { recursive: true });
11460
12011
  const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11461
- const filePath = path18.join(dir, fileName);
12012
+ const filePath = path20.join(dir, fileName);
11462
12013
  const tmpPath = `${filePath}.tmp`;
11463
12014
  const payload = JSON.stringify(entry, null, 2);
11464
- await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11465
- await fs16.promises.rename(tmpPath, filePath);
12015
+ await fs17.promises.writeFile(tmpPath, payload, "utf-8");
12016
+ await fs17.promises.rename(tmpPath, filePath);
11466
12017
  try {
11467
12018
  await this.applyRetention(taskId, retention);
11468
12019
  } catch (err) {
@@ -11486,7 +12037,7 @@ var TaskOutputStore = class {
11486
12037
  const slice = fileNames.slice(offset, offset + limit);
11487
12038
  const out = [];
11488
12039
  for (const name of slice) {
11489
- const entry = await this.readEntry(path18.join(dir, name));
12040
+ const entry = await this.readEntry(path20.join(dir, name));
11490
12041
  if (entry) out.push(entry);
11491
12042
  }
11492
12043
  return out;
@@ -11502,18 +12053,18 @@ var TaskOutputStore = class {
11502
12053
  }
11503
12054
  const dir = this.dirFor(taskId);
11504
12055
  const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11505
- return this.readEntry(path18.join(dir, fileName));
12056
+ return this.readEntry(path20.join(dir, fileName));
11506
12057
  }
11507
12058
  /**
11508
12059
  * The on-disk root for a given task. Exposed for tooling that needs to walk
11509
12060
  * outputs from outside the store API.
11510
12061
  */
11511
12062
  dirFor(taskId) {
11512
- return path18.join(this.rootDir, taskId, "outputs");
12063
+ return path20.join(this.rootDir, taskId, "outputs");
11513
12064
  }
11514
12065
  async readEntry(filePath) {
11515
12066
  try {
11516
- const buf = await fs16.promises.readFile(filePath, "utf-8");
12067
+ const buf = await fs17.promises.readFile(filePath, "utf-8");
11517
12068
  const parsed = JSON.parse(buf);
11518
12069
  return parsed;
11519
12070
  } catch {
@@ -11535,7 +12086,7 @@ var TaskOutputStore = class {
11535
12086
  const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11536
12087
  for (const name of toRemove) {
11537
12088
  try {
11538
- await fs16.promises.unlink(path18.join(dir, name));
12089
+ await fs17.promises.unlink(path20.join(dir, name));
11539
12090
  } catch {
11540
12091
  }
11541
12092
  }
@@ -11544,7 +12095,7 @@ var TaskOutputStore = class {
11544
12095
  async function listJsonFilesDescending(dir) {
11545
12096
  let names;
11546
12097
  try {
11547
- names = await fs16.promises.readdir(dir);
12098
+ names = await fs17.promises.readdir(dir);
11548
12099
  } catch {
11549
12100
  return [];
11550
12101
  }
@@ -11718,8 +12269,8 @@ function validateCheckShape(prefix, task, errors) {
11718
12269
  });
11719
12270
  }
11720
12271
  if (hasScript) {
11721
- const path22 = task.checkScript?.path;
11722
- if (!path22 || path22.trim().length === 0) {
12272
+ const path24 = task.checkScript?.path;
12273
+ if (!path24 || path24.trim().length === 0) {
11723
12274
  errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
11724
12275
  }
11725
12276
  if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
@@ -11845,9 +12396,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
11845
12396
  stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11846
12397
  }
11847
12398
  }
11848
- function reportCycle(path22, next, errors, reported) {
11849
- const cycleStart = path22.indexOf(next);
11850
- const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
12399
+ function reportCycle(path24, next, errors, reported) {
12400
+ const cycleStart = path24.indexOf(next);
12401
+ const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
11851
12402
  const key = cyclePath.join("\u2192");
11852
12403
  if (reported.has(key)) return;
11853
12404
  reported.add(key);
@@ -11858,11 +12409,6 @@ function reportCycle(path22, next, errors, reported) {
11858
12409
  }
11859
12410
 
11860
12411
  // 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
12412
  var Orchestrator = class extends import_node_events.EventEmitter {
11867
12413
  state;
11868
12414
  config;
@@ -11887,6 +12433,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11887
12433
  * construction time. Eliminating this fallback is autopilot Phase 4+.
11888
12434
  */
11889
12435
  backendFactory;
12436
+ /**
12437
+ * Spec B Phase 4 (D8): per-orchestrator in-process bus for
12438
+ * `RoutingDecision` events. Constructed alongside backendFactory when
12439
+ * agent.backends synthesis succeeds; null when legacy single-backend
12440
+ * config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
12441
+ * subscribe via `getRoutingDecisionBus()`.
12442
+ */
12443
+ routingDecisionBus;
11890
12444
  /**
11891
12445
  * Test-only: when overrides.backend is provided, dispatch uses this
11892
12446
  * instance directly (bypassing the factory). Mirrors Phase 1
@@ -11909,6 +12463,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11909
12463
  * so this map is the single source of truth post-migration.
11910
12464
  */
11911
12465
  localResolvers = /* @__PURE__ */ new Map();
12466
+ /**
12467
+ * Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
12468
+ * construction from `projectRoot/agents/skills/`. Consulted by
12469
+ * `buildRoutingUseCase` at dispatch start to construct
12470
+ * `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
12471
+ * Empty when the orchestrator runs outside a harness project root
12472
+ * (then dispatch falls through to per-tier, preserving F11/N2).
12473
+ */
12474
+ skillCatalog;
11912
12475
  /**
11913
12476
  * Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
11914
12477
  * (SC39): each local/pi resolver gets its own listener emitting a
@@ -11957,7 +12520,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
11957
12520
  completionHandler;
11958
12521
  /** Project root directory, derived from workspace root. */
11959
12522
  get projectRoot() {
11960
- return path19.resolve(this.config.workspace.root, "..", "..");
12523
+ return path21.resolve(this.config.workspace.root, "..", "..");
11961
12524
  }
11962
12525
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
11963
12526
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -12001,6 +12564,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12001
12564
  `migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
12002
12565
  );
12003
12566
  }
12567
+ const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
12568
+ this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
12569
+ if (this.skillCatalog.length === 0) {
12570
+ this.logger.warn(
12571
+ `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")}.`
12572
+ );
12573
+ }
12004
12574
  this.tracker = overrides?.tracker || this.createTracker();
12005
12575
  this.workspace = new WorkspaceManager(config.workspace, {
12006
12576
  emitEvent: (event) => {
@@ -12012,10 +12582,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12012
12582
  this.renderer = new PromptRenderer();
12013
12583
  this.overrideBackend = overrides?.backend ?? null;
12014
12584
  this.interactionQueue = new InteractionQueue(
12015
- path19.join(config.workspace.root, "..", "interactions"),
12585
+ path21.join(config.workspace.root, "..", "interactions"),
12016
12586
  this
12017
12587
  );
12018
- this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
12588
+ this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
12019
12589
  const backendsMap = this.config.agent.backends ?? {};
12020
12590
  for (const [name, def] of Object.entries(backendsMap)) {
12021
12591
  if (def.type === "local" || def.type === "pi") {
@@ -12036,6 +12606,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12036
12606
  const routing = this.config.agent.routing ?? {
12037
12607
  default: firstBackendName ?? "primary"
12038
12608
  };
12609
+ this.routingDecisionBus = new RoutingDecisionBus({
12610
+ capacity: 500,
12611
+ logger: this.logger
12612
+ });
12039
12613
  this.backendFactory = new OrchestratorBackendFactory({
12040
12614
  backends: this.config.agent.backends,
12041
12615
  routing,
@@ -12043,6 +12617,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12043
12617
  ...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
12044
12618
  ...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
12045
12619
  cacheMetrics: this.cacheMetrics,
12620
+ decisionBus: this.routingDecisionBus,
12046
12621
  getResolverModelFor: (name) => {
12047
12622
  const resolver = this.localResolvers.get(name);
12048
12623
  return resolver ? () => resolver.resolveModel() : void 0;
@@ -12050,6 +12625,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12050
12625
  });
12051
12626
  } else {
12052
12627
  this.backendFactory = null;
12628
+ this.routingDecisionBus = null;
12053
12629
  }
12054
12630
  this.pipeline = null;
12055
12631
  this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
@@ -12059,7 +12635,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12059
12635
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
12060
12636
  });
12061
12637
  this.recorder = new StreamRecorder(
12062
- path19.resolve(config.workspace.root, "..", "streams"),
12638
+ path21.resolve(config.workspace.root, "..", "streams"),
12063
12639
  this.logger
12064
12640
  );
12065
12641
  const self = this;
@@ -12090,10 +12666,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12090
12666
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
12091
12667
  if (config.server?.port) {
12092
12668
  const webhookStore = new WebhookStore(
12093
- path19.join(this.projectRoot, ".harness", "webhooks.json")
12669
+ path21.join(this.projectRoot, ".harness", "webhooks.json")
12094
12670
  );
12095
12671
  this.webhookQueue = new WebhookQueue(
12096
- path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12672
+ path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12097
12673
  );
12098
12674
  const webhookDelivery = new WebhookDelivery({
12099
12675
  queue: this.webhookQueue,
@@ -12131,7 +12707,16 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12131
12707
  queue: this.webhookQueue
12132
12708
  },
12133
12709
  cacheMetrics: this.cacheMetrics,
12134
- plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
12710
+ // Spec B Phase 5: routing observability accessors. Closures so the
12711
+ // server re-reads on every request — stop() / start() do not
12712
+ // require server reconstruction. Returns null if no backendFactory
12713
+ // (legacy single-backend configs), and the route handler renders
12714
+ // 503 in that case.
12715
+ getBackendRouter: () => this.getBackendRouter(),
12716
+ getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
12717
+ getRoutingConfig: () => this.getRoutingConfig(),
12718
+ getBackends: () => this.getBackends(),
12719
+ plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
12135
12720
  pipeline: this.pipeline,
12136
12721
  analysisArchive: this.analysisArchive,
12137
12722
  roadmapPath: config.tracker.filePath ?? null,
@@ -12241,7 +12826,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12241
12826
  }
12242
12827
  };
12243
12828
  const outputStore = new TaskOutputStore({
12244
- rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12829
+ rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12245
12830
  logger: this.logger
12246
12831
  });
12247
12832
  const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
@@ -12282,7 +12867,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
12282
12867
  ${messages}`);
12283
12868
  }
12284
12869
  this.maintenanceReporter = new MaintenanceReporter({
12285
- persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12870
+ persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
12286
12871
  logger: this.logger
12287
12872
  });
12288
12873
  await this.maintenanceReporter.load();
@@ -12333,10 +12918,17 @@ ${messages}`);
12333
12918
  }
12334
12919
  }
12335
12920
  createIntelligencePipeline() {
12921
+ if (!this.backendFactory) {
12922
+ this.logger.warn(
12923
+ "intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
12924
+ );
12925
+ return null;
12926
+ }
12336
12927
  const bundle = buildIntelligencePipeline({
12337
12928
  config: this.config,
12338
12929
  localResolvers: this.localResolvers,
12339
- logger: this.logger
12930
+ logger: this.logger,
12931
+ router: this.backendFactory.getRouter()
12340
12932
  });
12341
12933
  if (!bundle) return null;
12342
12934
  this.graphStore = bundle.graphStore;
@@ -12387,11 +12979,13 @@ ${messages}`);
12387
12979
  simulationResults,
12388
12980
  personaRecommendations
12389
12981
  } = pipelineResult ?? {};
12982
+ const selfAssignee = await this.orchestratorIdPromise;
12390
12983
  const tickEvent = {
12391
12984
  type: "tick",
12392
12985
  candidates,
12393
12986
  runningStates: runningStatesResult.value,
12394
12987
  nowMs,
12988
+ selfAssignee,
12395
12989
  ...concernSignals !== void 0 && { concernSignals },
12396
12990
  ...enrichedSpecs !== void 0 && { enrichedSpecs },
12397
12991
  ...complexityScores !== void 0 && { complexityScores },
@@ -12815,14 +13409,24 @@ ${messages}`);
12815
13409
  issue,
12816
13410
  attempt: attempt || 1
12817
13411
  });
12818
- const useCase = useCaseForBackendParam(issue, backend);
13412
+ const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
13413
+ const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
13414
+ const routerOpts = invocationOverride ? { invocationOverride } : void 0;
13415
+ if (invocationOverride) {
13416
+ this.logger.info(
13417
+ `Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
13418
+ { issueId: issue.id }
13419
+ );
13420
+ }
12819
13421
  let routedBackendName;
12820
13422
  if (this.overrideBackend !== null) {
12821
13423
  routedBackendName = this.overrideBackend.name;
12822
13424
  } else if (this.backendFactory !== null) {
12823
- routedBackendName = this.backendFactory.resolveName(useCase);
13425
+ routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
12824
13426
  } else {
12825
- routedBackendName = this.config.agent.routing?.default ?? this.config.agent.backend ?? "unknown";
13427
+ const routingDefault = this.config.agent.routing?.default;
13428
+ const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
13429
+ routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
12826
13430
  }
12827
13431
  const session = {
12828
13432
  sessionId: `pending-${Date.now()}`,
@@ -12861,7 +13465,7 @@ ${messages}`);
12861
13465
  if (this.overrideBackend !== null) {
12862
13466
  agentBackend = this.overrideBackend;
12863
13467
  } else if (this.backendFactory !== null) {
12864
- agentBackend = this.backendFactory.forUseCase(useCase);
13468
+ agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
12865
13469
  } else {
12866
13470
  throw new Error(
12867
13471
  `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 +13755,8 @@ ${messages}`);
13151
13755
  unsub();
13152
13756
  }
13153
13757
  this.localModelStatusUnsubscribes = [];
13758
+ this.routingDecisionBus?.clearListeners();
13759
+ this.routingDecisionBus = null;
13154
13760
  for (const resolver of this.localResolvers.values()) {
13155
13761
  resolver.stop();
13156
13762
  }
@@ -13224,6 +13830,42 @@ ${messages}`);
13224
13830
  tickActivity: this.tickActivity
13225
13831
  };
13226
13832
  }
13833
+ /**
13834
+ * Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
13835
+ * Phase 7 (dashboard WS broadcast). Returns null when the legacy
13836
+ * single-backend config bypassed agent.backends synthesis.
13837
+ */
13838
+ getRoutingDecisionBus() {
13839
+ return this.routingDecisionBus;
13840
+ }
13841
+ /**
13842
+ * Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
13843
+ * dispatch path uses the factory-owned router directly; observability
13844
+ * routes (config / decisions) reach it through this accessor. Returns
13845
+ * null when the legacy single-backend config bypassed agent.backends
13846
+ * synthesis (no backendFactory built).
13847
+ */
13848
+ getBackendRouter() {
13849
+ return this.backendFactory?.getRouter() ?? null;
13850
+ }
13851
+ /**
13852
+ * Spec B Phase 5: snapshot of the active RoutingConfig for the config
13853
+ * route and the trace route's bus-less router construction. Returns
13854
+ * null when the operator's harness.config.json carries no
13855
+ * `agent.routing` block.
13856
+ */
13857
+ getRoutingConfig() {
13858
+ return this.config.agent.routing ?? null;
13859
+ }
13860
+ /**
13861
+ * Spec B Phase 5: snapshot of `agent.backends` for the config route
13862
+ * (existence annotations) and the trace route (bus-less router
13863
+ * construction). Returns null when no synthesized backends map exists
13864
+ * (legacy single-backend configs).
13865
+ */
13866
+ getBackends() {
13867
+ return this.config.agent.backends ?? null;
13868
+ }
13227
13869
  /** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
13228
13870
  getMaintenanceStatus() {
13229
13871
  return this.maintenanceScheduler?.getStatus() ?? null;
@@ -13602,8 +14244,8 @@ async function syncMain(repoRoot, opts = {}) {
13602
14244
  }
13603
14245
 
13604
14246
  // src/sessions/search-index.ts
13605
- var fs17 = __toESM(require("fs"));
13606
- var path20 = __toESM(require("path"));
14247
+ var fs18 = __toESM(require("fs"));
14248
+ var path22 = __toESM(require("path"));
13607
14249
  var import_better_sqlite32 = __toESM(require("better-sqlite3"));
13608
14250
  var import_types31 = require("@harness-engineering/types");
13609
14251
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -13648,7 +14290,7 @@ function normalizeFts5Query(query) {
13648
14290
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
13649
14291
  }
13650
14292
  function searchIndexPath(projectPath) {
13651
- return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
14293
+ return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13652
14294
  }
13653
14295
  var FILE_KIND_TO_FILENAME = {
13654
14296
  summary: "summary.md",
@@ -13663,7 +14305,7 @@ var SqliteSearchIndex = class {
13663
14305
  removeSessionStmt;
13664
14306
  totalStmt;
13665
14307
  constructor(dbPath) {
13666
- fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
14308
+ fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
13667
14309
  this.db = new import_better_sqlite32.default(dbPath);
13668
14310
  this.db.pragma("journal_mode = WAL");
13669
14311
  this.db.pragma("synchronous = NORMAL");
@@ -13768,14 +14410,14 @@ function indexSessionDirectory(idx, args) {
13768
14410
  let docsWritten = 0;
13769
14411
  for (const kind of kinds) {
13770
14412
  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");
14413
+ const filePath = path22.join(args.sessionDir, fileName);
14414
+ if (!fs18.existsSync(filePath)) continue;
14415
+ let body = fs18.readFileSync(filePath, "utf8");
13774
14416
  if (Buffer.byteLength(body, "utf8") > cap) {
13775
14417
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
13776
14418
  }
13777
- const stat = fs17.statSync(filePath);
13778
- const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
14419
+ const stat = fs18.statSync(filePath);
14420
+ const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
13779
14421
  idx.upsertSessionDoc({
13780
14422
  sessionId: args.sessionId,
13781
14423
  archived: args.archived,
@@ -13790,17 +14432,17 @@ function indexSessionDirectory(idx, args) {
13790
14432
  }
13791
14433
  function reindexFromArchive(projectPath, opts = {}) {
13792
14434
  const start = Date.now();
13793
- const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
14435
+ const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
13794
14436
  const idx = openSearchIndex(projectPath);
13795
14437
  try {
13796
14438
  idx.resetArchived();
13797
14439
  let sessionsIndexed = 0;
13798
14440
  let docsWritten = 0;
13799
- if (fs17.existsSync(archiveBase)) {
13800
- const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
14441
+ if (fs18.existsSync(archiveBase)) {
14442
+ const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
13801
14443
  for (const entry of entries) {
13802
14444
  if (!entry.isDirectory()) continue;
13803
- const sessionDir = path20.join(archiveBase, entry.name);
14445
+ const sessionDir = path22.join(archiveBase, entry.name);
13804
14446
  const result = indexSessionDirectory(idx, {
13805
14447
  sessionId: entry.name,
13806
14448
  sessionDir,
@@ -13820,8 +14462,8 @@ function reindexFromArchive(projectPath, opts = {}) {
13820
14462
  }
13821
14463
 
13822
14464
  // src/sessions/summarize.ts
13823
- var fs18 = __toESM(require("fs"));
13824
- var path21 = __toESM(require("path"));
14465
+ var fs19 = __toESM(require("fs"));
14466
+ var path23 = __toESM(require("path"));
13825
14467
  var import_types32 = require("@harness-engineering/types");
13826
14468
  var import_types33 = require("@harness-engineering/types");
13827
14469
  var LLM_SUMMARY_FILE = "llm-summary.md";
@@ -13849,10 +14491,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
13849
14491
  function readInputCorpus(archiveDir) {
13850
14492
  const parts = [];
13851
14493
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
13852
- const p = path21.join(archiveDir, filename);
13853
- if (!fs18.existsSync(p)) continue;
14494
+ const p = path23.join(archiveDir, filename);
14495
+ if (!fs19.existsSync(p)) continue;
13854
14496
  try {
13855
- const content = fs18.readFileSync(p, "utf8");
14497
+ const content = fs19.readFileSync(p, "utf8");
13856
14498
  if (content.trim().length === 0) continue;
13857
14499
  parts.push(`## FILE: ${kind}
13858
14500
 
@@ -13903,7 +14545,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
13903
14545
  return lines.join("\n");
13904
14546
  }
13905
14547
  function writeStubMarkdown(archiveDir, reason) {
13906
- const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
14548
+ const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
13907
14549
  const body = `---
13908
14550
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
13909
14551
  schemaVersion: 1
@@ -13914,12 +14556,12 @@ status: failed
13914
14556
 
13915
14557
  - reason: ${reason}
13916
14558
  `;
13917
- fs18.writeFileSync(filePath, body, "utf8");
14559
+ fs19.writeFileSync(filePath, body, "utf8");
13918
14560
  return filePath;
13919
14561
  }
13920
14562
  async function summarizeArchivedSession(ctx) {
13921
14563
  const writeStubOnError = ctx.writeStubOnError ?? true;
13922
- if (!fs18.existsSync(ctx.archiveDir)) {
14564
+ if (!fs19.existsSync(ctx.archiveDir)) {
13923
14565
  return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
13924
14566
  }
13925
14567
  const corpus = readInputCorpus(ctx.archiveDir);
@@ -13980,9 +14622,9 @@ async function summarizeArchivedSession(ctx) {
13980
14622
  outputTokens: response.tokenUsage.outputTokens,
13981
14623
  schemaVersion: 1
13982
14624
  };
13983
- const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
14625
+ const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13984
14626
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
13985
- fs18.writeFileSync(filePath, body, "utf8");
14627
+ fs19.writeFileSync(filePath, body, "utf8");
13986
14628
  return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
13987
14629
  }
13988
14630
  function isSummaryEnabled(config) {
@@ -14061,12 +14703,14 @@ function buildArchiveHooks(opts) {
14061
14703
  0 && (module.exports = {
14062
14704
  AnalysisArchive,
14063
14705
  BUILT_IN_TASKS,
14706
+ BackendDefSchema,
14064
14707
  BackendRouter,
14065
14708
  ClaimManager,
14066
14709
  GateNotReadyError,
14067
14710
  GateRunError,
14068
14711
  InteractionQueue,
14069
14712
  LinearGraphQLStub,
14713
+ LocalModelResolver,
14070
14714
  MAX_ATTEMPTS,
14071
14715
  MockBackend,
14072
14716
  ORCHESTRATOR_IDENTITY_FILE,
@@ -14077,6 +14721,8 @@ function buildArchiveHooks(opts) {
14077
14721
  PromptRenderer,
14078
14722
  RETRY_DELAYS_MS,
14079
14723
  RoadmapTrackerAdapter,
14724
+ RoutingConfigSchema,
14725
+ RoutingValueSchema,
14080
14726
  SinkConfigError,
14081
14727
  SinkRegistry,
14082
14728
  SlackSink,
@@ -14096,7 +14742,11 @@ function buildArchiveHooks(opts) {
14096
14742
  computeRateLimitDelay,
14097
14743
  createBackend,
14098
14744
  createEmptyState,
14745
+ crossFieldRoutingIssues,
14746
+ defaultFetchModels,
14099
14747
  detectScopeTier,
14748
+ discoverSkillCatalog,
14749
+ discoverSkillCatalogNames,
14100
14750
  emitProposalApproved,
14101
14751
  emitProposalCreated,
14102
14752
  emitProposalRejected,
@@ -14112,6 +14762,7 @@ function buildArchiveHooks(opts) {
14112
14762
  loadPublishedIndex,
14113
14763
  migrateAgentConfig,
14114
14764
  normalizeFts5Query,
14765
+ normalizeLocalModel,
14115
14766
  openSearchIndex,
14116
14767
  promote,
14117
14768
  reconcile,
@@ -14122,6 +14773,7 @@ function buildArchiveHooks(opts) {
14122
14773
  resolveEscalationConfig,
14123
14774
  resolveOrchestratorId,
14124
14775
  routeIssue,
14776
+ routingWarnings,
14125
14777
  runGate,
14126
14778
  savePublishedIndex,
14127
14779
  searchIndexPath,