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