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