@harness-engineering/orchestrator 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +623 -51
- package/dist/index.d.ts +623 -51
- package/dist/index.js +2587 -529
- package/dist/index.mjs +2560 -501
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -31,8 +31,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AnalysisArchive: () => AnalysisArchive,
|
|
34
|
+
BUILT_IN_TASKS: () => BUILT_IN_TASKS,
|
|
34
35
|
BackendRouter: () => BackendRouter,
|
|
35
36
|
ClaimManager: () => ClaimManager,
|
|
37
|
+
GateNotReadyError: () => GateNotReadyError,
|
|
38
|
+
GateRunError: () => GateRunError,
|
|
36
39
|
InteractionQueue: () => InteractionQueue,
|
|
37
40
|
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
38
41
|
MAX_ATTEMPTS: () => MAX_ATTEMPTS,
|
|
@@ -41,6 +44,7 @@ __export(index_exports, {
|
|
|
41
44
|
Orchestrator: () => Orchestrator,
|
|
42
45
|
OrchestratorBackendFactory: () => OrchestratorBackendFactory,
|
|
43
46
|
PRDetector: () => PRDetector,
|
|
47
|
+
PromotionError: () => PromotionError,
|
|
44
48
|
PromptRenderer: () => PromptRenderer,
|
|
45
49
|
RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
|
|
46
50
|
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
@@ -49,6 +53,7 @@ __export(index_exports, {
|
|
|
49
53
|
SlackSink: () => SlackSink,
|
|
50
54
|
SqliteSearchIndex: () => SqliteSearchIndex,
|
|
51
55
|
StreamRecorder: () => StreamRecorder,
|
|
56
|
+
TaskOutputStore: () => TaskOutputStore,
|
|
52
57
|
TokenStore: () => TokenStore,
|
|
53
58
|
WebhookQueue: () => WebhookQueue,
|
|
54
59
|
WorkflowLoader: () => WorkflowLoader,
|
|
@@ -62,7 +67,13 @@ __export(index_exports, {
|
|
|
62
67
|
computeRateLimitDelay: () => computeRateLimitDelay,
|
|
63
68
|
createBackend: () => createBackend,
|
|
64
69
|
createEmptyState: () => createEmptyState,
|
|
70
|
+
crossFieldRoutingIssues: () => crossFieldRoutingIssues,
|
|
65
71
|
detectScopeTier: () => detectScopeTier,
|
|
72
|
+
discoverSkillCatalog: () => discoverSkillCatalog,
|
|
73
|
+
discoverSkillCatalogNames: () => discoverSkillCatalogNames,
|
|
74
|
+
emitProposalApproved: () => emitProposalApproved,
|
|
75
|
+
emitProposalCreated: () => emitProposalCreated,
|
|
76
|
+
emitProposalRejected: () => emitProposalRejected,
|
|
66
77
|
extractHighlights: () => extractHighlights,
|
|
67
78
|
extractTitlePrefix: () => extractTitlePrefix,
|
|
68
79
|
getAvailableSlots: () => getAvailableSlots,
|
|
@@ -76,6 +87,7 @@ __export(index_exports, {
|
|
|
76
87
|
migrateAgentConfig: () => migrateAgentConfig,
|
|
77
88
|
normalizeFts5Query: () => normalizeFts5Query,
|
|
78
89
|
openSearchIndex: () => openSearchIndex,
|
|
90
|
+
promote: () => promote,
|
|
79
91
|
reconcile: () => reconcile,
|
|
80
92
|
reindexFromArchive: () => reindexFromArchive,
|
|
81
93
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
@@ -84,6 +96,8 @@ __export(index_exports, {
|
|
|
84
96
|
resolveEscalationConfig: () => resolveEscalationConfig,
|
|
85
97
|
resolveOrchestratorId: () => resolveOrchestratorId,
|
|
86
98
|
routeIssue: () => routeIssue,
|
|
99
|
+
routingWarnings: () => routingWarnings,
|
|
100
|
+
runGate: () => runGate,
|
|
87
101
|
savePublishedIndex: () => savePublishedIndex,
|
|
88
102
|
searchIndexPath: () => searchIndexPath,
|
|
89
103
|
selectCandidates: () => selectCandidates,
|
|
@@ -92,6 +106,7 @@ __export(index_exports, {
|
|
|
92
106
|
syncMain: () => syncMain,
|
|
93
107
|
triageIssue: () => triageIssue,
|
|
94
108
|
truncateForBudget: () => truncateForBudget,
|
|
109
|
+
validateCustomTasks: () => validateCustomTasks,
|
|
95
110
|
validateWorkflowConfig: () => validateWorkflowConfig,
|
|
96
111
|
wireNotificationSinks: () => wireNotificationSinks,
|
|
97
112
|
wrapAsEnvelope: () => wrapAsEnvelope
|
|
@@ -127,7 +142,7 @@ function sortCandidates(issues) {
|
|
|
127
142
|
return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
|
|
128
143
|
});
|
|
129
144
|
}
|
|
130
|
-
function isEligible(issue, state, activeStates, terminalStates) {
|
|
145
|
+
function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
|
|
131
146
|
if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
|
|
132
147
|
return false;
|
|
133
148
|
}
|
|
@@ -149,6 +164,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
149
164
|
if (state.completed.has(issue.id)) {
|
|
150
165
|
return false;
|
|
151
166
|
}
|
|
167
|
+
if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
152
170
|
if (normalizedState === "todo" && issue.blockedBy.length > 0) {
|
|
153
171
|
const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
|
|
154
172
|
if (blocker.state === null) return true;
|
|
@@ -160,9 +178,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
160
178
|
}
|
|
161
179
|
return true;
|
|
162
180
|
}
|
|
163
|
-
function selectCandidates(issues, state, activeStates, terminalStates) {
|
|
181
|
+
function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
|
|
164
182
|
const sorted = sortCandidates(issues);
|
|
165
|
-
return sorted.filter(
|
|
183
|
+
return sorted.filter(
|
|
184
|
+
(issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
|
|
185
|
+
);
|
|
166
186
|
}
|
|
167
187
|
|
|
168
188
|
// src/core/concurrency.ts
|
|
@@ -758,7 +778,8 @@ function handleTick(state, event, config) {
|
|
|
758
778
|
candidates,
|
|
759
779
|
next,
|
|
760
780
|
config.tracker.activeStates,
|
|
761
|
-
config.tracker.terminalStates
|
|
781
|
+
config.tracker.terminalStates,
|
|
782
|
+
event.selfAssignee
|
|
762
783
|
);
|
|
763
784
|
const escalationConfig = resolveEscalationConfig(config);
|
|
764
785
|
for (const issue of eligible) {
|
|
@@ -1244,7 +1265,7 @@ var ClaimManager = class {
|
|
|
1244
1265
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1245
1266
|
if (!claimResult.ok) return claimResult;
|
|
1246
1267
|
if (this.verifyDelayMs > 0) {
|
|
1247
|
-
await new Promise((
|
|
1268
|
+
await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
|
|
1248
1269
|
}
|
|
1249
1270
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1250
1271
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1896,8 +1917,9 @@ function formatFilesList(files) {
|
|
|
1896
1917
|
}
|
|
1897
1918
|
|
|
1898
1919
|
// src/workflow/loader.ts
|
|
1899
|
-
var
|
|
1900
|
-
var
|
|
1920
|
+
var fs7 = __toESM(require("fs/promises"));
|
|
1921
|
+
var path7 = __toESM(require("path"));
|
|
1922
|
+
var import_yaml2 = require("yaml");
|
|
1901
1923
|
var import_types3 = require("@harness-engineering/types");
|
|
1902
1924
|
|
|
1903
1925
|
// src/workflow/config.ts
|
|
@@ -1949,16 +1971,29 @@ var BackendDefSchema = import_zod.z.discriminatedUnion("type", [
|
|
|
1949
1971
|
probeIntervalMs: import_zod.z.number().int().min(1e3).optional()
|
|
1950
1972
|
}).strict()
|
|
1951
1973
|
]);
|
|
1974
|
+
var RoutingValueSchema = import_zod.z.union([
|
|
1975
|
+
import_zod.z.string().min(1),
|
|
1976
|
+
import_zod.z.array(import_zod.z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
|
|
1977
|
+
]);
|
|
1952
1978
|
var RoutingConfigSchema = import_zod.z.object({
|
|
1953
|
-
default:
|
|
1954
|
-
"quick-fix":
|
|
1955
|
-
"guided-change":
|
|
1956
|
-
"full-exploration":
|
|
1957
|
-
diagnostic:
|
|
1979
|
+
default: RoutingValueSchema,
|
|
1980
|
+
"quick-fix": RoutingValueSchema.optional(),
|
|
1981
|
+
"guided-change": RoutingValueSchema.optional(),
|
|
1982
|
+
"full-exploration": RoutingValueSchema.optional(),
|
|
1983
|
+
diagnostic: RoutingValueSchema.optional(),
|
|
1958
1984
|
intelligence: import_zod.z.object({
|
|
1959
|
-
sel:
|
|
1960
|
-
pesl:
|
|
1961
|
-
}).strict().optional()
|
|
1985
|
+
sel: RoutingValueSchema.optional(),
|
|
1986
|
+
pesl: RoutingValueSchema.optional()
|
|
1987
|
+
}).strict().optional(),
|
|
1988
|
+
// --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
|
|
1989
|
+
isolation: import_zod.z.object({
|
|
1990
|
+
none: RoutingValueSchema.optional(),
|
|
1991
|
+
container: RoutingValueSchema.optional(),
|
|
1992
|
+
"remote-sandbox": RoutingValueSchema.optional()
|
|
1993
|
+
}).strict().optional(),
|
|
1994
|
+
// --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
|
|
1995
|
+
skills: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional(),
|
|
1996
|
+
modes: import_zod.z.record(import_zod.z.string().min(1), RoutingValueSchema).optional()
|
|
1962
1997
|
}).strict();
|
|
1963
1998
|
|
|
1964
1999
|
// src/workflow/config.ts
|
|
@@ -1967,13 +2002,17 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
|
|
|
1967
2002
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1968
2003
|
const issues = [];
|
|
1969
2004
|
const names = new Set(Object.keys(backends));
|
|
1970
|
-
const checkRef = (
|
|
1971
|
-
if (
|
|
2005
|
+
const checkRef = (path24, value) => {
|
|
2006
|
+
if (value === void 0) return;
|
|
2007
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
2008
|
+
entries.forEach((name, idx) => {
|
|
2009
|
+
if (names.has(name)) return;
|
|
2010
|
+
const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
|
|
1972
2011
|
issues.push({
|
|
1973
|
-
path:
|
|
1974
|
-
message: `routing.${
|
|
2012
|
+
path: pathWithIdx,
|
|
2013
|
+
message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1975
2014
|
});
|
|
1976
|
-
}
|
|
2015
|
+
});
|
|
1977
2016
|
};
|
|
1978
2017
|
checkRef(["default"], routing.default);
|
|
1979
2018
|
checkRef(["quick-fix"], routing["quick-fix"]);
|
|
@@ -1982,9 +2021,44 @@ function crossFieldRoutingIssues(backends, routing) {
|
|
|
1982
2021
|
checkRef(["diagnostic"], routing.diagnostic);
|
|
1983
2022
|
checkRef(["intelligence", "sel"], routing.intelligence?.sel);
|
|
1984
2023
|
checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
|
|
2024
|
+
checkRef(["isolation", "none"], routing.isolation?.none);
|
|
2025
|
+
checkRef(["isolation", "container"], routing.isolation?.container);
|
|
2026
|
+
checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
|
|
2027
|
+
if (routing.skills) {
|
|
2028
|
+
for (const [skill, value] of Object.entries(routing.skills)) {
|
|
2029
|
+
checkRef(["skills", skill], value);
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
if (routing.modes) {
|
|
2033
|
+
for (const [mode, value] of Object.entries(routing.modes)) {
|
|
2034
|
+
checkRef(["modes", mode], value);
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
1985
2037
|
return issues;
|
|
1986
2038
|
}
|
|
1987
|
-
function
|
|
2039
|
+
function routingWarnings(routing, knownSkillNames) {
|
|
2040
|
+
const warnings = [];
|
|
2041
|
+
if (knownSkillNames.length > 0 && routing.skills) {
|
|
2042
|
+
const known = new Set(knownSkillNames);
|
|
2043
|
+
for (const name of Object.keys(routing.skills)) {
|
|
2044
|
+
if (known.has(name)) continue;
|
|
2045
|
+
warnings.push(
|
|
2046
|
+
`routing.skills.${name} references a skill that is not present in the local skill catalog. If this is intentional (e.g., a skill installed by a downstream consumer), this warning can be ignored.`
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
if (routing.modes) {
|
|
2051
|
+
const standardModes = new Set(import_types2.STANDARD_COGNITIVE_MODES);
|
|
2052
|
+
for (const mode of Object.keys(routing.modes)) {
|
|
2053
|
+
if (standardModes.has(mode)) continue;
|
|
2054
|
+
warnings.push(
|
|
2055
|
+
`routing.modes.${mode} is not in STANDARD_COGNITIVE_MODES (${[...import_types2.STANDARD_COGNITIVE_MODES].join(", ")}). Custom cognitive modes are allowed but uncommon; verify this is not a typo.`
|
|
2056
|
+
);
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
return warnings;
|
|
2060
|
+
}
|
|
2061
|
+
function validateWorkflowConfig(config, options = {}) {
|
|
1988
2062
|
if (!config || typeof config !== "object")
|
|
1989
2063
|
return (0, import_types2.Err)(new Error("Config is missing or not an object"));
|
|
1990
2064
|
const c = config;
|
|
@@ -2000,6 +2074,7 @@ function validateWorkflowConfig(config) {
|
|
|
2000
2074
|
if (!hasLegacyBackend && !hasModernBackends) {
|
|
2001
2075
|
return (0, import_types2.Err)(new Error("Config must define agent.backend or agent.backends."));
|
|
2002
2076
|
}
|
|
2077
|
+
const warnings = [];
|
|
2003
2078
|
if (hasModernBackends) {
|
|
2004
2079
|
const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
|
|
2005
2080
|
if (!backendsParsed.success) {
|
|
@@ -2010,9 +2085,10 @@ function validateWorkflowConfig(config) {
|
|
|
2010
2085
|
return (0, import_types2.Err)(new Error(`agent.routing: ${routingParsed.error.message}`));
|
|
2011
2086
|
}
|
|
2012
2087
|
if (routingParsed.data) {
|
|
2088
|
+
const routingData = routingParsed.data;
|
|
2013
2089
|
const cross = crossFieldRoutingIssues(
|
|
2014
2090
|
backendsParsed.data,
|
|
2015
|
-
|
|
2091
|
+
routingData
|
|
2016
2092
|
);
|
|
2017
2093
|
if (cross.length > 0) {
|
|
2018
2094
|
return (0, import_types2.Err)(
|
|
@@ -2021,9 +2097,10 @@ function validateWorkflowConfig(config) {
|
|
|
2021
2097
|
)
|
|
2022
2098
|
);
|
|
2023
2099
|
}
|
|
2100
|
+
warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
|
|
2024
2101
|
}
|
|
2025
2102
|
}
|
|
2026
|
-
return (0, import_types2.Ok)(config);
|
|
2103
|
+
return (0, import_types2.Ok)({ config, warnings });
|
|
2027
2104
|
}
|
|
2028
2105
|
function getDefaultConfig() {
|
|
2029
2106
|
return {
|
|
@@ -2076,11 +2153,55 @@ function getDefaultConfig() {
|
|
|
2076
2153
|
};
|
|
2077
2154
|
}
|
|
2078
2155
|
|
|
2156
|
+
// src/workflow/skill-catalog.ts
|
|
2157
|
+
var fs6 = __toESM(require("fs"));
|
|
2158
|
+
var path6 = __toESM(require("path"));
|
|
2159
|
+
var import_yaml = require("yaml");
|
|
2160
|
+
function discoverSkillCatalog(projectRoot) {
|
|
2161
|
+
const skillsRoot = path6.join(projectRoot, "agents", "skills");
|
|
2162
|
+
if (!fs6.existsSync(skillsRoot)) return [];
|
|
2163
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2164
|
+
let hosts;
|
|
2165
|
+
try {
|
|
2166
|
+
hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
|
|
2167
|
+
} catch {
|
|
2168
|
+
return [];
|
|
2169
|
+
}
|
|
2170
|
+
for (const host of hosts) {
|
|
2171
|
+
if (!host.isDirectory()) continue;
|
|
2172
|
+
const hostDir = path6.join(skillsRoot, host.name);
|
|
2173
|
+
let skills;
|
|
2174
|
+
try {
|
|
2175
|
+
skills = fs6.readdirSync(hostDir, { withFileTypes: true });
|
|
2176
|
+
} catch {
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
for (const skill of skills) {
|
|
2180
|
+
if (!skill.isDirectory()) continue;
|
|
2181
|
+
const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
|
|
2182
|
+
if (!fs6.existsSync(skillYamlPath)) continue;
|
|
2183
|
+
try {
|
|
2184
|
+
const content = fs6.readFileSync(skillYamlPath, "utf-8");
|
|
2185
|
+
const parsed = (0, import_yaml.parse)(content);
|
|
2186
|
+
if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
|
|
2187
|
+
const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
|
|
2188
|
+
byName.set(parsed.name, entry);
|
|
2189
|
+
}
|
|
2190
|
+
} catch {
|
|
2191
|
+
}
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2195
|
+
}
|
|
2196
|
+
function discoverSkillCatalogNames(projectRoot) {
|
|
2197
|
+
return discoverSkillCatalog(projectRoot).map((e) => e.name);
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2079
2200
|
// src/workflow/loader.ts
|
|
2080
2201
|
var WorkflowLoader = class {
|
|
2081
2202
|
async loadWorkflow(filePath) {
|
|
2082
2203
|
try {
|
|
2083
|
-
const content = await
|
|
2204
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
2084
2205
|
const parts = content.split("---");
|
|
2085
2206
|
if (parts.length < 3) {
|
|
2086
2207
|
return (0, import_types3.Err)(
|
|
@@ -2091,14 +2212,17 @@ var WorkflowLoader = class {
|
|
|
2091
2212
|
}
|
|
2092
2213
|
const yamlContent = parts[1].trim();
|
|
2093
2214
|
const promptTemplate = parts.slice(2).join("---").trim();
|
|
2094
|
-
const configData = (0,
|
|
2095
|
-
const
|
|
2215
|
+
const configData = (0, import_yaml2.parse)(yamlContent);
|
|
2216
|
+
const projectRoot = path7.dirname(path7.resolve(filePath));
|
|
2217
|
+
const knownSkillNames = discoverSkillCatalogNames(projectRoot);
|
|
2218
|
+
const configResult = validateWorkflowConfig(configData, { knownSkillNames });
|
|
2096
2219
|
if (!configResult.ok) {
|
|
2097
2220
|
return (0, import_types3.Err)(configResult.error);
|
|
2098
2221
|
}
|
|
2099
2222
|
return (0, import_types3.Ok)({
|
|
2100
|
-
config: configResult.value,
|
|
2101
|
-
promptTemplate
|
|
2223
|
+
config: configResult.value.config,
|
|
2224
|
+
promptTemplate,
|
|
2225
|
+
warnings: configResult.value.warnings
|
|
2102
2226
|
});
|
|
2103
2227
|
} catch (error) {
|
|
2104
2228
|
return (0, import_types3.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2107,7 +2231,7 @@ var WorkflowLoader = class {
|
|
|
2107
2231
|
};
|
|
2108
2232
|
|
|
2109
2233
|
// src/tracker/adapters/roadmap.ts
|
|
2110
|
-
var
|
|
2234
|
+
var fs8 = __toESM(require("fs/promises"));
|
|
2111
2235
|
var import_node_crypto2 = require("crypto");
|
|
2112
2236
|
var import_core = require("@harness-engineering/core");
|
|
2113
2237
|
var import_types4 = require("@harness-engineering/types");
|
|
@@ -2138,7 +2262,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2138
2262
|
async fetchIssuesByStates(stateNames) {
|
|
2139
2263
|
try {
|
|
2140
2264
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2141
|
-
const content = await
|
|
2265
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2142
2266
|
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2143
2267
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2144
2268
|
const issues = [];
|
|
@@ -2170,7 +2294,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2170
2294
|
if (!terminal) {
|
|
2171
2295
|
return (0, import_types4.Err)(new Error("Tracker config has no terminalStates; cannot mark complete"));
|
|
2172
2296
|
}
|
|
2173
|
-
const content = await
|
|
2297
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2174
2298
|
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2175
2299
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2176
2300
|
const roadmap = roadmapResult.value;
|
|
@@ -2179,7 +2303,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2179
2303
|
const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
|
|
2180
2304
|
if (normalizedTerminal.includes(target.status.toLowerCase())) return (0, import_types4.Ok)(void 0);
|
|
2181
2305
|
target.status = terminal;
|
|
2182
|
-
await
|
|
2306
|
+
await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
|
|
2183
2307
|
return (0, import_types4.Ok)(void 0);
|
|
2184
2308
|
} catch (error) {
|
|
2185
2309
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2193,19 +2317,22 @@ var RoadmapTrackerAdapter = class {
|
|
|
2193
2317
|
async claimIssue(issueId, orchestratorId) {
|
|
2194
2318
|
try {
|
|
2195
2319
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2196
|
-
const content = await
|
|
2320
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2197
2321
|
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2198
2322
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2199
2323
|
const roadmap = roadmapResult.value;
|
|
2200
2324
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2201
2325
|
if (!target) return (0, import_types4.Ok)(void 0);
|
|
2326
|
+
if (target.assignee != null && target.assignee !== orchestratorId) {
|
|
2327
|
+
return (0, import_types4.Ok)(void 0);
|
|
2328
|
+
}
|
|
2202
2329
|
if (target.status === "in-progress" && target.assignee === orchestratorId) {
|
|
2203
2330
|
return (0, import_types4.Ok)(void 0);
|
|
2204
2331
|
}
|
|
2205
2332
|
target.status = "in-progress";
|
|
2206
2333
|
target.assignee = orchestratorId;
|
|
2207
2334
|
target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2208
|
-
await
|
|
2335
|
+
await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
|
|
2209
2336
|
return (0, import_types4.Ok)(void 0);
|
|
2210
2337
|
} catch (error) {
|
|
2211
2338
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2222,7 +2349,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2222
2349
|
if (!activeState) {
|
|
2223
2350
|
return (0, import_types4.Err)(new Error("Tracker config has no activeStates; cannot release"));
|
|
2224
2351
|
}
|
|
2225
|
-
const content = await
|
|
2352
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2226
2353
|
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2227
2354
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2228
2355
|
const roadmap = roadmapResult.value;
|
|
@@ -2234,7 +2361,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2234
2361
|
target.status = activeState;
|
|
2235
2362
|
target.assignee = null;
|
|
2236
2363
|
target.updatedAt = null;
|
|
2237
|
-
await
|
|
2364
|
+
await fs8.writeFile(this.config.filePath, (0, import_core.serializeRoadmap)(roadmap), "utf-8");
|
|
2238
2365
|
return (0, import_types4.Ok)(void 0);
|
|
2239
2366
|
} catch (error) {
|
|
2240
2367
|
return (0, import_types4.Err)(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2256,7 +2383,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2256
2383
|
async fetchIssueStatesByIds(issueIds) {
|
|
2257
2384
|
try {
|
|
2258
2385
|
if (!this.config.filePath) return (0, import_types4.Err)(new Error("Missing filePath"));
|
|
2259
|
-
const content = await
|
|
2386
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2260
2387
|
const roadmapResult = (0, import_core.parseRoadmap)(content);
|
|
2261
2388
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2262
2389
|
const issueMap = /* @__PURE__ */ new Map();
|
|
@@ -2321,8 +2448,8 @@ var LinearGraphQLStub = class {
|
|
|
2321
2448
|
};
|
|
2322
2449
|
|
|
2323
2450
|
// src/workspace/manager.ts
|
|
2324
|
-
var
|
|
2325
|
-
var
|
|
2451
|
+
var fs9 = __toESM(require("fs/promises"));
|
|
2452
|
+
var path8 = __toESM(require("path"));
|
|
2326
2453
|
var import_node_child_process2 = require("child_process");
|
|
2327
2454
|
var import_node_util2 = require("util");
|
|
2328
2455
|
var import_types6 = require("@harness-engineering/types");
|
|
@@ -2353,15 +2480,15 @@ var WorkspaceManager = class {
|
|
|
2353
2480
|
*/
|
|
2354
2481
|
resolvePath(identifier) {
|
|
2355
2482
|
const sanitized = this.sanitizeIdentifier(identifier);
|
|
2356
|
-
return
|
|
2483
|
+
return path8.join(this.config.root, sanitized);
|
|
2357
2484
|
}
|
|
2358
2485
|
/**
|
|
2359
2486
|
* Discovers the git repository root from the workspace root directory.
|
|
2360
2487
|
*/
|
|
2361
2488
|
async getRepoRoot() {
|
|
2362
2489
|
if (this.repoRoot) return this.repoRoot;
|
|
2363
|
-
const root =
|
|
2364
|
-
await
|
|
2490
|
+
const root = path8.resolve(this.config.root);
|
|
2491
|
+
await fs9.mkdir(root, { recursive: true });
|
|
2365
2492
|
const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
|
|
2366
2493
|
this.repoRoot = stdout.trim();
|
|
2367
2494
|
return this.repoRoot;
|
|
@@ -2372,23 +2499,23 @@ var WorkspaceManager = class {
|
|
|
2372
2499
|
*/
|
|
2373
2500
|
async ensureWorkspace(identifier) {
|
|
2374
2501
|
try {
|
|
2375
|
-
const workspacePath =
|
|
2502
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2376
2503
|
try {
|
|
2377
|
-
await
|
|
2504
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2378
2505
|
const repoRoot2 = await this.getRepoRoot();
|
|
2379
2506
|
try {
|
|
2380
2507
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2381
2508
|
} catch {
|
|
2382
|
-
await
|
|
2509
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2383
2510
|
}
|
|
2384
2511
|
} catch {
|
|
2385
2512
|
try {
|
|
2386
|
-
await
|
|
2513
|
+
await fs9.access(workspacePath);
|
|
2387
2514
|
const repoRoot2 = await this.getRepoRoot();
|
|
2388
2515
|
try {
|
|
2389
2516
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2390
2517
|
} catch {
|
|
2391
|
-
await
|
|
2518
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2392
2519
|
}
|
|
2393
2520
|
} catch {
|
|
2394
2521
|
}
|
|
@@ -2484,7 +2611,7 @@ var WorkspaceManager = class {
|
|
|
2484
2611
|
async exists(identifier) {
|
|
2485
2612
|
try {
|
|
2486
2613
|
const workspacePath = this.resolvePath(identifier);
|
|
2487
|
-
await
|
|
2614
|
+
await fs9.access(workspacePath);
|
|
2488
2615
|
return true;
|
|
2489
2616
|
} catch {
|
|
2490
2617
|
return false;
|
|
@@ -2497,9 +2624,9 @@ var WorkspaceManager = class {
|
|
|
2497
2624
|
*/
|
|
2498
2625
|
async findPushedBranch(identifier) {
|
|
2499
2626
|
try {
|
|
2500
|
-
const workspacePath =
|
|
2627
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2501
2628
|
try {
|
|
2502
|
-
await
|
|
2629
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2503
2630
|
} catch {
|
|
2504
2631
|
return null;
|
|
2505
2632
|
}
|
|
@@ -2605,12 +2732,12 @@ var WorkspaceManager = class {
|
|
|
2605
2732
|
*/
|
|
2606
2733
|
async removeWorkspace(identifier) {
|
|
2607
2734
|
try {
|
|
2608
|
-
const workspacePath =
|
|
2735
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2609
2736
|
try {
|
|
2610
2737
|
const repoRoot = await this.getRepoRoot();
|
|
2611
2738
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
|
|
2612
2739
|
} catch {
|
|
2613
|
-
await
|
|
2740
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2614
2741
|
}
|
|
2615
2742
|
return (0, import_types6.Ok)(void 0);
|
|
2616
2743
|
} catch (error) {
|
|
@@ -2635,7 +2762,7 @@ var WorkspaceHooks = class {
|
|
|
2635
2762
|
if (!command) {
|
|
2636
2763
|
return (0, import_types7.Ok)(void 0);
|
|
2637
2764
|
}
|
|
2638
|
-
return new Promise((
|
|
2765
|
+
return new Promise((resolve8) => {
|
|
2639
2766
|
const filteredEnv = {};
|
|
2640
2767
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2641
2768
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2648,19 +2775,19 @@ var WorkspaceHooks = class {
|
|
|
2648
2775
|
});
|
|
2649
2776
|
const timeout = setTimeout(() => {
|
|
2650
2777
|
child.kill();
|
|
2651
|
-
|
|
2778
|
+
resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2652
2779
|
}, this.config.timeoutMs);
|
|
2653
2780
|
child.on("exit", (code) => {
|
|
2654
2781
|
clearTimeout(timeout);
|
|
2655
2782
|
if (code === 0) {
|
|
2656
|
-
|
|
2783
|
+
resolve8((0, import_types7.Ok)(void 0));
|
|
2657
2784
|
} else {
|
|
2658
|
-
|
|
2785
|
+
resolve8((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2659
2786
|
}
|
|
2660
2787
|
});
|
|
2661
2788
|
child.on("error", (error) => {
|
|
2662
2789
|
clearTimeout(timeout);
|
|
2663
|
-
|
|
2790
|
+
resolve8((0, import_types7.Err)(error));
|
|
2664
2791
|
});
|
|
2665
2792
|
});
|
|
2666
2793
|
}
|
|
@@ -2698,7 +2825,7 @@ var MockBackend = class {
|
|
|
2698
2825
|
content: "Thinking...",
|
|
2699
2826
|
sessionId: session.sessionId
|
|
2700
2827
|
};
|
|
2701
|
-
await new Promise((
|
|
2828
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
2702
2829
|
yield {
|
|
2703
2830
|
type: "thought",
|
|
2704
2831
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2750,12 +2877,12 @@ var PromptRenderer = class {
|
|
|
2750
2877
|
|
|
2751
2878
|
// src/orchestrator.ts
|
|
2752
2879
|
var import_node_events = require("events");
|
|
2753
|
-
var
|
|
2880
|
+
var path21 = __toESM(require("path"));
|
|
2754
2881
|
var import_node_crypto16 = require("crypto");
|
|
2755
|
-
var
|
|
2882
|
+
var import_core14 = require("@harness-engineering/core");
|
|
2756
2883
|
|
|
2757
2884
|
// src/intelligence/pipeline-runner.ts
|
|
2758
|
-
var
|
|
2885
|
+
var path9 = __toESM(require("path"));
|
|
2759
2886
|
var import_intelligence = require("@harness-engineering/intelligence");
|
|
2760
2887
|
var import_core2 = require("@harness-engineering/core");
|
|
2761
2888
|
var CONNECTION_ERROR_PATTERNS = [
|
|
@@ -2874,7 +3001,7 @@ var IntelligencePipelineRunner = class {
|
|
|
2874
3001
|
}
|
|
2875
3002
|
async loadGraphStore() {
|
|
2876
3003
|
try {
|
|
2877
|
-
const graphDir =
|
|
3004
|
+
const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
|
|
2878
3005
|
const loaded = await this.ctx.graphStore.load(graphDir);
|
|
2879
3006
|
if (loaded) {
|
|
2880
3007
|
this.ctx.logger.info("Graph store loaded from disk");
|
|
@@ -3152,7 +3279,7 @@ var IntelligencePipelineRunner = class {
|
|
|
3152
3279
|
};
|
|
3153
3280
|
|
|
3154
3281
|
// src/completion/handler.ts
|
|
3155
|
-
var
|
|
3282
|
+
var path10 = __toESM(require("path"));
|
|
3156
3283
|
var import_core3 = require("@harness-engineering/core");
|
|
3157
3284
|
var CompletionHandler = class {
|
|
3158
3285
|
ctx;
|
|
@@ -3235,7 +3362,7 @@ var CompletionHandler = class {
|
|
|
3235
3362
|
result: outcome.result
|
|
3236
3363
|
});
|
|
3237
3364
|
if (this.ctx.graphStore) {
|
|
3238
|
-
const graphDir =
|
|
3365
|
+
const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
|
|
3239
3366
|
await this.ctx.graphStore.save(graphDir);
|
|
3240
3367
|
}
|
|
3241
3368
|
} catch (err) {
|
|
@@ -3313,7 +3440,7 @@ var CompletionHandler = class {
|
|
|
3313
3440
|
};
|
|
3314
3441
|
|
|
3315
3442
|
// src/orchestrator.ts
|
|
3316
|
-
var
|
|
3443
|
+
var import_core15 = require("@harness-engineering/core");
|
|
3317
3444
|
|
|
3318
3445
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3319
3446
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -3717,11 +3844,11 @@ function detectLegacyFields(agent) {
|
|
|
3717
3844
|
}
|
|
3718
3845
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3719
3846
|
const warnings = [];
|
|
3720
|
-
for (const
|
|
3721
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3722
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3847
|
+
for (const path24 of presentLegacy) {
|
|
3848
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
|
|
3849
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
|
|
3723
3850
|
warnings.push(
|
|
3724
|
-
`Ignoring legacy field '${
|
|
3851
|
+
`Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3725
3852
|
);
|
|
3726
3853
|
}
|
|
3727
3854
|
return warnings;
|
|
@@ -3749,7 +3876,7 @@ function migrateAgentConfig(agent) {
|
|
|
3749
3876
|
}
|
|
3750
3877
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3751
3878
|
const warnings = presentLegacy.map(
|
|
3752
|
-
(
|
|
3879
|
+
(path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3753
3880
|
);
|
|
3754
3881
|
return {
|
|
3755
3882
|
config: { ...agent, backends, routing },
|
|
@@ -3812,61 +3939,160 @@ function synthesizeLocal(agent) {
|
|
|
3812
3939
|
}
|
|
3813
3940
|
|
|
3814
3941
|
// src/agent/backend-router.ts
|
|
3942
|
+
function toArray(value) {
|
|
3943
|
+
return Array.isArray(value) ? value : [value];
|
|
3944
|
+
}
|
|
3815
3945
|
var BackendRouter = class {
|
|
3816
3946
|
backends;
|
|
3817
3947
|
routing;
|
|
3948
|
+
decisionBus;
|
|
3818
3949
|
constructor(opts) {
|
|
3819
3950
|
this.backends = opts.backends;
|
|
3820
3951
|
this.routing = opts.routing;
|
|
3952
|
+
this.decisionBus = opts.decisionBus;
|
|
3821
3953
|
this.validateReferences();
|
|
3822
3954
|
}
|
|
3823
3955
|
/**
|
|
3824
|
-
*
|
|
3956
|
+
* Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
|
|
3957
|
+
*
|
|
3958
|
+
* @param useCase the routing query
|
|
3959
|
+
* @param opts.invocationOverride if set and the named backend exists,
|
|
3960
|
+
* beats all other sources (D7 — the `--backend <name>` escape hatch)
|
|
3961
|
+
*/
|
|
3962
|
+
resolve(useCase, opts) {
|
|
3963
|
+
const startedAt = performance.now();
|
|
3964
|
+
const path24 = [];
|
|
3965
|
+
const tryChain = (source, value) => {
|
|
3966
|
+
if (value === void 0) return void 0;
|
|
3967
|
+
for (const name of toArray(value)) {
|
|
3968
|
+
const step = { source, candidate: name, outcome: "considered" };
|
|
3969
|
+
path24.push(step);
|
|
3970
|
+
if (this.backends[name]) {
|
|
3971
|
+
step.outcome = "chosen";
|
|
3972
|
+
return name;
|
|
3973
|
+
}
|
|
3974
|
+
step.outcome = "unknown-backend";
|
|
3975
|
+
}
|
|
3976
|
+
return void 0;
|
|
3977
|
+
};
|
|
3978
|
+
const decide = (backendName) => {
|
|
3979
|
+
const def = this.backends[backendName];
|
|
3980
|
+
if (!def) {
|
|
3981
|
+
throw new Error(
|
|
3982
|
+
`BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
|
|
3983
|
+
);
|
|
3984
|
+
}
|
|
3985
|
+
return {
|
|
3986
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3987
|
+
useCase,
|
|
3988
|
+
resolutionPath: path24,
|
|
3989
|
+
backendName,
|
|
3990
|
+
backendType: def.type,
|
|
3991
|
+
durationMs: performance.now() - startedAt
|
|
3992
|
+
};
|
|
3993
|
+
};
|
|
3994
|
+
const emitAndReturn = (decision) => {
|
|
3995
|
+
this.decisionBus?.emit(decision);
|
|
3996
|
+
return decision;
|
|
3997
|
+
};
|
|
3998
|
+
const fromInvocation = tryChain(
|
|
3999
|
+
"invocation",
|
|
4000
|
+
opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
|
|
4001
|
+
);
|
|
4002
|
+
if (fromInvocation) return emitAndReturn(decide(fromInvocation));
|
|
4003
|
+
if (useCase.kind === "skill") {
|
|
4004
|
+
const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
|
|
4005
|
+
if (fromSkill) return emitAndReturn(decide(fromSkill));
|
|
4006
|
+
}
|
|
4007
|
+
const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
|
|
4008
|
+
if (mode !== void 0) {
|
|
4009
|
+
const fromMode = tryChain("mode", this.routing.modes?.[mode]);
|
|
4010
|
+
if (fromMode) return emitAndReturn(decide(fromMode));
|
|
4011
|
+
}
|
|
4012
|
+
const fromExisting = this.resolveExistingUseCase(useCase);
|
|
4013
|
+
if (fromExisting !== void 0) {
|
|
4014
|
+
const chained = tryChain("tier", fromExisting);
|
|
4015
|
+
if (chained) return emitAndReturn(decide(chained));
|
|
4016
|
+
}
|
|
4017
|
+
const fromDefault = tryChain("default", this.routing.default);
|
|
4018
|
+
if (fromDefault) return emitAndReturn(decide(fromDefault));
|
|
4019
|
+
const knownList = Object.keys(this.backends).join(", ") || "(none)";
|
|
4020
|
+
throw new Error(
|
|
4021
|
+
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
|
|
4022
|
+
);
|
|
4023
|
+
}
|
|
4024
|
+
/**
|
|
4025
|
+
* Returns the {@link BackendDef} reference for the resolved name.
|
|
4026
|
+
* Identity-equal to the entry in `backends` (no copy) so callers
|
|
4027
|
+
* relying on reference equality (SC21) continue to work.
|
|
4028
|
+
*/
|
|
4029
|
+
resolveDefinition(useCase, opts) {
|
|
4030
|
+
const decision = this.resolve(useCase, opts);
|
|
4031
|
+
const def = this.backends[decision.backendName];
|
|
4032
|
+
if (!def) {
|
|
4033
|
+
throw new Error(
|
|
4034
|
+
`BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
4035
|
+
);
|
|
4036
|
+
}
|
|
4037
|
+
return def;
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
|
|
4041
|
+
* for callers that need both. Replaces the previous pattern of
|
|
4042
|
+
* `resolveDefinition(useCase) + resolve(useCase)` which produced two
|
|
4043
|
+
* RoutingDecision emissions per dispatch — doubling routing-decision
|
|
4044
|
+
* log volume now that Phase 4 emits.
|
|
3825
4045
|
*
|
|
3826
|
-
* - `
|
|
3827
|
-
*
|
|
3828
|
-
|
|
3829
|
-
|
|
4046
|
+
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
4047
|
+
* reference equality (SC21) continue to work.
|
|
4048
|
+
*/
|
|
4049
|
+
resolveDecisionAndDef(useCase, opts) {
|
|
4050
|
+
const decision = this.resolve(useCase, opts);
|
|
4051
|
+
const def = this.backends[decision.backendName];
|
|
4052
|
+
if (!def) {
|
|
4053
|
+
throw new Error(
|
|
4054
|
+
`BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
4055
|
+
);
|
|
4056
|
+
}
|
|
4057
|
+
return { decision, def };
|
|
4058
|
+
}
|
|
4059
|
+
/**
|
|
4060
|
+
* The pre-Spec-B resolution helper: returns the configured
|
|
4061
|
+
* {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
|
|
4062
|
+
* use cases (or `undefined` for skill/mode use cases, which are owned
|
|
4063
|
+
* by the per-skill / per-mode steps in {@link resolve}). Returning
|
|
4064
|
+
* `undefined` lets the caller fall through to `routing.default`.
|
|
3830
4065
|
*/
|
|
3831
|
-
|
|
4066
|
+
resolveExistingUseCase(useCase) {
|
|
3832
4067
|
switch (useCase.kind) {
|
|
3833
4068
|
case "tier": {
|
|
3834
|
-
const
|
|
3835
|
-
return
|
|
4069
|
+
const tierMap = this.routing;
|
|
4070
|
+
return tierMap[useCase.tier];
|
|
3836
4071
|
}
|
|
3837
4072
|
case "intelligence": {
|
|
3838
4073
|
const intel = this.routing.intelligence;
|
|
3839
|
-
return intel?.[useCase.layer]
|
|
4074
|
+
return intel?.[useCase.layer];
|
|
3840
4075
|
}
|
|
3841
4076
|
case "isolation": {
|
|
3842
4077
|
const iso = this.routing.isolation;
|
|
3843
|
-
return iso?.[useCase.tier]
|
|
4078
|
+
return iso?.[useCase.tier];
|
|
3844
4079
|
}
|
|
3845
4080
|
case "maintenance":
|
|
3846
4081
|
case "chat":
|
|
3847
|
-
return
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
* Returns the BackendDef reference for the resolved name. Returns the
|
|
3852
|
-
* exact reference held in `backends` (no copy) so identity comparisons
|
|
3853
|
-
* succeed (SC21).
|
|
3854
|
-
*/
|
|
3855
|
-
resolveDefinition(useCase) {
|
|
3856
|
-
const name = this.resolve(useCase);
|
|
3857
|
-
const def = this.backends[name];
|
|
3858
|
-
if (!def) {
|
|
3859
|
-
throw new Error(
|
|
3860
|
-
`BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3861
|
-
);
|
|
4082
|
+
return void 0;
|
|
4083
|
+
case "skill":
|
|
4084
|
+
case "mode":
|
|
4085
|
+
return void 0;
|
|
3862
4086
|
}
|
|
3863
|
-
return def;
|
|
3864
4087
|
}
|
|
3865
4088
|
validateReferences() {
|
|
3866
4089
|
const known = new Set(Object.keys(this.backends));
|
|
3867
4090
|
const missing = [];
|
|
3868
|
-
const check = (
|
|
3869
|
-
if (
|
|
4091
|
+
const check = (label, value) => {
|
|
4092
|
+
if (value === void 0) return;
|
|
4093
|
+
for (const name of toArray(value)) {
|
|
4094
|
+
if (!known.has(name)) missing.push({ path: label, name });
|
|
4095
|
+
}
|
|
3870
4096
|
};
|
|
3871
4097
|
check("default", this.routing.default);
|
|
3872
4098
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3878,8 +4104,14 @@ var BackendRouter = class {
|
|
|
3878
4104
|
check("isolation.none", this.routing.isolation?.none);
|
|
3879
4105
|
check("isolation.container", this.routing.isolation?.container);
|
|
3880
4106
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
4107
|
+
for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
|
|
4108
|
+
check(`skills.${skill}`, value);
|
|
4109
|
+
}
|
|
4110
|
+
for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
|
|
4111
|
+
check(`modes.${mode}`, value);
|
|
4112
|
+
}
|
|
3881
4113
|
if (missing.length > 0) {
|
|
3882
|
-
const detail = missing.map(({ path:
|
|
4114
|
+
const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
|
|
3883
4115
|
const known_ = [...known].join(", ") || "(none)";
|
|
3884
4116
|
throw new Error(
|
|
3885
4117
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3893,11 +4125,11 @@ var import_node_child_process4 = require("child_process");
|
|
|
3893
4125
|
var readline = __toESM(require("readline"));
|
|
3894
4126
|
var import_node_crypto3 = require("crypto");
|
|
3895
4127
|
var import_types10 = require("@harness-engineering/types");
|
|
3896
|
-
function resolveExitCode(code, command,
|
|
4128
|
+
function resolveExitCode(code, command, resolve8) {
|
|
3897
4129
|
if (code === 0) {
|
|
3898
|
-
|
|
4130
|
+
resolve8((0, import_types10.Ok)(void 0));
|
|
3899
4131
|
} else {
|
|
3900
|
-
|
|
4132
|
+
resolve8(
|
|
3901
4133
|
(0, import_types10.Err)({
|
|
3902
4134
|
category: "agent_not_found",
|
|
3903
4135
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3905,8 +4137,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3905
4137
|
);
|
|
3906
4138
|
}
|
|
3907
4139
|
}
|
|
3908
|
-
function resolveSpawnError(command,
|
|
3909
|
-
|
|
4140
|
+
function resolveSpawnError(command, resolve8) {
|
|
4141
|
+
resolve8((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3910
4142
|
}
|
|
3911
4143
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3912
4144
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4219,10 +4451,10 @@ var ClaudeBackend = class {
|
|
|
4219
4451
|
errRl.close();
|
|
4220
4452
|
}
|
|
4221
4453
|
if (exitCode === null) {
|
|
4222
|
-
await new Promise((
|
|
4454
|
+
await new Promise((resolve8) => {
|
|
4223
4455
|
child.on("exit", (code) => {
|
|
4224
4456
|
exitCode = code;
|
|
4225
|
-
|
|
4457
|
+
resolve8(null);
|
|
4226
4458
|
});
|
|
4227
4459
|
});
|
|
4228
4460
|
}
|
|
@@ -4244,10 +4476,10 @@ var ClaudeBackend = class {
|
|
|
4244
4476
|
return (0, import_types10.Ok)(void 0);
|
|
4245
4477
|
}
|
|
4246
4478
|
async healthCheck() {
|
|
4247
|
-
return new Promise((
|
|
4479
|
+
return new Promise((resolve8) => {
|
|
4248
4480
|
const child = (0, import_node_child_process4.spawn)(this.command, ["--version"]);
|
|
4249
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4250
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4481
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
|
|
4482
|
+
child.on("error", () => resolveSpawnError(this.command, resolve8));
|
|
4251
4483
|
});
|
|
4252
4484
|
}
|
|
4253
4485
|
};
|
|
@@ -4855,7 +5087,7 @@ var PiBackend = class {
|
|
|
4855
5087
|
} else {
|
|
4856
5088
|
resolvedModelName = this.config.model;
|
|
4857
5089
|
}
|
|
4858
|
-
const piSdk = await import("@
|
|
5090
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4859
5091
|
const model = buildLocalModel({
|
|
4860
5092
|
model: resolvedModelName,
|
|
4861
5093
|
endpoint: this.config.endpoint,
|
|
@@ -5010,7 +5242,7 @@ var PiBackend = class {
|
|
|
5010
5242
|
}
|
|
5011
5243
|
async healthCheck() {
|
|
5012
5244
|
try {
|
|
5013
|
-
await import("@
|
|
5245
|
+
await import("@earendil-works/pi-coding-agent");
|
|
5014
5246
|
return (0, import_types15.Ok)(void 0);
|
|
5015
5247
|
} catch (err) {
|
|
5016
5248
|
return (0, import_types15.Err)({
|
|
@@ -5161,14 +5393,14 @@ var SshBackend = class {
|
|
|
5161
5393
|
async healthCheck() {
|
|
5162
5394
|
const args = [...this.buildSshArgs()];
|
|
5163
5395
|
args[args.length - 1] = "true";
|
|
5164
|
-
return new Promise((
|
|
5396
|
+
return new Promise((resolve8) => {
|
|
5165
5397
|
let child;
|
|
5166
5398
|
try {
|
|
5167
5399
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5168
5400
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5169
5401
|
});
|
|
5170
5402
|
} catch (err) {
|
|
5171
|
-
|
|
5403
|
+
resolve8(
|
|
5172
5404
|
(0, import_types16.Err)({
|
|
5173
5405
|
category: "agent_not_found",
|
|
5174
5406
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5189,9 +5421,9 @@ var SshBackend = class {
|
|
|
5189
5421
|
child.on("close", (code) => {
|
|
5190
5422
|
clearTimeout(timer);
|
|
5191
5423
|
if (code === 0) {
|
|
5192
|
-
|
|
5424
|
+
resolve8((0, import_types16.Ok)(void 0));
|
|
5193
5425
|
} else {
|
|
5194
|
-
|
|
5426
|
+
resolve8(
|
|
5195
5427
|
(0, import_types16.Err)({
|
|
5196
5428
|
category: "agent_not_found",
|
|
5197
5429
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5201,7 +5433,7 @@ var SshBackend = class {
|
|
|
5201
5433
|
});
|
|
5202
5434
|
child.on("error", (err) => {
|
|
5203
5435
|
clearTimeout(timer);
|
|
5204
|
-
|
|
5436
|
+
resolve8((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
|
|
5205
5437
|
});
|
|
5206
5438
|
});
|
|
5207
5439
|
}
|
|
@@ -5249,13 +5481,13 @@ async function* readLines(stream) {
|
|
|
5249
5481
|
if (buffer.length > 0) yield buffer;
|
|
5250
5482
|
}
|
|
5251
5483
|
function waitForExit(child) {
|
|
5252
|
-
return new Promise((
|
|
5484
|
+
return new Promise((resolve8) => {
|
|
5253
5485
|
if (child.exitCode !== null) {
|
|
5254
|
-
|
|
5486
|
+
resolve8(child.exitCode);
|
|
5255
5487
|
return;
|
|
5256
5488
|
}
|
|
5257
|
-
child.once("close", (code) =>
|
|
5258
|
-
child.once("error", () =>
|
|
5489
|
+
child.once("close", (code) => resolve8(code));
|
|
5490
|
+
child.once("error", () => resolve8(null));
|
|
5259
5491
|
});
|
|
5260
5492
|
}
|
|
5261
5493
|
|
|
@@ -5442,14 +5674,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5442
5674
|
return out;
|
|
5443
5675
|
}
|
|
5444
5676
|
runOneShot(binary, args) {
|
|
5445
|
-
return new Promise((
|
|
5677
|
+
return new Promise((resolve8) => {
|
|
5446
5678
|
let child;
|
|
5447
5679
|
try {
|
|
5448
5680
|
child = this.spawnImpl(binary, args, {
|
|
5449
5681
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5450
5682
|
});
|
|
5451
5683
|
} catch (err) {
|
|
5452
|
-
|
|
5684
|
+
resolve8(
|
|
5453
5685
|
(0, import_types17.Err)({
|
|
5454
5686
|
category: "agent_not_found",
|
|
5455
5687
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5474,9 +5706,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5474
5706
|
child.on("close", (code) => {
|
|
5475
5707
|
clearTimeout(timer);
|
|
5476
5708
|
if (code === 0) {
|
|
5477
|
-
|
|
5709
|
+
resolve8((0, import_types17.Ok)(stdout));
|
|
5478
5710
|
} else {
|
|
5479
|
-
|
|
5711
|
+
resolve8(
|
|
5480
5712
|
(0, import_types17.Err)({
|
|
5481
5713
|
category: "response_error",
|
|
5482
5714
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5486,7 +5718,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5486
5718
|
});
|
|
5487
5719
|
child.on("error", (err) => {
|
|
5488
5720
|
clearTimeout(timer);
|
|
5489
|
-
|
|
5721
|
+
resolve8((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
|
|
5490
5722
|
});
|
|
5491
5723
|
});
|
|
5492
5724
|
}
|
|
@@ -5546,13 +5778,13 @@ async function* readLines2(stream) {
|
|
|
5546
5778
|
if (buffer.length > 0) yield buffer;
|
|
5547
5779
|
}
|
|
5548
5780
|
function waitForExit2(child) {
|
|
5549
|
-
return new Promise((
|
|
5781
|
+
return new Promise((resolve8) => {
|
|
5550
5782
|
if (child.exitCode !== null) {
|
|
5551
|
-
|
|
5783
|
+
resolve8(child.exitCode);
|
|
5552
5784
|
return;
|
|
5553
5785
|
}
|
|
5554
|
-
child.once("close", (code) =>
|
|
5555
|
-
child.once("error", () =>
|
|
5786
|
+
child.once("close", (code) => resolve8(code));
|
|
5787
|
+
child.once("error", () => resolve8(null));
|
|
5556
5788
|
});
|
|
5557
5789
|
}
|
|
5558
5790
|
|
|
@@ -5750,13 +5982,13 @@ var ContainerBackend = class {
|
|
|
5750
5982
|
var import_node_child_process7 = require("child_process");
|
|
5751
5983
|
var import_types19 = require("@harness-engineering/types");
|
|
5752
5984
|
function dockerExec(args) {
|
|
5753
|
-
return new Promise((
|
|
5985
|
+
return new Promise((resolve8, reject) => {
|
|
5754
5986
|
(0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
|
|
5755
5987
|
if (error) {
|
|
5756
5988
|
reject(error);
|
|
5757
5989
|
return;
|
|
5758
5990
|
}
|
|
5759
|
-
|
|
5991
|
+
resolve8(stdout.trim());
|
|
5760
5992
|
});
|
|
5761
5993
|
});
|
|
5762
5994
|
}
|
|
@@ -5815,11 +6047,11 @@ var DockerRuntime = class {
|
|
|
5815
6047
|
} finally {
|
|
5816
6048
|
rl.close();
|
|
5817
6049
|
}
|
|
5818
|
-
const exitCode = await new Promise((
|
|
6050
|
+
const exitCode = await new Promise((resolve8) => {
|
|
5819
6051
|
if (child.exitCode !== null) {
|
|
5820
|
-
|
|
6052
|
+
resolve8(child.exitCode);
|
|
5821
6053
|
} else {
|
|
5822
|
-
child.on("exit", (code) =>
|
|
6054
|
+
child.on("exit", (code) => resolve8(code ?? 1));
|
|
5823
6055
|
}
|
|
5824
6056
|
});
|
|
5825
6057
|
return exitCode;
|
|
@@ -5878,13 +6110,13 @@ var EnvSecretBackend = class {
|
|
|
5878
6110
|
var import_node_child_process8 = require("child_process");
|
|
5879
6111
|
var import_types21 = require("@harness-engineering/types");
|
|
5880
6112
|
function opExec(args) {
|
|
5881
|
-
return new Promise((
|
|
6113
|
+
return new Promise((resolve8, reject) => {
|
|
5882
6114
|
(0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
|
|
5883
6115
|
if (error) {
|
|
5884
6116
|
reject(error);
|
|
5885
6117
|
return;
|
|
5886
6118
|
}
|
|
5887
|
-
|
|
6119
|
+
resolve8(stdout.trim());
|
|
5888
6120
|
});
|
|
5889
6121
|
});
|
|
5890
6122
|
}
|
|
@@ -5927,13 +6159,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5927
6159
|
var import_node_child_process9 = require("child_process");
|
|
5928
6160
|
var import_types22 = require("@harness-engineering/types");
|
|
5929
6161
|
function vaultExec(args, env) {
|
|
5930
|
-
return new Promise((
|
|
6162
|
+
return new Promise((resolve8, reject) => {
|
|
5931
6163
|
(0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5932
6164
|
if (error) {
|
|
5933
6165
|
reject(error);
|
|
5934
6166
|
return;
|
|
5935
6167
|
}
|
|
5936
|
-
|
|
6168
|
+
resolve8(stdout.trim());
|
|
5937
6169
|
});
|
|
5938
6170
|
});
|
|
5939
6171
|
}
|
|
@@ -6008,7 +6240,11 @@ var OrchestratorBackendFactory = class {
|
|
|
6008
6240
|
opts;
|
|
6009
6241
|
constructor(opts) {
|
|
6010
6242
|
this.opts = opts;
|
|
6011
|
-
this.router = new BackendRouter({
|
|
6243
|
+
this.router = new BackendRouter({
|
|
6244
|
+
backends: opts.backends,
|
|
6245
|
+
routing: opts.routing,
|
|
6246
|
+
...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
|
|
6247
|
+
});
|
|
6012
6248
|
}
|
|
6013
6249
|
/**
|
|
6014
6250
|
* Resolve `useCase` to a backend name, materialize a fresh
|
|
@@ -6027,12 +6263,21 @@ var OrchestratorBackendFactory = class {
|
|
|
6027
6263
|
* is `undefined` for pure-modern configs. Threading the routed name
|
|
6028
6264
|
* through dispatch eliminates that gap.
|
|
6029
6265
|
*/
|
|
6030
|
-
resolveName(useCase) {
|
|
6031
|
-
return this.router.resolve(useCase);
|
|
6266
|
+
resolveName(useCase, opts) {
|
|
6267
|
+
return this.router.resolve(useCase, opts).backendName;
|
|
6032
6268
|
}
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6269
|
+
/**
|
|
6270
|
+
* Spec B Phase 1: expose the underlying router for callers that need
|
|
6271
|
+
* it directly (e.g., {@link buildIntelligencePipeline} for the
|
|
6272
|
+
* I1 SEL/PESL comparison fix). Read-only access; consumers must not
|
|
6273
|
+
* mutate router state.
|
|
6274
|
+
*/
|
|
6275
|
+
getRouter() {
|
|
6276
|
+
return this.router;
|
|
6277
|
+
}
|
|
6278
|
+
forUseCase(useCase, opts) {
|
|
6279
|
+
const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
|
|
6280
|
+
const name = decision.backendName;
|
|
6036
6281
|
let backend;
|
|
6037
6282
|
const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
|
|
6038
6283
|
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
@@ -6196,15 +6441,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
6196
6441
|
|
|
6197
6442
|
// src/agent/intelligence-factory.ts
|
|
6198
6443
|
function buildIntelligencePipeline(deps) {
|
|
6199
|
-
const { config } = deps;
|
|
6444
|
+
const { config, router } = deps;
|
|
6200
6445
|
const intel = config.intelligence;
|
|
6201
6446
|
if (!intel?.enabled) return null;
|
|
6202
6447
|
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
6203
6448
|
if (!selProvider) return null;
|
|
6204
|
-
const
|
|
6205
|
-
const
|
|
6206
|
-
const
|
|
6207
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6449
|
+
const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
|
|
6450
|
+
const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
|
|
6451
|
+
const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6208
6452
|
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
6209
6453
|
const graphStore = new import_graph.GraphStore();
|
|
6210
6454
|
const pipeline = new import_intelligence3.IntelligencePipeline(selProvider, graphStore, {
|
|
@@ -6221,7 +6465,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6221
6465
|
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
6222
6466
|
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
6223
6467
|
}
|
|
6224
|
-
const routed = resolveRoutedBackend(layer,
|
|
6468
|
+
const routed = resolveRoutedBackend(layer, deps);
|
|
6225
6469
|
if (!routed) return null;
|
|
6226
6470
|
const { name, def } = routed;
|
|
6227
6471
|
const resolver = localResolvers.get(name);
|
|
@@ -6246,20 +6490,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6246
6490
|
logger
|
|
6247
6491
|
});
|
|
6248
6492
|
}
|
|
6249
|
-
function resolveRoutedBackend(layer,
|
|
6250
|
-
const
|
|
6493
|
+
function resolveRoutedBackend(layer, deps) {
|
|
6494
|
+
const { config, router, logger } = deps;
|
|
6251
6495
|
const backends = config.agent.backends;
|
|
6252
|
-
if (!
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6496
|
+
if (!backends || !router) return null;
|
|
6497
|
+
try {
|
|
6498
|
+
const decision = router.resolve({ kind: "intelligence", layer });
|
|
6499
|
+
const def = backends[decision.backendName];
|
|
6500
|
+
if (!def) {
|
|
6501
|
+
logger.warn(
|
|
6502
|
+
`Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
|
|
6503
|
+
);
|
|
6504
|
+
return null;
|
|
6505
|
+
}
|
|
6506
|
+
return { name: decision.backendName, def };
|
|
6507
|
+
} catch (err) {
|
|
6257
6508
|
logger.warn(
|
|
6258
|
-
`Intelligence pipeline:
|
|
6509
|
+
`Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
|
|
6259
6510
|
);
|
|
6260
6511
|
return null;
|
|
6261
6512
|
}
|
|
6262
|
-
return { name, def };
|
|
6263
6513
|
}
|
|
6264
6514
|
function buildExplicitProvider(provider, selModel, config) {
|
|
6265
6515
|
if (provider.kind === "anthropic") {
|
|
@@ -6294,10 +6544,105 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6294
6544
|
});
|
|
6295
6545
|
}
|
|
6296
6546
|
|
|
6547
|
+
// src/routing/decision-bus.ts
|
|
6548
|
+
var RoutingDecisionBus = class {
|
|
6549
|
+
ringBuffer = [];
|
|
6550
|
+
listeners = /* @__PURE__ */ new Set();
|
|
6551
|
+
capacity;
|
|
6552
|
+
logger;
|
|
6553
|
+
constructor(opts) {
|
|
6554
|
+
this.capacity = opts?.capacity ?? 500;
|
|
6555
|
+
this.logger = opts?.logger;
|
|
6556
|
+
}
|
|
6557
|
+
emit(decision) {
|
|
6558
|
+
this.ringBuffer.push(decision);
|
|
6559
|
+
if (this.ringBuffer.length > this.capacity) {
|
|
6560
|
+
this.ringBuffer.shift();
|
|
6561
|
+
}
|
|
6562
|
+
if (this.logger) {
|
|
6563
|
+
this.logger.info("routing-decision", {
|
|
6564
|
+
useCase: decision.useCase,
|
|
6565
|
+
backendName: decision.backendName,
|
|
6566
|
+
resolutionPathLength: decision.resolutionPath.length,
|
|
6567
|
+
durationMs: decision.durationMs
|
|
6568
|
+
});
|
|
6569
|
+
}
|
|
6570
|
+
for (const listener of this.listeners) {
|
|
6571
|
+
try {
|
|
6572
|
+
listener(decision);
|
|
6573
|
+
} catch (err) {
|
|
6574
|
+
if (this.logger) {
|
|
6575
|
+
this.logger.warn("RoutingDecisionBus subscriber threw", {
|
|
6576
|
+
error: String(err)
|
|
6577
|
+
});
|
|
6578
|
+
}
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
}
|
|
6582
|
+
recent(filter) {
|
|
6583
|
+
let out = this.ringBuffer.slice();
|
|
6584
|
+
if (filter?.skillName !== void 0) {
|
|
6585
|
+
out = out.filter(
|
|
6586
|
+
(d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
|
|
6587
|
+
);
|
|
6588
|
+
}
|
|
6589
|
+
if (filter?.mode !== void 0) {
|
|
6590
|
+
const m = filter.mode;
|
|
6591
|
+
out = out.filter(
|
|
6592
|
+
(d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
|
|
6593
|
+
);
|
|
6594
|
+
}
|
|
6595
|
+
if (filter?.backendName !== void 0) {
|
|
6596
|
+
out = out.filter((d) => d.backendName === filter.backendName);
|
|
6597
|
+
}
|
|
6598
|
+
if (filter?.limit !== void 0) {
|
|
6599
|
+
out = out.slice(-filter.limit).reverse();
|
|
6600
|
+
} else {
|
|
6601
|
+
out = out.reverse();
|
|
6602
|
+
}
|
|
6603
|
+
return out;
|
|
6604
|
+
}
|
|
6605
|
+
subscribe(listener) {
|
|
6606
|
+
this.listeners.add(listener);
|
|
6607
|
+
return () => {
|
|
6608
|
+
this.listeners.delete(listener);
|
|
6609
|
+
};
|
|
6610
|
+
}
|
|
6611
|
+
/**
|
|
6612
|
+
* Spec B Phase 5 (review-S2 fix): release all subscriber references so
|
|
6613
|
+
* teardown can complete without anchoring closures. Called from
|
|
6614
|
+
* `Orchestrator.stop()` before nulling the bus reference. The bus
|
|
6615
|
+
* remains usable after clear — `subscribe()` works as normal.
|
|
6616
|
+
*/
|
|
6617
|
+
clearListeners() {
|
|
6618
|
+
this.listeners.clear();
|
|
6619
|
+
}
|
|
6620
|
+
};
|
|
6621
|
+
|
|
6622
|
+
// src/agent/triage-skill-mapping.ts
|
|
6623
|
+
function resolveSkillForTriage(triageSkill, catalog) {
|
|
6624
|
+
const expected = `harness-${triageSkill}`;
|
|
6625
|
+
const match = catalog.find((e) => e.name === expected);
|
|
6626
|
+
if (!match) return void 0;
|
|
6627
|
+
return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
|
|
6628
|
+
}
|
|
6629
|
+
|
|
6630
|
+
// src/agent/use-case-builder.ts
|
|
6631
|
+
function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
6632
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
6633
|
+
const decision = triageIssue(issue, {});
|
|
6634
|
+
const resolved = resolveSkillForTriage(decision.skill, catalog);
|
|
6635
|
+
if (resolved) {
|
|
6636
|
+
return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
|
|
6637
|
+
}
|
|
6638
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
6639
|
+
return { kind: "tier", tier };
|
|
6640
|
+
}
|
|
6641
|
+
|
|
6297
6642
|
// src/server/http.ts
|
|
6298
6643
|
var http = __toESM(require("http"));
|
|
6299
|
-
var
|
|
6300
|
-
var
|
|
6644
|
+
var path17 = __toESM(require("path"));
|
|
6645
|
+
var import_core11 = require("@harness-engineering/core");
|
|
6301
6646
|
|
|
6302
6647
|
// src/server/websocket.ts
|
|
6303
6648
|
var import_ws = require("ws");
|
|
@@ -6359,7 +6704,7 @@ var import_zod3 = require("zod");
|
|
|
6359
6704
|
// src/server/utils.ts
|
|
6360
6705
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6361
6706
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6362
|
-
return new Promise((
|
|
6707
|
+
return new Promise((resolve8, reject) => {
|
|
6363
6708
|
let body = "";
|
|
6364
6709
|
let bytes = 0;
|
|
6365
6710
|
req.on("data", (chunk) => {
|
|
@@ -6371,7 +6716,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6371
6716
|
}
|
|
6372
6717
|
body += String(chunk);
|
|
6373
6718
|
});
|
|
6374
|
-
req.on("end", () =>
|
|
6719
|
+
req.on("end", () => resolve8(body));
|
|
6375
6720
|
req.on("error", reject);
|
|
6376
6721
|
});
|
|
6377
6722
|
}
|
|
@@ -6492,8 +6837,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
|
6492
6837
|
|
|
6493
6838
|
// src/server/routes/plans.ts
|
|
6494
6839
|
var import_zod5 = require("zod");
|
|
6495
|
-
var
|
|
6496
|
-
var
|
|
6840
|
+
var fs10 = __toESM(require("fs/promises"));
|
|
6841
|
+
var path11 = __toESM(require("path"));
|
|
6497
6842
|
var PlanWriteSchema = import_zod5.z.object({
|
|
6498
6843
|
filename: import_zod5.z.string().min(1),
|
|
6499
6844
|
content: import_zod5.z.string().min(1)
|
|
@@ -6513,7 +6858,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6513
6858
|
return;
|
|
6514
6859
|
}
|
|
6515
6860
|
const parsed = result.data;
|
|
6516
|
-
const basename3 =
|
|
6861
|
+
const basename3 = path11.basename(parsed.filename);
|
|
6517
6862
|
if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
|
|
6518
6863
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6519
6864
|
res.end(
|
|
@@ -6521,9 +6866,9 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6521
6866
|
);
|
|
6522
6867
|
return;
|
|
6523
6868
|
}
|
|
6524
|
-
await
|
|
6525
|
-
const filePath =
|
|
6526
|
-
await
|
|
6869
|
+
await fs10.mkdir(plansDir, { recursive: true });
|
|
6870
|
+
const filePath = path11.join(plansDir, basename3);
|
|
6871
|
+
await fs10.writeFile(filePath, parsed.content, "utf-8");
|
|
6527
6872
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
6528
6873
|
res.end(JSON.stringify({ ok: true, filename: basename3 }));
|
|
6529
6874
|
} catch {
|
|
@@ -6898,8 +7243,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6898
7243
|
}
|
|
6899
7244
|
|
|
6900
7245
|
// src/server/routes/roadmap-actions.ts
|
|
6901
|
-
var
|
|
6902
|
-
var
|
|
7246
|
+
var fs11 = __toESM(require("fs/promises"));
|
|
7247
|
+
var path12 = __toESM(require("path"));
|
|
6903
7248
|
var import_core7 = require("@harness-engineering/core");
|
|
6904
7249
|
var import_zod8 = require("zod");
|
|
6905
7250
|
var AppendRoadmapRequestSchema = import_zod8.z.object({
|
|
@@ -6927,7 +7272,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6927
7272
|
sendJSON2(res, 503, { error: "Roadmap path not configured" });
|
|
6928
7273
|
return;
|
|
6929
7274
|
}
|
|
6930
|
-
const projectRoot =
|
|
7275
|
+
const projectRoot = path12.dirname(path12.dirname(roadmapPath));
|
|
6931
7276
|
const mode = (0, import_core7.loadProjectRoadmapMode)(projectRoot);
|
|
6932
7277
|
if (mode === "file-less") {
|
|
6933
7278
|
const trackerCfg = (0, import_core7.loadTrackerClientConfigFromProject)(projectRoot);
|
|
@@ -6980,7 +7325,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6980
7325
|
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
6981
7326
|
return;
|
|
6982
7327
|
}
|
|
6983
|
-
const content = await
|
|
7328
|
+
const content = await fs11.readFile(roadmapPath, "utf-8");
|
|
6984
7329
|
const roadmapResult = (0, import_core7.parseRoadmap)(content);
|
|
6985
7330
|
if (!roadmapResult.ok) {
|
|
6986
7331
|
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
@@ -7011,8 +7356,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
7011
7356
|
roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
|
|
7012
7357
|
const tmpPath = roadmapPath + ".tmp";
|
|
7013
7358
|
const serialized = (0, import_core7.serializeRoadmap)(roadmap);
|
|
7014
|
-
await
|
|
7015
|
-
await
|
|
7359
|
+
await fs11.writeFile(tmpPath, serialized, "utf-8");
|
|
7360
|
+
await fs11.rename(tmpPath, roadmapPath);
|
|
7016
7361
|
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
7017
7362
|
} catch (err) {
|
|
7018
7363
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
@@ -7499,85 +7844,761 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
7499
7844
|
return false;
|
|
7500
7845
|
}
|
|
7501
7846
|
|
|
7502
|
-
// src/server/routes/
|
|
7503
|
-
var fs11 = __toESM(require("fs/promises"));
|
|
7504
|
-
var path11 = __toESM(require("path"));
|
|
7847
|
+
// src/server/routes/v1/proposals.ts
|
|
7505
7848
|
var import_zod13 = require("zod");
|
|
7506
|
-
var
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7514
|
-
|
|
7515
|
-
|
|
7849
|
+
var import_core10 = require("@harness-engineering/core");
|
|
7850
|
+
var import_types24 = require("@harness-engineering/types");
|
|
7851
|
+
|
|
7852
|
+
// src/proposals/gate.ts
|
|
7853
|
+
var import_yaml3 = require("yaml");
|
|
7854
|
+
var import_core8 = require("@harness-engineering/core");
|
|
7855
|
+
var GateRunError = class extends Error {
|
|
7856
|
+
constructor(message) {
|
|
7857
|
+
super(message);
|
|
7858
|
+
this.name = "GateRunError";
|
|
7859
|
+
}
|
|
7860
|
+
};
|
|
7861
|
+
var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
7862
|
+
function checkSkillYaml(yaml) {
|
|
7863
|
+
const findings = [];
|
|
7864
|
+
let doc;
|
|
7865
|
+
try {
|
|
7866
|
+
doc = (0, import_yaml3.parse)(yaml);
|
|
7867
|
+
} catch (err) {
|
|
7868
|
+
findings.push({
|
|
7869
|
+
severity: "error",
|
|
7870
|
+
title: "skill.yaml does not parse",
|
|
7871
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
7872
|
+
});
|
|
7873
|
+
return findings;
|
|
7874
|
+
}
|
|
7875
|
+
if (!doc || typeof doc !== "object") {
|
|
7876
|
+
findings.push({
|
|
7877
|
+
severity: "error",
|
|
7878
|
+
title: "skill.yaml top-level is not a mapping",
|
|
7879
|
+
detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
|
|
7880
|
+
});
|
|
7881
|
+
return findings;
|
|
7882
|
+
}
|
|
7883
|
+
const obj = doc;
|
|
7884
|
+
if (typeof obj["name"] !== "string") {
|
|
7885
|
+
findings.push({
|
|
7886
|
+
severity: "error",
|
|
7887
|
+
title: "skill.yaml missing `name`",
|
|
7888
|
+
detail: "Every skill must declare its kebab-case name."
|
|
7889
|
+
});
|
|
7890
|
+
}
|
|
7891
|
+
if (typeof obj["version"] !== "string") {
|
|
7892
|
+
findings.push({
|
|
7893
|
+
severity: "error",
|
|
7894
|
+
title: "skill.yaml missing `version`",
|
|
7895
|
+
detail: "Every skill must declare a semver version string."
|
|
7896
|
+
});
|
|
7897
|
+
}
|
|
7898
|
+
if (typeof obj["description"] !== "string") {
|
|
7899
|
+
findings.push({
|
|
7900
|
+
severity: "warning",
|
|
7901
|
+
title: "skill.yaml missing `description`",
|
|
7902
|
+
detail: "Description is strongly recommended for discoverability."
|
|
7903
|
+
});
|
|
7904
|
+
}
|
|
7905
|
+
return findings;
|
|
7516
7906
|
}
|
|
7517
|
-
function
|
|
7518
|
-
const
|
|
7519
|
-
|
|
7520
|
-
|
|
7907
|
+
function checkSkillMd(md) {
|
|
7908
|
+
const findings = [];
|
|
7909
|
+
if (md.trim().length < 40) {
|
|
7910
|
+
findings.push({
|
|
7911
|
+
severity: "error",
|
|
7912
|
+
title: "SKILL.md is too short",
|
|
7913
|
+
detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
|
|
7914
|
+
});
|
|
7915
|
+
}
|
|
7916
|
+
if (!/^#\s+\S/m.test(md)) {
|
|
7917
|
+
findings.push({
|
|
7918
|
+
severity: "warning",
|
|
7919
|
+
title: "SKILL.md has no top-level heading",
|
|
7920
|
+
detail: "Convention: open SKILL.md with `# <Skill Name>`."
|
|
7921
|
+
});
|
|
7922
|
+
}
|
|
7923
|
+
return findings;
|
|
7521
7924
|
}
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
const content = await fs11.readFile(
|
|
7530
|
-
path11.join(sessionsDir, entry.name, "session.json"),
|
|
7531
|
-
"utf-8"
|
|
7532
|
-
);
|
|
7533
|
-
sessions.push(JSON.parse(content));
|
|
7534
|
-
} catch {
|
|
7535
|
-
}
|
|
7925
|
+
function checkName(name) {
|
|
7926
|
+
if (SKILL_NAME_RE.test(name)) return [];
|
|
7927
|
+
return [
|
|
7928
|
+
{
|
|
7929
|
+
severity: "error",
|
|
7930
|
+
title: "skill name violates the kebab-case rule",
|
|
7931
|
+
detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
|
|
7536
7932
|
}
|
|
7537
|
-
|
|
7538
|
-
|
|
7933
|
+
];
|
|
7934
|
+
}
|
|
7935
|
+
function checkDiff(diff) {
|
|
7936
|
+
const findings = [];
|
|
7937
|
+
if (!diff.includes("---") || !diff.includes("+++")) {
|
|
7938
|
+
findings.push({
|
|
7939
|
+
severity: "error",
|
|
7940
|
+
title: "Refinement diff is not in unified-diff format",
|
|
7941
|
+
detail: "Diffs must include both `---` and `+++` headers."
|
|
7942
|
+
});
|
|
7943
|
+
}
|
|
7944
|
+
if (!/^@@\s/m.test(diff)) {
|
|
7945
|
+
findings.push({
|
|
7946
|
+
severity: "warning",
|
|
7947
|
+
title: "Refinement diff has no hunk marker",
|
|
7948
|
+
detail: "A unified diff typically contains at least one `@@` line."
|
|
7949
|
+
});
|
|
7950
|
+
}
|
|
7951
|
+
return findings;
|
|
7952
|
+
}
|
|
7953
|
+
function deriveFindings(proposal) {
|
|
7954
|
+
const findings = [];
|
|
7955
|
+
findings.push(...checkName(proposal.content.name));
|
|
7956
|
+
if (proposal.kind === "new-skill") {
|
|
7957
|
+
findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
|
|
7958
|
+
findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
|
|
7959
|
+
} else if (proposal.kind === "refinement") {
|
|
7960
|
+
findings.push(...checkDiff(proposal.content.diff ?? ""));
|
|
7961
|
+
}
|
|
7962
|
+
return findings;
|
|
7963
|
+
}
|
|
7964
|
+
async function runGate(projectPath, proposalId) {
|
|
7965
|
+
const proposal = await (0, import_core8.getProposal)(projectPath, proposalId);
|
|
7966
|
+
if (!proposal) throw new import_core8.ProposalNotFoundError(proposalId);
|
|
7967
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7968
|
+
throw new GateRunError(
|
|
7969
|
+
`proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
|
|
7539
7970
|
);
|
|
7540
|
-
jsonResponse(res, 200, sessions);
|
|
7541
|
-
} catch (err) {
|
|
7542
|
-
if (err.code === "ENOENT") {
|
|
7543
|
-
jsonResponse(res, 200, []);
|
|
7544
|
-
return;
|
|
7545
|
-
}
|
|
7546
|
-
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
7547
7971
|
}
|
|
7972
|
+
const findings = deriveFindings(proposal);
|
|
7973
|
+
const runAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7974
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
7975
|
+
const nextStatus = hasError ? "gate-failed" : "gate-running";
|
|
7976
|
+
const updated = await (0, import_core8.updateProposal)(projectPath, proposalId, {
|
|
7977
|
+
status: nextStatus,
|
|
7978
|
+
gate: { lastRunAt: runAt, findings }
|
|
7979
|
+
});
|
|
7980
|
+
return {
|
|
7981
|
+
proposalId: updated.id,
|
|
7982
|
+
status: updated.status,
|
|
7983
|
+
findings,
|
|
7984
|
+
runAt
|
|
7985
|
+
};
|
|
7548
7986
|
}
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
7552
|
-
|
|
7987
|
+
|
|
7988
|
+
// src/proposals/promote.ts
|
|
7989
|
+
var fs12 = __toESM(require("fs"));
|
|
7990
|
+
var path13 = __toESM(require("path"));
|
|
7991
|
+
var import_yaml4 = require("yaml");
|
|
7992
|
+
var import_core9 = require("@harness-engineering/core");
|
|
7993
|
+
var GateNotReadyError = class extends Error {
|
|
7994
|
+
constructor(message) {
|
|
7995
|
+
super(message);
|
|
7996
|
+
this.name = "GateNotReadyError";
|
|
7997
|
+
}
|
|
7998
|
+
};
|
|
7999
|
+
var PromotionError = class extends Error {
|
|
8000
|
+
constructor(message) {
|
|
8001
|
+
super(message);
|
|
8002
|
+
this.name = "PromotionError";
|
|
7553
8003
|
}
|
|
8004
|
+
};
|
|
8005
|
+
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
8006
|
+
function skillDir(projectPath, name) {
|
|
8007
|
+
return path13.join(projectPath, "agents", "skills", "claude-code", name);
|
|
8008
|
+
}
|
|
8009
|
+
function readIfExists(p) {
|
|
7554
8010
|
try {
|
|
7555
|
-
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
if (err.code === "ENOENT") {
|
|
7559
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
7560
|
-
return;
|
|
7561
|
-
}
|
|
7562
|
-
jsonResponse(res, 500, { error: "Failed to read session" });
|
|
8011
|
+
return fs12.readFileSync(p, "utf-8");
|
|
8012
|
+
} catch {
|
|
8013
|
+
return null;
|
|
7563
8014
|
}
|
|
7564
8015
|
}
|
|
7565
|
-
|
|
8016
|
+
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
8017
|
+
let doc;
|
|
7566
8018
|
try {
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
8019
|
+
doc = (0, import_yaml4.parse)(yamlText);
|
|
8020
|
+
} catch (err) {
|
|
8021
|
+
throw new PromotionError(
|
|
8022
|
+
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
8023
|
+
);
|
|
8024
|
+
}
|
|
8025
|
+
if (!doc || typeof doc !== "object") {
|
|
8026
|
+
throw new PromotionError("skill.yaml top-level is not a mapping");
|
|
8027
|
+
}
|
|
8028
|
+
const obj = doc;
|
|
8029
|
+
obj["provenance"] = "agent-proposed";
|
|
8030
|
+
obj["originatingProposalId"] = proposalId;
|
|
8031
|
+
return (0, import_yaml4.stringify)(obj);
|
|
8032
|
+
}
|
|
8033
|
+
function assertGateReady(proposal) {
|
|
8034
|
+
if (proposal.status !== "gate-running") {
|
|
8035
|
+
throw new GateNotReadyError(
|
|
8036
|
+
`proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
|
|
8037
|
+
);
|
|
8038
|
+
}
|
|
8039
|
+
const findings = proposal.gate?.findings ?? [];
|
|
8040
|
+
if (findings.some((f) => f.severity === "error")) {
|
|
8041
|
+
throw new GateNotReadyError(
|
|
8042
|
+
`proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
|
|
8043
|
+
);
|
|
8044
|
+
}
|
|
8045
|
+
if (!proposal.gate?.lastRunAt) {
|
|
8046
|
+
throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
|
|
8047
|
+
}
|
|
8048
|
+
const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
|
|
8049
|
+
if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
|
|
8050
|
+
throw new GateNotReadyError(
|
|
8051
|
+
`proposal ${proposal.id} gate run is older than 24h; re-run before approving`
|
|
8052
|
+
);
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
async function promoteNewSkill(projectPath, proposal) {
|
|
8056
|
+
const target = skillDir(projectPath, proposal.content.name);
|
|
8057
|
+
if (fs12.existsSync(target)) {
|
|
8058
|
+
throw new PromotionError(
|
|
8059
|
+
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
8060
|
+
);
|
|
8061
|
+
}
|
|
8062
|
+
fs12.mkdirSync(target, { recursive: true });
|
|
8063
|
+
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
8064
|
+
fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
|
|
8065
|
+
fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
8066
|
+
return { skillPath: target };
|
|
8067
|
+
}
|
|
8068
|
+
async function promoteRefinement(projectPath, proposal) {
|
|
8069
|
+
if (!proposal.targetSkill) {
|
|
8070
|
+
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
8071
|
+
}
|
|
8072
|
+
const target = skillDir(projectPath, proposal.targetSkill);
|
|
8073
|
+
if (!fs12.existsSync(target)) {
|
|
8074
|
+
throw new PromotionError(
|
|
8075
|
+
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
8076
|
+
);
|
|
8077
|
+
}
|
|
8078
|
+
const yamlPath = path13.join(target, "skill.yaml");
|
|
8079
|
+
const before = readIfExists(yamlPath) ?? "";
|
|
8080
|
+
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
8081
|
+
if (after === before) {
|
|
8082
|
+
throw new PromotionError(
|
|
8083
|
+
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
8084
|
+
);
|
|
8085
|
+
}
|
|
8086
|
+
fs12.writeFileSync(yamlPath, after);
|
|
8087
|
+
return { skillPath: target };
|
|
8088
|
+
}
|
|
8089
|
+
async function promote(projectPath, proposalId, decidedBy) {
|
|
8090
|
+
const proposal = await (0, import_core9.getProposal)(projectPath, proposalId);
|
|
8091
|
+
if (!proposal) throw new import_core9.ProposalNotFoundError(proposalId);
|
|
8092
|
+
assertGateReady(proposal);
|
|
8093
|
+
const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
|
|
8094
|
+
await (0, import_core9.updateProposal)(projectPath, proposalId, {
|
|
8095
|
+
status: "approved",
|
|
8096
|
+
decision: {
|
|
8097
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8098
|
+
decidedBy,
|
|
8099
|
+
action: "approved"
|
|
8100
|
+
}
|
|
8101
|
+
});
|
|
8102
|
+
return {
|
|
8103
|
+
proposalId,
|
|
8104
|
+
skillPath: out.skillPath,
|
|
8105
|
+
provenance: "agent-proposed"
|
|
8106
|
+
};
|
|
8107
|
+
}
|
|
8108
|
+
|
|
8109
|
+
// src/proposals/events.ts
|
|
8110
|
+
function emit3(bus, topic, data) {
|
|
8111
|
+
bus.emit(topic, data);
|
|
8112
|
+
}
|
|
8113
|
+
function emitProposalCreated(bus, proposal) {
|
|
8114
|
+
const data = {
|
|
8115
|
+
id: proposal.id,
|
|
8116
|
+
kind: proposal.kind,
|
|
8117
|
+
name: proposal.content.name,
|
|
8118
|
+
proposedBy: proposal.proposedBy,
|
|
8119
|
+
justification: proposal.source.justification
|
|
8120
|
+
};
|
|
8121
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
8122
|
+
emit3(bus, "proposal.created", data);
|
|
8123
|
+
}
|
|
8124
|
+
function emitProposalApproved(bus, proposal) {
|
|
8125
|
+
const data = {
|
|
8126
|
+
id: proposal.id,
|
|
8127
|
+
kind: proposal.kind,
|
|
8128
|
+
name: proposal.content.name,
|
|
8129
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
|
|
8130
|
+
};
|
|
8131
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
8132
|
+
emit3(bus, "proposal.approved", data);
|
|
8133
|
+
}
|
|
8134
|
+
function emitProposalRejected(bus, proposal) {
|
|
8135
|
+
const data = {
|
|
8136
|
+
id: proposal.id,
|
|
8137
|
+
kind: proposal.kind,
|
|
8138
|
+
name: proposal.content.name,
|
|
8139
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
|
|
8140
|
+
reason: proposal.decision?.reason ?? "(no reason given)"
|
|
8141
|
+
};
|
|
8142
|
+
emit3(bus, "proposal.rejected", data);
|
|
8143
|
+
}
|
|
8144
|
+
|
|
8145
|
+
// src/server/routes/v1/proposals.ts
|
|
8146
|
+
var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
|
|
8147
|
+
var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
|
|
8148
|
+
var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
|
|
8149
|
+
var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
|
|
8150
|
+
var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
|
|
8151
|
+
var ProposalStatusValues = [
|
|
8152
|
+
"open",
|
|
8153
|
+
"gate-running",
|
|
8154
|
+
"gate-failed",
|
|
8155
|
+
"approved",
|
|
8156
|
+
"rejected"
|
|
8157
|
+
];
|
|
8158
|
+
var RejectBody = import_zod13.z.object({
|
|
8159
|
+
reason: import_zod13.z.string().min(1).max(280)
|
|
8160
|
+
});
|
|
8161
|
+
function sendJSON8(res, status, body) {
|
|
8162
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8163
|
+
res.end(JSON.stringify(body));
|
|
8164
|
+
}
|
|
8165
|
+
function getDecidedBy(req, deps) {
|
|
8166
|
+
if (deps.decidedByResolver) return deps.decidedByResolver(req);
|
|
8167
|
+
const token = req._authToken;
|
|
8168
|
+
return token?.id ?? "unknown";
|
|
8169
|
+
}
|
|
8170
|
+
function parseStatusFromQuery(url) {
|
|
8171
|
+
const queryIdx = url.indexOf("?");
|
|
8172
|
+
if (queryIdx === -1) return void 0;
|
|
8173
|
+
const params = new URLSearchParams(url.slice(queryIdx + 1));
|
|
8174
|
+
const raw = params.get("status");
|
|
8175
|
+
if (!raw) return void 0;
|
|
8176
|
+
if (raw === "all") return "all";
|
|
8177
|
+
if (ProposalStatusValues.includes(raw)) return raw;
|
|
8178
|
+
return void 0;
|
|
8179
|
+
}
|
|
8180
|
+
async function handleList(req, res, deps) {
|
|
8181
|
+
const url = req.url ?? "";
|
|
8182
|
+
const status = parseStatusFromQuery(url);
|
|
8183
|
+
const proposals = await (0, import_core10.listProposals)(deps.projectPath, status ? { status } : {});
|
|
8184
|
+
sendJSON8(res, 200, proposals);
|
|
8185
|
+
}
|
|
8186
|
+
async function handleGet(res, deps, id) {
|
|
8187
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
8188
|
+
if (!proposal) {
|
|
8189
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8190
|
+
return;
|
|
8191
|
+
}
|
|
8192
|
+
sendJSON8(res, 200, proposal);
|
|
8193
|
+
}
|
|
8194
|
+
async function handleRunGate(res, deps, id) {
|
|
8195
|
+
try {
|
|
8196
|
+
const result = await runGate(deps.projectPath, id);
|
|
8197
|
+
sendJSON8(res, 200, result);
|
|
8198
|
+
} catch (err) {
|
|
8199
|
+
if (err instanceof import_core10.ProposalNotFoundError) {
|
|
8200
|
+
sendJSON8(res, 404, { error: err.message });
|
|
8201
|
+
return;
|
|
8202
|
+
}
|
|
8203
|
+
if (err instanceof GateRunError) {
|
|
8204
|
+
sendJSON8(res, 409, { error: err.message });
|
|
8205
|
+
return;
|
|
8206
|
+
}
|
|
8207
|
+
sendJSON8(res, 500, {
|
|
8208
|
+
error: "gate run failed",
|
|
8209
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8210
|
+
});
|
|
8211
|
+
}
|
|
8212
|
+
}
|
|
8213
|
+
async function handleApprove(req, res, deps, id) {
|
|
8214
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
8215
|
+
try {
|
|
8216
|
+
const result = await promote(deps.projectPath, id, decidedBy);
|
|
8217
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
8218
|
+
if (proposal) emitProposalApproved(deps.bus, proposal);
|
|
8219
|
+
sendJSON8(res, 200, { promotion: result, proposal });
|
|
8220
|
+
} catch (err) {
|
|
8221
|
+
if (err instanceof import_core10.ProposalNotFoundError) {
|
|
8222
|
+
sendJSON8(res, 404, { error: err.message });
|
|
8223
|
+
return;
|
|
8224
|
+
}
|
|
8225
|
+
if (err instanceof GateNotReadyError) {
|
|
8226
|
+
sendJSON8(res, 409, { error: err.message });
|
|
8227
|
+
return;
|
|
8228
|
+
}
|
|
8229
|
+
if (err instanceof PromotionError) {
|
|
8230
|
+
sendJSON8(res, 422, { error: err.message });
|
|
8231
|
+
return;
|
|
8232
|
+
}
|
|
8233
|
+
sendJSON8(res, 500, {
|
|
8234
|
+
error: "approve failed",
|
|
8235
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8236
|
+
});
|
|
8237
|
+
}
|
|
8238
|
+
}
|
|
8239
|
+
async function handleReject(req, res, deps, id) {
|
|
8240
|
+
let raw;
|
|
8241
|
+
try {
|
|
8242
|
+
raw = await readBody(req);
|
|
8243
|
+
} catch (err) {
|
|
8244
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
8245
|
+
return;
|
|
8246
|
+
}
|
|
8247
|
+
let json;
|
|
8248
|
+
try {
|
|
8249
|
+
json = raw.length > 0 ? JSON.parse(raw) : {};
|
|
8250
|
+
} catch {
|
|
8251
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
8252
|
+
return;
|
|
8253
|
+
}
|
|
8254
|
+
const parsed = RejectBody.safeParse(json);
|
|
8255
|
+
if (!parsed.success) {
|
|
8256
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
8257
|
+
return;
|
|
8258
|
+
}
|
|
8259
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
8260
|
+
if (!proposal) {
|
|
8261
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8262
|
+
return;
|
|
8263
|
+
}
|
|
8264
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
8265
|
+
sendJSON8(res, 409, {
|
|
8266
|
+
error: `proposal already ${proposal.status}; cannot reject`
|
|
8267
|
+
});
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
8271
|
+
const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
|
|
8272
|
+
status: "rejected",
|
|
8273
|
+
decision: {
|
|
8274
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8275
|
+
decidedBy,
|
|
8276
|
+
action: "rejected",
|
|
8277
|
+
reason: parsed.data.reason
|
|
8278
|
+
}
|
|
8279
|
+
});
|
|
8280
|
+
emitProposalRejected(deps.bus, updated);
|
|
8281
|
+
sendJSON8(res, 200, updated);
|
|
8282
|
+
}
|
|
8283
|
+
async function handleEdit(req, res, deps, id) {
|
|
8284
|
+
let raw;
|
|
8285
|
+
try {
|
|
8286
|
+
raw = await readBody(req);
|
|
8287
|
+
} catch (err) {
|
|
8288
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
8289
|
+
return;
|
|
8290
|
+
}
|
|
8291
|
+
let json;
|
|
8292
|
+
try {
|
|
8293
|
+
json = JSON.parse(raw);
|
|
8294
|
+
} catch {
|
|
8295
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
8296
|
+
return;
|
|
8297
|
+
}
|
|
8298
|
+
const parsed = import_types24.EditProposalInputSchema.safeParse(json);
|
|
8299
|
+
if (!parsed.success) {
|
|
8300
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
8301
|
+
return;
|
|
8302
|
+
}
|
|
8303
|
+
const existing = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
8304
|
+
if (!existing) {
|
|
8305
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8306
|
+
return;
|
|
8307
|
+
}
|
|
8308
|
+
if (existing.status === "approved" || existing.status === "rejected") {
|
|
8309
|
+
sendJSON8(res, 409, {
|
|
8310
|
+
error: `proposal already ${existing.status}; cannot edit`
|
|
8311
|
+
});
|
|
8312
|
+
return;
|
|
8313
|
+
}
|
|
8314
|
+
const mergedContent = {
|
|
8315
|
+
...existing.content,
|
|
8316
|
+
...parsed.data.content,
|
|
8317
|
+
name: parsed.data.content.name ?? existing.content.name,
|
|
8318
|
+
description: parsed.data.content.description ?? existing.content.description
|
|
8319
|
+
};
|
|
8320
|
+
try {
|
|
8321
|
+
const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
|
|
8322
|
+
content: mergedContent,
|
|
8323
|
+
status: "open",
|
|
8324
|
+
gate: void 0
|
|
8325
|
+
});
|
|
8326
|
+
sendJSON8(res, 200, updated);
|
|
8327
|
+
} catch (err) {
|
|
8328
|
+
sendJSON8(res, 422, {
|
|
8329
|
+
error: "edit failed",
|
|
8330
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8331
|
+
});
|
|
8332
|
+
}
|
|
8333
|
+
}
|
|
8334
|
+
function handleV1ProposalsRoute(req, res, deps) {
|
|
8335
|
+
const url = req.url ?? "";
|
|
8336
|
+
const method = req.method ?? "GET";
|
|
8337
|
+
if (method === "GET" && LIST_RE.test(url)) {
|
|
8338
|
+
void handleList(req, res, deps);
|
|
8339
|
+
return true;
|
|
8340
|
+
}
|
|
8341
|
+
const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
|
|
8342
|
+
if (runGateMatch) {
|
|
8343
|
+
void handleRunGate(res, deps, runGateMatch[1]);
|
|
8344
|
+
return true;
|
|
8345
|
+
}
|
|
8346
|
+
const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
|
|
8347
|
+
if (approveMatch) {
|
|
8348
|
+
void handleApprove(req, res, deps, approveMatch[1]);
|
|
8349
|
+
return true;
|
|
8350
|
+
}
|
|
8351
|
+
const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
|
|
8352
|
+
if (rejectMatch) {
|
|
8353
|
+
void handleReject(req, res, deps, rejectMatch[1]);
|
|
8354
|
+
return true;
|
|
8355
|
+
}
|
|
8356
|
+
if (method === "PATCH") {
|
|
8357
|
+
const m = SINGLE_RE.exec(url);
|
|
8358
|
+
if (m) {
|
|
8359
|
+
void handleEdit(req, res, deps, m[1]);
|
|
8360
|
+
return true;
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
if (method === "GET") {
|
|
8364
|
+
const m = SINGLE_RE.exec(url);
|
|
8365
|
+
if (m) {
|
|
8366
|
+
void handleGet(res, deps, m[1]);
|
|
8367
|
+
return true;
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
return false;
|
|
8371
|
+
}
|
|
8372
|
+
|
|
8373
|
+
// src/server/routes/v1/routing.ts
|
|
8374
|
+
var import_zod14 = require("zod");
|
|
8375
|
+
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
8376
|
+
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
8377
|
+
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
8378
|
+
function sendJSON9(res, status, body) {
|
|
8379
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8380
|
+
res.end(JSON.stringify(body));
|
|
8381
|
+
}
|
|
8382
|
+
function unavailable(res) {
|
|
8383
|
+
sendJSON9(res, 503, { error: "BackendRouter not available" });
|
|
8384
|
+
return true;
|
|
8385
|
+
}
|
|
8386
|
+
function resolveChain(value, backends) {
|
|
8387
|
+
return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
|
|
8388
|
+
}
|
|
8389
|
+
function buildResolvedChains(routing, backends) {
|
|
8390
|
+
const out = {};
|
|
8391
|
+
out["default"] = resolveChain(routing.default, backends);
|
|
8392
|
+
for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
|
|
8393
|
+
const v = routing[tier];
|
|
8394
|
+
if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
|
|
8395
|
+
}
|
|
8396
|
+
if (routing.intelligence) {
|
|
8397
|
+
for (const [layer, v] of Object.entries(routing.intelligence)) {
|
|
8398
|
+
if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
|
|
8399
|
+
}
|
|
8400
|
+
}
|
|
8401
|
+
if (routing.isolation) {
|
|
8402
|
+
for (const [tier, v] of Object.entries(routing.isolation)) {
|
|
8403
|
+
if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
if (routing.skills) {
|
|
8407
|
+
for (const [name, v] of Object.entries(routing.skills)) {
|
|
8408
|
+
if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
|
|
8409
|
+
}
|
|
8410
|
+
}
|
|
8411
|
+
if (routing.modes) {
|
|
8412
|
+
for (const [mode, v] of Object.entries(routing.modes)) {
|
|
8413
|
+
if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
|
|
8414
|
+
}
|
|
8415
|
+
}
|
|
8416
|
+
return out;
|
|
8417
|
+
}
|
|
8418
|
+
function handleConfig(res, deps) {
|
|
8419
|
+
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
8420
|
+
sendJSON9(res, 200, {
|
|
8421
|
+
routing: deps.routing,
|
|
8422
|
+
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
8423
|
+
backends: Object.keys(deps.backends)
|
|
8424
|
+
});
|
|
8425
|
+
return true;
|
|
8426
|
+
}
|
|
8427
|
+
function parseDecisionsQuery(url) {
|
|
8428
|
+
const qIdx = url.indexOf("?");
|
|
8429
|
+
if (qIdx === -1) return {};
|
|
8430
|
+
const p = new URLSearchParams(url.slice(qIdx + 1));
|
|
8431
|
+
const filter = {};
|
|
8432
|
+
const skill = p.get("skill");
|
|
8433
|
+
const mode = p.get("mode");
|
|
8434
|
+
const backend = p.get("backend");
|
|
8435
|
+
const limit = p.get("limit");
|
|
8436
|
+
if (skill) filter.skillName = skill;
|
|
8437
|
+
if (mode) filter.mode = mode;
|
|
8438
|
+
if (backend) filter.backendName = backend;
|
|
8439
|
+
if (limit) {
|
|
8440
|
+
const n = Number(limit);
|
|
8441
|
+
if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
|
|
8442
|
+
}
|
|
8443
|
+
return filter;
|
|
8444
|
+
}
|
|
8445
|
+
function handleDecisions(req, res, deps) {
|
|
8446
|
+
if (!deps.bus) return unavailable(res);
|
|
8447
|
+
const filter = parseDecisionsQuery(req.url ?? "");
|
|
8448
|
+
sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
|
|
8449
|
+
return true;
|
|
8450
|
+
}
|
|
8451
|
+
var UseCaseSchema = import_zod14.z.discriminatedUnion("kind", [
|
|
8452
|
+
import_zod14.z.object({
|
|
8453
|
+
kind: import_zod14.z.literal("tier"),
|
|
8454
|
+
tier: import_zod14.z.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
|
|
8455
|
+
}),
|
|
8456
|
+
import_zod14.z.object({ kind: import_zod14.z.literal("intelligence"), layer: import_zod14.z.enum(["sel", "pesl"]) }),
|
|
8457
|
+
import_zod14.z.object({ kind: import_zod14.z.literal("isolation"), tier: import_zod14.z.string() }),
|
|
8458
|
+
import_zod14.z.object({ kind: import_zod14.z.literal("maintenance") }),
|
|
8459
|
+
import_zod14.z.object({ kind: import_zod14.z.literal("chat") }),
|
|
8460
|
+
import_zod14.z.object({
|
|
8461
|
+
kind: import_zod14.z.literal("skill"),
|
|
8462
|
+
skillName: import_zod14.z.string().min(1),
|
|
8463
|
+
cognitiveMode: import_zod14.z.string().optional()
|
|
8464
|
+
}),
|
|
8465
|
+
import_zod14.z.object({ kind: import_zod14.z.literal("mode"), cognitiveMode: import_zod14.z.string().min(1) })
|
|
8466
|
+
]);
|
|
8467
|
+
var TraceBodySchema = import_zod14.z.object({
|
|
8468
|
+
useCase: UseCaseSchema,
|
|
8469
|
+
invocationOverride: import_zod14.z.string().min(1).optional()
|
|
8470
|
+
});
|
|
8471
|
+
async function handleTrace(req, res, deps) {
|
|
8472
|
+
if (!deps.routing || !deps.backends) {
|
|
8473
|
+
unavailable(res);
|
|
8474
|
+
return true;
|
|
8475
|
+
}
|
|
8476
|
+
let raw;
|
|
8477
|
+
try {
|
|
8478
|
+
raw = await readBody(req);
|
|
8479
|
+
} catch {
|
|
8480
|
+
sendJSON9(res, 400, { error: "body read failed" });
|
|
8481
|
+
return true;
|
|
8482
|
+
}
|
|
8483
|
+
let parsed;
|
|
8484
|
+
try {
|
|
8485
|
+
parsed = JSON.parse(raw);
|
|
8486
|
+
} catch {
|
|
8487
|
+
sendJSON9(res, 400, { error: "invalid JSON body" });
|
|
8488
|
+
return true;
|
|
8489
|
+
}
|
|
8490
|
+
const r = TraceBodySchema.safeParse(parsed);
|
|
8491
|
+
if (!r.success) {
|
|
8492
|
+
sendJSON9(res, 400, { error: r.error.message });
|
|
8493
|
+
return true;
|
|
8494
|
+
}
|
|
8495
|
+
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
8496
|
+
try {
|
|
8497
|
+
const dryRunRouter = new BackendRouter({
|
|
8498
|
+
backends: deps.backends,
|
|
8499
|
+
routing: deps.routing
|
|
8500
|
+
});
|
|
8501
|
+
const { decision, def } = dryRunRouter.resolveDecisionAndDef(
|
|
8502
|
+
r.data.useCase,
|
|
8503
|
+
opts
|
|
8504
|
+
);
|
|
8505
|
+
sendJSON9(res, 200, { decision, def: { type: def.type } });
|
|
8506
|
+
} catch (err) {
|
|
8507
|
+
sendJSON9(res, 500, { error: String(err) });
|
|
8508
|
+
}
|
|
8509
|
+
return true;
|
|
8510
|
+
}
|
|
8511
|
+
function handleV1RoutingRoute(req, res, deps) {
|
|
8512
|
+
const url = req.url ?? "";
|
|
8513
|
+
const method = req.method ?? "GET";
|
|
8514
|
+
if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
|
|
8515
|
+
if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
|
|
8516
|
+
if (method === "POST" && TRACE_RE.test(url)) {
|
|
8517
|
+
void handleTrace(req, res, deps);
|
|
8518
|
+
return true;
|
|
8519
|
+
}
|
|
8520
|
+
return false;
|
|
8521
|
+
}
|
|
8522
|
+
|
|
8523
|
+
// src/server/routes/sessions.ts
|
|
8524
|
+
var fs13 = __toESM(require("fs/promises"));
|
|
8525
|
+
var path14 = __toESM(require("path"));
|
|
8526
|
+
var import_zod15 = require("zod");
|
|
8527
|
+
var SessionCreateSchema = import_zod15.z.object({
|
|
8528
|
+
sessionId: import_zod15.z.string().min(1)
|
|
8529
|
+
}).passthrough();
|
|
8530
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8531
|
+
function isSafeId(id) {
|
|
8532
|
+
return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
|
|
8533
|
+
}
|
|
8534
|
+
function jsonResponse(res, status, data) {
|
|
8535
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8536
|
+
res.end(JSON.stringify(data));
|
|
8537
|
+
}
|
|
8538
|
+
function extractSessionId(url) {
|
|
8539
|
+
const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
|
|
8540
|
+
const id = segments.pop();
|
|
8541
|
+
return id && id !== "sessions" ? id : null;
|
|
8542
|
+
}
|
|
8543
|
+
async function handleList2(res, sessionsDir) {
|
|
8544
|
+
try {
|
|
8545
|
+
const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
|
|
8546
|
+
const sessions = [];
|
|
8547
|
+
for (const entry of entries) {
|
|
8548
|
+
if (!entry.isDirectory()) continue;
|
|
8549
|
+
try {
|
|
8550
|
+
const content = await fs13.readFile(
|
|
8551
|
+
path14.join(sessionsDir, entry.name, "session.json"),
|
|
8552
|
+
"utf-8"
|
|
8553
|
+
);
|
|
8554
|
+
sessions.push(JSON.parse(content));
|
|
8555
|
+
} catch {
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8558
|
+
sessions.sort(
|
|
8559
|
+
(a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime()
|
|
8560
|
+
);
|
|
8561
|
+
jsonResponse(res, 200, sessions);
|
|
8562
|
+
} catch (err) {
|
|
8563
|
+
if (err.code === "ENOENT") {
|
|
8564
|
+
jsonResponse(res, 200, []);
|
|
8565
|
+
return;
|
|
8566
|
+
}
|
|
8567
|
+
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
8571
|
+
if (!isSafeId(id)) {
|
|
8572
|
+
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
8573
|
+
return;
|
|
8574
|
+
}
|
|
8575
|
+
try {
|
|
8576
|
+
const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
|
|
8577
|
+
jsonResponse(res, 200, JSON.parse(content));
|
|
8578
|
+
} catch (err) {
|
|
8579
|
+
if (err.code === "ENOENT") {
|
|
8580
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
8581
|
+
return;
|
|
8582
|
+
}
|
|
8583
|
+
jsonResponse(res, 500, { error: "Failed to read session" });
|
|
8584
|
+
}
|
|
8585
|
+
}
|
|
8586
|
+
async function handleCreate(req, res, sessionsDir) {
|
|
8587
|
+
try {
|
|
8588
|
+
const body = await readBody(req);
|
|
8589
|
+
const result = SessionCreateSchema.safeParse(JSON.parse(body));
|
|
8590
|
+
if (!result.success) {
|
|
8591
|
+
jsonResponse(res, 400, { error: "Missing sessionId" });
|
|
8592
|
+
return;
|
|
8593
|
+
}
|
|
8594
|
+
const session = result.data;
|
|
8595
|
+
if (!isSafeId(session.sessionId)) {
|
|
7575
8596
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7576
8597
|
return;
|
|
7577
8598
|
}
|
|
7578
|
-
const sessionDir =
|
|
7579
|
-
await
|
|
7580
|
-
await
|
|
8599
|
+
const sessionDir = path14.join(sessionsDir, session.sessionId);
|
|
8600
|
+
await fs13.mkdir(sessionDir, { recursive: true });
|
|
8601
|
+
await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
7581
8602
|
jsonResponse(res, 200, { ok: true });
|
|
7582
8603
|
} catch {
|
|
7583
8604
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -7591,10 +8612,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
7591
8612
|
return;
|
|
7592
8613
|
}
|
|
7593
8614
|
const body = await readBody(req);
|
|
7594
|
-
const updates =
|
|
7595
|
-
const sessionFilePath =
|
|
7596
|
-
const current = JSON.parse(await
|
|
7597
|
-
await
|
|
8615
|
+
const updates = import_zod15.z.record(import_zod15.z.unknown()).parse(JSON.parse(body));
|
|
8616
|
+
const sessionFilePath = path14.join(sessionsDir, id, "session.json");
|
|
8617
|
+
const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
|
|
8618
|
+
await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
7598
8619
|
jsonResponse(res, 200, { ok: true });
|
|
7599
8620
|
} catch {
|
|
7600
8621
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -7607,7 +8628,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
7607
8628
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
7608
8629
|
return;
|
|
7609
8630
|
}
|
|
7610
|
-
await
|
|
8631
|
+
await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
|
|
7611
8632
|
jsonResponse(res, 200, { ok: true });
|
|
7612
8633
|
} catch {
|
|
7613
8634
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7620,8 +8641,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7620
8641
|
switch (method) {
|
|
7621
8642
|
case "GET": {
|
|
7622
8643
|
const id = extractSessionId(url);
|
|
7623
|
-
if (id) void
|
|
7624
|
-
else void
|
|
8644
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8645
|
+
else void handleList2(res, sessionsDir);
|
|
7625
8646
|
return true;
|
|
7626
8647
|
}
|
|
7627
8648
|
case "POST":
|
|
@@ -7711,16 +8732,16 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7711
8732
|
}
|
|
7712
8733
|
|
|
7713
8734
|
// src/server/routes/auth.ts
|
|
7714
|
-
var
|
|
7715
|
-
var
|
|
7716
|
-
var CreateBodySchema =
|
|
7717
|
-
name:
|
|
7718
|
-
scopes:
|
|
7719
|
-
bridgeKind:
|
|
7720
|
-
tenantId:
|
|
7721
|
-
expiresAt:
|
|
8735
|
+
var import_zod16 = require("zod");
|
|
8736
|
+
var import_types25 = require("@harness-engineering/types");
|
|
8737
|
+
var CreateBodySchema = import_zod16.z.object({
|
|
8738
|
+
name: import_zod16.z.string().min(1).max(100),
|
|
8739
|
+
scopes: import_zod16.z.array(import_types25.TokenScopeSchema).min(1),
|
|
8740
|
+
bridgeKind: import_types25.BridgeKindSchema.optional(),
|
|
8741
|
+
tenantId: import_zod16.z.string().optional(),
|
|
8742
|
+
expiresAt: import_zod16.z.string().datetime().optional()
|
|
7722
8743
|
});
|
|
7723
|
-
function
|
|
8744
|
+
function sendJSON10(res, status, body) {
|
|
7724
8745
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7725
8746
|
res.end(JSON.stringify(body));
|
|
7726
8747
|
}
|
|
@@ -7730,19 +8751,19 @@ async function handlePost(req, res, store) {
|
|
|
7730
8751
|
raw = await readBody(req);
|
|
7731
8752
|
} catch (err) {
|
|
7732
8753
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7733
|
-
|
|
8754
|
+
sendJSON10(res, 413, { error: msg });
|
|
7734
8755
|
return;
|
|
7735
8756
|
}
|
|
7736
8757
|
let json;
|
|
7737
8758
|
try {
|
|
7738
8759
|
json = JSON.parse(raw);
|
|
7739
8760
|
} catch {
|
|
7740
|
-
|
|
8761
|
+
sendJSON10(res, 400, { error: "Invalid JSON body" });
|
|
7741
8762
|
return;
|
|
7742
8763
|
}
|
|
7743
8764
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7744
8765
|
if (!parsed.success) {
|
|
7745
|
-
|
|
8766
|
+
sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7746
8767
|
return;
|
|
7747
8768
|
}
|
|
7748
8769
|
try {
|
|
@@ -7754,38 +8775,38 @@ async function handlePost(req, res, store) {
|
|
|
7754
8775
|
if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
|
|
7755
8776
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7756
8777
|
const result = await store.create(input);
|
|
7757
|
-
const publicRecord =
|
|
7758
|
-
|
|
8778
|
+
const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
|
|
8779
|
+
sendJSON10(res, 200, {
|
|
7759
8780
|
...publicRecord,
|
|
7760
8781
|
token: result.token
|
|
7761
8782
|
});
|
|
7762
8783
|
} catch (err) {
|
|
7763
8784
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7764
8785
|
if (msg.includes("already exists")) {
|
|
7765
|
-
|
|
8786
|
+
sendJSON10(res, 409, { error: msg });
|
|
7766
8787
|
return;
|
|
7767
8788
|
}
|
|
7768
|
-
|
|
8789
|
+
sendJSON10(res, 500, { error: "Internal error creating token" });
|
|
7769
8790
|
}
|
|
7770
8791
|
}
|
|
7771
|
-
async function
|
|
8792
|
+
async function handleList3(res, store) {
|
|
7772
8793
|
try {
|
|
7773
8794
|
const list = await store.list();
|
|
7774
|
-
|
|
8795
|
+
sendJSON10(res, 200, list);
|
|
7775
8796
|
} catch {
|
|
7776
|
-
|
|
8797
|
+
sendJSON10(res, 500, { error: "Internal error listing tokens" });
|
|
7777
8798
|
}
|
|
7778
8799
|
}
|
|
7779
8800
|
async function handleDelete2(res, store, id) {
|
|
7780
8801
|
try {
|
|
7781
8802
|
const ok = await store.revoke(id);
|
|
7782
8803
|
if (!ok) {
|
|
7783
|
-
|
|
8804
|
+
sendJSON10(res, 404, { error: "Token not found" });
|
|
7784
8805
|
return;
|
|
7785
8806
|
}
|
|
7786
|
-
|
|
8807
|
+
sendJSON10(res, 200, { deleted: true });
|
|
7787
8808
|
} catch {
|
|
7788
|
-
|
|
8809
|
+
sendJSON10(res, 500, { error: "Internal error revoking token" });
|
|
7789
8810
|
}
|
|
7790
8811
|
}
|
|
7791
8812
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7799,7 +8820,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7799
8820
|
return true;
|
|
7800
8821
|
}
|
|
7801
8822
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7802
|
-
void
|
|
8823
|
+
void handleList3(res, store);
|
|
7803
8824
|
return true;
|
|
7804
8825
|
}
|
|
7805
8826
|
if (method === "DELETE") {
|
|
@@ -7810,12 +8831,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7810
8831
|
return true;
|
|
7811
8832
|
}
|
|
7812
8833
|
}
|
|
7813
|
-
|
|
8834
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7814
8835
|
return true;
|
|
7815
8836
|
}
|
|
7816
8837
|
|
|
7817
8838
|
// src/server/routes/local-model.ts
|
|
7818
|
-
function
|
|
8839
|
+
function sendJSON11(res, status, body) {
|
|
7819
8840
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7820
8841
|
res.end(JSON.stringify(body));
|
|
7821
8842
|
}
|
|
@@ -7823,36 +8844,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7823
8844
|
const { method, url } = req;
|
|
7824
8845
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7825
8846
|
if (method !== "GET") {
|
|
7826
|
-
|
|
8847
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
7827
8848
|
return true;
|
|
7828
8849
|
}
|
|
7829
8850
|
if (!getStatus) {
|
|
7830
|
-
|
|
8851
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
7831
8852
|
return true;
|
|
7832
8853
|
}
|
|
7833
8854
|
const status = getStatus();
|
|
7834
8855
|
if (!status) {
|
|
7835
|
-
|
|
8856
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
7836
8857
|
return true;
|
|
7837
8858
|
}
|
|
7838
|
-
|
|
8859
|
+
sendJSON11(res, 200, status);
|
|
7839
8860
|
return true;
|
|
7840
8861
|
}
|
|
7841
8862
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7842
8863
|
const { method, url } = req;
|
|
7843
8864
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7844
8865
|
if (method !== "GET") {
|
|
7845
|
-
|
|
8866
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
7846
8867
|
return true;
|
|
7847
8868
|
}
|
|
7848
8869
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7849
|
-
|
|
8870
|
+
sendJSON11(res, 200, statuses);
|
|
7850
8871
|
return true;
|
|
7851
8872
|
}
|
|
7852
8873
|
|
|
7853
8874
|
// src/server/static.ts
|
|
7854
|
-
var
|
|
7855
|
-
var
|
|
8875
|
+
var fs14 = __toESM(require("fs"));
|
|
8876
|
+
var path15 = __toESM(require("path"));
|
|
7856
8877
|
var MIME_TYPES = {
|
|
7857
8878
|
".html": "text/html; charset=utf-8",
|
|
7858
8879
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7872,29 +8893,29 @@ var MIME_TYPES = {
|
|
|
7872
8893
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7873
8894
|
const { method, url } = req;
|
|
7874
8895
|
if (method !== "GET") return false;
|
|
7875
|
-
const apiPrefix =
|
|
7876
|
-
const wsPath =
|
|
8896
|
+
const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
|
|
8897
|
+
const wsPath = path15.posix.join(path15.posix.sep, "ws");
|
|
7877
8898
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7878
8899
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7879
|
-
const requestedPath =
|
|
7880
|
-
const resolved =
|
|
7881
|
-
if (!resolved.startsWith(
|
|
7882
|
-
return serveFile(
|
|
8900
|
+
const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8901
|
+
const resolved = path15.resolve(requestedPath);
|
|
8902
|
+
if (!resolved.startsWith(path15.resolve(dashboardDir))) {
|
|
8903
|
+
return serveFile(path15.join(dashboardDir, "index.html"), res);
|
|
7883
8904
|
}
|
|
7884
|
-
if (
|
|
8905
|
+
if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
|
|
7885
8906
|
return serveFile(resolved, res);
|
|
7886
8907
|
}
|
|
7887
|
-
const indexPath =
|
|
7888
|
-
if (
|
|
8908
|
+
const indexPath = path15.join(dashboardDir, "index.html");
|
|
8909
|
+
if (fs14.existsSync(indexPath)) {
|
|
7889
8910
|
return serveFile(indexPath, res);
|
|
7890
8911
|
}
|
|
7891
8912
|
return false;
|
|
7892
8913
|
}
|
|
7893
8914
|
function serveFile(filePath, res) {
|
|
7894
|
-
const ext =
|
|
8915
|
+
const ext = path15.extname(filePath).toLowerCase();
|
|
7895
8916
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7896
8917
|
try {
|
|
7897
|
-
const content =
|
|
8918
|
+
const content = fs14.readFileSync(filePath);
|
|
7898
8919
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7899
8920
|
res.end(content);
|
|
7900
8921
|
return true;
|
|
@@ -7904,8 +8925,8 @@ function serveFile(filePath, res) {
|
|
|
7904
8925
|
}
|
|
7905
8926
|
|
|
7906
8927
|
// src/server/plan-watcher.ts
|
|
7907
|
-
var
|
|
7908
|
-
var
|
|
8928
|
+
var fs15 = __toESM(require("fs"));
|
|
8929
|
+
var path16 = __toESM(require("path"));
|
|
7909
8930
|
var PlanWatcher = class {
|
|
7910
8931
|
plansDir;
|
|
7911
8932
|
queue;
|
|
@@ -7919,11 +8940,11 @@ var PlanWatcher = class {
|
|
|
7919
8940
|
* Creates the directory if it does not exist.
|
|
7920
8941
|
*/
|
|
7921
8942
|
start() {
|
|
7922
|
-
|
|
7923
|
-
this.watcher =
|
|
8943
|
+
fs15.mkdirSync(this.plansDir, { recursive: true });
|
|
8944
|
+
this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
|
|
7924
8945
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7925
|
-
const filePath =
|
|
7926
|
-
if (
|
|
8946
|
+
const filePath = path16.join(this.plansDir, filename);
|
|
8947
|
+
if (fs15.existsSync(filePath)) {
|
|
7927
8948
|
void this.handleNewPlan(filename);
|
|
7928
8949
|
}
|
|
7929
8950
|
}
|
|
@@ -7958,7 +8979,7 @@ var import_node_crypto9 = require("crypto");
|
|
|
7958
8979
|
var import_promises = require("fs/promises");
|
|
7959
8980
|
var import_node_path = require("path");
|
|
7960
8981
|
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
7961
|
-
var
|
|
8982
|
+
var import_types26 = require("@harness-engineering/types");
|
|
7962
8983
|
var BCRYPT_ROUNDS = 12;
|
|
7963
8984
|
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
7964
8985
|
function genId() {
|
|
@@ -7973,8 +8994,8 @@ function parseToken(raw) {
|
|
|
7973
8994
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7974
8995
|
}
|
|
7975
8996
|
var TokenStore = class {
|
|
7976
|
-
constructor(
|
|
7977
|
-
this.path =
|
|
8997
|
+
constructor(path24) {
|
|
8998
|
+
this.path = path24;
|
|
7978
8999
|
}
|
|
7979
9000
|
path;
|
|
7980
9001
|
cache = null;
|
|
@@ -7985,7 +9006,7 @@ var TokenStore = class {
|
|
|
7985
9006
|
const parsed = JSON.parse(raw);
|
|
7986
9007
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
7987
9008
|
this.cache = list.map((entry) => {
|
|
7988
|
-
const r =
|
|
9009
|
+
const r = import_types26.AuthTokenSchema.safeParse(entry);
|
|
7989
9010
|
return r.success ? r.data : null;
|
|
7990
9011
|
}).filter((x) => x !== null);
|
|
7991
9012
|
} catch (err) {
|
|
@@ -8047,7 +9068,7 @@ var TokenStore = class {
|
|
|
8047
9068
|
}
|
|
8048
9069
|
async list() {
|
|
8049
9070
|
const records = await this.load();
|
|
8050
|
-
return records.map((r) =>
|
|
9071
|
+
return records.map((r) => import_types26.AuthTokenPublicSchema.parse(r));
|
|
8051
9072
|
}
|
|
8052
9073
|
async revoke(id) {
|
|
8053
9074
|
const records = await this.load();
|
|
@@ -8079,10 +9100,10 @@ var TokenStore = class {
|
|
|
8079
9100
|
// src/auth/audit.ts
|
|
8080
9101
|
var import_promises2 = require("fs/promises");
|
|
8081
9102
|
var import_node_path2 = require("path");
|
|
8082
|
-
var
|
|
9103
|
+
var import_types27 = require("@harness-engineering/types");
|
|
8083
9104
|
var AuditLogger = class {
|
|
8084
|
-
constructor(
|
|
8085
|
-
this.path =
|
|
9105
|
+
constructor(path24, opts = {}) {
|
|
9106
|
+
this.path = path24;
|
|
8086
9107
|
this.opts = opts;
|
|
8087
9108
|
}
|
|
8088
9109
|
path;
|
|
@@ -8090,7 +9111,7 @@ var AuditLogger = class {
|
|
|
8090
9111
|
queue = Promise.resolve();
|
|
8091
9112
|
dirEnsured = false;
|
|
8092
9113
|
async append(input) {
|
|
8093
|
-
const entry =
|
|
9114
|
+
const entry = import_types27.AuthAuditEntrySchema.parse({
|
|
8094
9115
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8095
9116
|
tokenId: input.tokenId,
|
|
8096
9117
|
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
@@ -8166,20 +9187,79 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8166
9187
|
scope: "subscribe-webhook",
|
|
8167
9188
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
8168
9189
|
},
|
|
9190
|
+
// Hermes Phase 4 — skill proposal review queue.
|
|
9191
|
+
{
|
|
9192
|
+
method: "GET",
|
|
9193
|
+
pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
|
|
9194
|
+
scope: "read-status",
|
|
9195
|
+
description: "List skill proposals (open + decided)."
|
|
9196
|
+
},
|
|
9197
|
+
{
|
|
9198
|
+
method: "GET",
|
|
9199
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
9200
|
+
scope: "read-status",
|
|
9201
|
+
description: "Get a single skill proposal."
|
|
9202
|
+
},
|
|
9203
|
+
{
|
|
9204
|
+
method: "POST",
|
|
9205
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
|
|
9206
|
+
scope: "manage-proposals",
|
|
9207
|
+
description: "Run the soundness-review gate against a proposal."
|
|
9208
|
+
},
|
|
9209
|
+
{
|
|
9210
|
+
method: "POST",
|
|
9211
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
|
|
9212
|
+
scope: "manage-proposals",
|
|
9213
|
+
description: "Approve a proposal \u2014 promotes the skill into the catalog."
|
|
9214
|
+
},
|
|
9215
|
+
{
|
|
9216
|
+
method: "POST",
|
|
9217
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
|
|
9218
|
+
scope: "manage-proposals",
|
|
9219
|
+
description: "Reject a proposal with a one-line reason."
|
|
9220
|
+
},
|
|
9221
|
+
{
|
|
9222
|
+
method: "PATCH",
|
|
9223
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
9224
|
+
scope: "manage-proposals",
|
|
9225
|
+
description: "Edit proposal content (resets gate to not-run)."
|
|
9226
|
+
},
|
|
8169
9227
|
// ── Phase 5 bridge primitives ──
|
|
8170
9228
|
{
|
|
8171
9229
|
method: "GET",
|
|
8172
9230
|
pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
|
|
8173
9231
|
scope: "read-telemetry",
|
|
8174
9232
|
description: "Prompt-cache hit/miss snapshot (rolling window)."
|
|
9233
|
+
},
|
|
9234
|
+
// ── Spec B Phase 5 routing observability ──
|
|
9235
|
+
// D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
|
|
9236
|
+
// precedent (read-only observability). A dedicated `read-routing`
|
|
9237
|
+
// scope was rejected to avoid a TokenScopeSchema + ADR cascade.
|
|
9238
|
+
{
|
|
9239
|
+
method: "GET",
|
|
9240
|
+
pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
|
|
9241
|
+
scope: "read-telemetry",
|
|
9242
|
+
description: "Current routing config + resolved fallback chains + known backends."
|
|
9243
|
+
},
|
|
9244
|
+
{
|
|
9245
|
+
method: "GET",
|
|
9246
|
+
pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
|
|
9247
|
+
scope: "read-telemetry",
|
|
9248
|
+
description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
|
|
9249
|
+
},
|
|
9250
|
+
{
|
|
9251
|
+
method: "POST",
|
|
9252
|
+
pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
|
|
9253
|
+
scope: "read-telemetry",
|
|
9254
|
+
description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
|
|
8175
9255
|
}
|
|
8176
9256
|
];
|
|
8177
9257
|
function isV1Bridge(method, url) {
|
|
8178
9258
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8179
9259
|
}
|
|
8180
|
-
function requiredBridgeScope(method,
|
|
9260
|
+
function requiredBridgeScope(method, path24) {
|
|
8181
9261
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8182
|
-
if (r.method === method && r.pattern.test(
|
|
9262
|
+
if (r.method === method && r.pattern.test(path24)) return r.scope;
|
|
8183
9263
|
}
|
|
8184
9264
|
return null;
|
|
8185
9265
|
}
|
|
@@ -8189,24 +9269,24 @@ function hasScope(held, required) {
|
|
|
8189
9269
|
if (held.includes("admin")) return true;
|
|
8190
9270
|
return held.includes(required);
|
|
8191
9271
|
}
|
|
8192
|
-
function requiredScopeForRoute(method,
|
|
8193
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
9272
|
+
function requiredScopeForRoute(method, path24) {
|
|
9273
|
+
const bridgeScope = requiredBridgeScope(method, path24);
|
|
8194
9274
|
if (bridgeScope) return bridgeScope;
|
|
8195
|
-
if (
|
|
8196
|
-
if (
|
|
8197
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
8198
|
-
if ((
|
|
8199
|
-
if (
|
|
8200
|
-
if (
|
|
8201
|
-
if (
|
|
8202
|
-
if (
|
|
8203
|
-
if (
|
|
8204
|
-
if (
|
|
9275
|
+
if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
9276
|
+
if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
9277
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
|
|
9278
|
+
if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
|
|
9279
|
+
if (path24.startsWith("/api/interactions")) return "resolve-interaction";
|
|
9280
|
+
if (path24.startsWith("/api/plans")) return "read-status";
|
|
9281
|
+
if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
|
|
9282
|
+
if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
9283
|
+
if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
9284
|
+
if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
|
|
8205
9285
|
return "read-status";
|
|
8206
|
-
if (
|
|
8207
|
-
if (
|
|
8208
|
-
if (
|
|
8209
|
-
if (
|
|
9286
|
+
if (path24.startsWith("/api/maintenance")) return "trigger-job";
|
|
9287
|
+
if (path24.startsWith("/api/streams")) return "read-status";
|
|
9288
|
+
if (path24.startsWith("/api/sessions")) return "read-status";
|
|
9289
|
+
if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
8210
9290
|
return null;
|
|
8211
9291
|
}
|
|
8212
9292
|
|
|
@@ -8260,11 +9340,25 @@ var OrchestratorServer = class {
|
|
|
8260
9340
|
roadmapPath;
|
|
8261
9341
|
dispatchAdHoc;
|
|
8262
9342
|
sessionsDir;
|
|
9343
|
+
/**
|
|
9344
|
+
* Project root used by file-backed routes (Phase 4 proposals at
|
|
9345
|
+
* `.harness/proposals/`). Defaults to process.cwd().
|
|
9346
|
+
*/
|
|
9347
|
+
projectPath;
|
|
8263
9348
|
maintenanceDeps = null;
|
|
8264
9349
|
getLocalModelStatus = null;
|
|
8265
9350
|
getLocalModelStatuses = null;
|
|
8266
9351
|
webhooks;
|
|
8267
9352
|
cacheMetrics;
|
|
9353
|
+
// Spec B Phase 5 — routing observability accessor closures + the WS
|
|
9354
|
+
// broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
|
|
9355
|
+
// calls it explicitly; clearListeners in Orchestrator.stop() is the
|
|
9356
|
+
// belt-and-suspenders second line).
|
|
9357
|
+
getBackendRouterFn = null;
|
|
9358
|
+
getRoutingDecisionBusFn = null;
|
|
9359
|
+
getRoutingConfigFn = null;
|
|
9360
|
+
getBackendsFn = null;
|
|
9361
|
+
routingDecisionUnsubscribe = null;
|
|
8268
9362
|
recorder = null;
|
|
8269
9363
|
planWatcher = null;
|
|
8270
9364
|
tokenStore;
|
|
@@ -8277,8 +9371,8 @@ var OrchestratorServer = class {
|
|
|
8277
9371
|
this.orchestrator = orchestrator;
|
|
8278
9372
|
this.port = port;
|
|
8279
9373
|
this.initDependencies(deps);
|
|
8280
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8281
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
9374
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
|
|
9375
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
|
|
8282
9376
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8283
9377
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8284
9378
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8291,19 +9385,24 @@ var OrchestratorServer = class {
|
|
|
8291
9385
|
}
|
|
8292
9386
|
initDependencies(deps) {
|
|
8293
9387
|
this.interactionQueue = deps?.interactionQueue;
|
|
8294
|
-
this.plansDir = deps?.plansDir ??
|
|
8295
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
9388
|
+
this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
|
|
9389
|
+
this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
|
|
8296
9390
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8297
9391
|
this.pipeline = deps?.pipeline ?? null;
|
|
8298
9392
|
this.analysisArchive = deps?.analysisArchive;
|
|
8299
9393
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8300
9394
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8301
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
9395
|
+
this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
|
|
9396
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8302
9397
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8303
9398
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8304
9399
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
8305
9400
|
this.webhooks = deps?.webhooks;
|
|
8306
9401
|
this.cacheMetrics = deps?.cacheMetrics;
|
|
9402
|
+
this.getBackendRouterFn = deps?.getBackendRouter ?? null;
|
|
9403
|
+
this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
|
|
9404
|
+
this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
|
|
9405
|
+
this.getBackendsFn = deps?.getBackends ?? null;
|
|
8307
9406
|
}
|
|
8308
9407
|
wireEvents() {
|
|
8309
9408
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -8314,6 +9413,12 @@ var OrchestratorServer = class {
|
|
|
8314
9413
|
};
|
|
8315
9414
|
this.orchestrator.on("state_change", this.stateChangeListener);
|
|
8316
9415
|
this.orchestrator.on("agent_event", this.agentEventListener);
|
|
9416
|
+
const bus = this.getRoutingDecisionBusFn?.() ?? null;
|
|
9417
|
+
if (bus) {
|
|
9418
|
+
this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
|
|
9419
|
+
this.broadcaster.broadcast("routing:decision", decision);
|
|
9420
|
+
});
|
|
9421
|
+
}
|
|
8317
9422
|
}
|
|
8318
9423
|
/**
|
|
8319
9424
|
* Broadcast a new interaction to all WebSocket clients.
|
|
@@ -8469,6 +9574,23 @@ var OrchestratorServer = class {
|
|
|
8469
9574
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
8470
9575
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
8471
9576
|
}),
|
|
9577
|
+
// Spec B Phase 5 — routing observability. Returns 503 when the
|
|
9578
|
+
// backendFactory is null (legacy single-backend configs).
|
|
9579
|
+
(req, res) => handleV1RoutingRoute(req, res, {
|
|
9580
|
+
router: this.getBackendRouterFn?.() ?? null,
|
|
9581
|
+
bus: this.getRoutingDecisionBusFn?.() ?? null,
|
|
9582
|
+
routing: this.getRoutingConfigFn?.() ?? null,
|
|
9583
|
+
backends: this.getBackendsFn?.() ?? null
|
|
9584
|
+
}),
|
|
9585
|
+
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9586
|
+
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9587
|
+
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
9588
|
+
// business logic. `projectPath` defaults to process.cwd() — that is
|
|
9589
|
+
// where `.harness/proposals/` lives in every deployment we ship.
|
|
9590
|
+
(req, res) => handleV1ProposalsRoute(req, res, {
|
|
9591
|
+
projectPath: this.projectPath,
|
|
9592
|
+
bus: this.orchestrator
|
|
9593
|
+
}),
|
|
8472
9594
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
8473
9595
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
8474
9596
|
];
|
|
@@ -8551,22 +9673,26 @@ var OrchestratorServer = class {
|
|
|
8551
9673
|
return this.broadcaster.clientCount;
|
|
8552
9674
|
}
|
|
8553
9675
|
async start() {
|
|
8554
|
-
(0,
|
|
9676
|
+
(0, import_core11.assertPortUsable)(this.port, "orchestrator");
|
|
8555
9677
|
if (this.interactionQueue) {
|
|
8556
9678
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
8557
9679
|
this.planWatcher.start();
|
|
8558
9680
|
}
|
|
8559
|
-
return new Promise((
|
|
9681
|
+
return new Promise((resolve8) => {
|
|
8560
9682
|
const host = getBindHost();
|
|
8561
9683
|
this.httpServer.listen(this.port, host, () => {
|
|
8562
9684
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
8563
|
-
|
|
9685
|
+
resolve8();
|
|
8564
9686
|
});
|
|
8565
9687
|
});
|
|
8566
9688
|
}
|
|
8567
9689
|
stop() {
|
|
8568
9690
|
this.orchestrator.removeListener("state_change", this.stateChangeListener);
|
|
8569
9691
|
this.orchestrator.removeListener("agent_event", this.agentEventListener);
|
|
9692
|
+
if (this.routingDecisionUnsubscribe) {
|
|
9693
|
+
this.routingDecisionUnsubscribe();
|
|
9694
|
+
this.routingDecisionUnsubscribe = null;
|
|
9695
|
+
}
|
|
8570
9696
|
if (this.planWatcher) {
|
|
8571
9697
|
this.planWatcher.stop();
|
|
8572
9698
|
this.planWatcher = null;
|
|
@@ -8580,7 +9706,7 @@ var OrchestratorServer = class {
|
|
|
8580
9706
|
var import_node_crypto11 = require("crypto");
|
|
8581
9707
|
var import_promises3 = require("fs/promises");
|
|
8582
9708
|
var import_node_path3 = require("path");
|
|
8583
|
-
var
|
|
9709
|
+
var import_types28 = require("@harness-engineering/types");
|
|
8584
9710
|
|
|
8585
9711
|
// src/gateway/webhooks/signer.ts
|
|
8586
9712
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8610,8 +9736,8 @@ function genSecret2() {
|
|
|
8610
9736
|
return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
|
|
8611
9737
|
}
|
|
8612
9738
|
var WebhookStore = class {
|
|
8613
|
-
constructor(
|
|
8614
|
-
this.path =
|
|
9739
|
+
constructor(path24) {
|
|
9740
|
+
this.path = path24;
|
|
8615
9741
|
}
|
|
8616
9742
|
path;
|
|
8617
9743
|
cache = null;
|
|
@@ -8622,7 +9748,7 @@ var WebhookStore = class {
|
|
|
8622
9748
|
const parsed = JSON.parse(raw);
|
|
8623
9749
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
8624
9750
|
this.cache = list.map((entry) => {
|
|
8625
|
-
const r =
|
|
9751
|
+
const r = import_types28.WebhookSubscriptionSchema.safeParse(entry);
|
|
8626
9752
|
return r.success ? r.data : null;
|
|
8627
9753
|
}).filter((x) => x !== null);
|
|
8628
9754
|
} catch (err) {
|
|
@@ -9002,7 +10128,12 @@ var WEBHOOK_TOPICS = [
|
|
|
9002
10128
|
"maintenance:completed",
|
|
9003
10129
|
"maintenance:error",
|
|
9004
10130
|
"webhook.subscription.created",
|
|
9005
|
-
"webhook.subscription.deleted"
|
|
10131
|
+
"webhook.subscription.deleted",
|
|
10132
|
+
// Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
|
|
10133
|
+
// `proposal.*` glob pattern to receive all three.
|
|
10134
|
+
"proposal.created",
|
|
10135
|
+
"proposal.approved",
|
|
10136
|
+
"proposal.rejected"
|
|
9006
10137
|
];
|
|
9007
10138
|
function newEventId2() {
|
|
9008
10139
|
return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
|
|
@@ -9036,7 +10167,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
9036
10167
|
|
|
9037
10168
|
// src/gateway/telemetry/fanout.ts
|
|
9038
10169
|
var import_node_crypto14 = require("crypto");
|
|
9039
|
-
var
|
|
10170
|
+
var import_core12 = require("@harness-engineering/core");
|
|
9040
10171
|
var TOPICS = {
|
|
9041
10172
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
9042
10173
|
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
@@ -9171,7 +10302,7 @@ function wireTelemetryFanout(params) {
|
|
|
9171
10302
|
spanId,
|
|
9172
10303
|
...parentSpanId !== void 0 ? { parentSpanId } : {},
|
|
9173
10304
|
name: SPAN_NAME[topic],
|
|
9174
|
-
kind:
|
|
10305
|
+
kind: import_core12.SpanKind.INTERNAL,
|
|
9175
10306
|
startTimeNs: startNs,
|
|
9176
10307
|
endTimeNs: startNs,
|
|
9177
10308
|
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
@@ -9427,6 +10558,33 @@ var ENVELOPE_DERIVERS = {
|
|
|
9427
10558
|
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9428
10559
|
severity: "info"
|
|
9429
10560
|
};
|
|
10561
|
+
},
|
|
10562
|
+
// Hermes Phase 4 — skill proposal lifecycle events.
|
|
10563
|
+
"proposal.created": (event) => {
|
|
10564
|
+
const data = asObj(event.data);
|
|
10565
|
+
const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
|
|
10566
|
+
return {
|
|
10567
|
+
title: `New skill proposal: ${label}`,
|
|
10568
|
+
summary: truncate(data.justification ?? "No justification provided.", 240),
|
|
10569
|
+
severity: "info"
|
|
10570
|
+
};
|
|
10571
|
+
},
|
|
10572
|
+
"proposal.approved": (event) => {
|
|
10573
|
+
const data = asObj(event.data);
|
|
10574
|
+
const label = data.name ?? data.targetSkill ?? "(unknown skill)";
|
|
10575
|
+
return {
|
|
10576
|
+
title: `Skill proposal approved: ${label}`,
|
|
10577
|
+
summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
|
|
10578
|
+
severity: "success"
|
|
10579
|
+
};
|
|
10580
|
+
},
|
|
10581
|
+
"proposal.rejected": (event) => {
|
|
10582
|
+
const data = asObj(event.data);
|
|
10583
|
+
return {
|
|
10584
|
+
title: "Skill proposal rejected",
|
|
10585
|
+
summary: truncate(data.reason ?? "No reason provided.", 240),
|
|
10586
|
+
severity: "warning"
|
|
10587
|
+
};
|
|
9430
10588
|
}
|
|
9431
10589
|
};
|
|
9432
10590
|
function truncate(s, max) {
|
|
@@ -9471,7 +10629,11 @@ var NOTIFICATION_TOPICS = [
|
|
|
9471
10629
|
"interaction.resolved",
|
|
9472
10630
|
"maintenance:started",
|
|
9473
10631
|
"maintenance:completed",
|
|
9474
|
-
"maintenance:error"
|
|
10632
|
+
"maintenance:error",
|
|
10633
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10634
|
+
"proposal.created",
|
|
10635
|
+
"proposal.approved",
|
|
10636
|
+
"proposal.rejected"
|
|
9475
10637
|
];
|
|
9476
10638
|
function newEventId4() {
|
|
9477
10639
|
return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
|
|
@@ -9527,7 +10689,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
9527
10689
|
}
|
|
9528
10690
|
|
|
9529
10691
|
// src/orchestrator.ts
|
|
9530
|
-
var
|
|
10692
|
+
var import_core16 = require("@harness-engineering/core");
|
|
9531
10693
|
|
|
9532
10694
|
// src/logging/logger.ts
|
|
9533
10695
|
var StructuredLogger = class {
|
|
@@ -9569,7 +10731,7 @@ var StructuredLogger = class {
|
|
|
9569
10731
|
// src/workspace/config-scanner.ts
|
|
9570
10732
|
var import_node_fs = require("fs");
|
|
9571
10733
|
var import_node_path4 = require("path");
|
|
9572
|
-
var
|
|
10734
|
+
var import_core13 = require("@harness-engineering/core");
|
|
9573
10735
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
9574
10736
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
9575
10737
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -9591,25 +10753,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
9591
10753
|
} catch {
|
|
9592
10754
|
return null;
|
|
9593
10755
|
}
|
|
9594
|
-
const injectionFindings = (0,
|
|
9595
|
-
const findings = (0,
|
|
10756
|
+
const injectionFindings = (0, import_core13.scanForInjection)(content);
|
|
10757
|
+
const findings = (0, import_core13.mapInjectionFindings)(injectionFindings);
|
|
9596
10758
|
const secFindings = await scanner.scanFile(filePath);
|
|
9597
|
-
findings.push(...(0,
|
|
10759
|
+
findings.push(...(0, import_core13.mapSecurityFindings)(secFindings, findings));
|
|
9598
10760
|
const adjusted = adjustFindingSeverity(findings);
|
|
9599
10761
|
return {
|
|
9600
10762
|
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
9601
10763
|
findings: adjusted,
|
|
9602
|
-
overallSeverity: (0,
|
|
10764
|
+
overallSeverity: (0, import_core13.computeOverallSeverity)(adjusted)
|
|
9603
10765
|
};
|
|
9604
10766
|
}
|
|
9605
10767
|
async function scanWorkspaceConfig(workspacePath) {
|
|
9606
|
-
const scanner = new
|
|
10768
|
+
const scanner = new import_core13.SecurityScanner((0, import_core13.parseSecurityConfig)({}));
|
|
9607
10769
|
const results = [];
|
|
9608
10770
|
for (const configFile of CONFIG_FILES) {
|
|
9609
10771
|
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
9610
10772
|
if (result) results.push(result);
|
|
9611
10773
|
}
|
|
9612
|
-
return { exitCode: (0,
|
|
10774
|
+
return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
|
|
9613
10775
|
}
|
|
9614
10776
|
|
|
9615
10777
|
// src/maintenance/task-registry.ts
|
|
@@ -9792,6 +10954,19 @@ var BUILT_IN_TASKS = [
|
|
|
9792
10954
|
schedule: "*/15 * * * *",
|
|
9793
10955
|
branch: null,
|
|
9794
10956
|
checkCommand: ["harness", "sync-main", "--json"]
|
|
10957
|
+
},
|
|
10958
|
+
// Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
|
|
10959
|
+
// on every existing catalog skill. Schedule is Feb 31 (a date that never
|
|
10960
|
+
// exists) so the cron loop never fires it automatically; operators trigger
|
|
10961
|
+
// it once via the dashboard "Run now" button or `harness backfill-skill-
|
|
10962
|
+
// provenance` after upgrading to Phase 4.
|
|
10963
|
+
{
|
|
10964
|
+
id: "proposal-provenance-backfill",
|
|
10965
|
+
type: "housekeeping",
|
|
10966
|
+
description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
|
|
10967
|
+
schedule: "0 0 31 2 *",
|
|
10968
|
+
branch: null,
|
|
10969
|
+
checkCommand: ["backfill-skill-provenance"]
|
|
9795
10970
|
}
|
|
9796
10971
|
];
|
|
9797
10972
|
|
|
@@ -9884,24 +11059,49 @@ var MaintenanceScheduler = class {
|
|
|
9884
11059
|
this.resolvedTasks = this.resolveTasks();
|
|
9885
11060
|
}
|
|
9886
11061
|
/**
|
|
9887
|
-
* Merge built-in task definitions with config overrides
|
|
9888
|
-
*
|
|
9889
|
-
*
|
|
11062
|
+
* Merge built-in task definitions with config overrides, then append
|
|
11063
|
+
* Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
|
|
11064
|
+
* overrides). Tasks with `enabled: false` are filtered out. Schedule
|
|
11065
|
+
* overrides replace the default cron expression.
|
|
9890
11066
|
*/
|
|
9891
11067
|
resolveTasks() {
|
|
9892
11068
|
const overrides = this.config.tasks ?? {};
|
|
9893
|
-
|
|
11069
|
+
const customs = this.config.customTasks ?? {};
|
|
11070
|
+
const merged = [];
|
|
11071
|
+
for (const task of BUILT_IN_TASKS) {
|
|
9894
11072
|
const override = overrides[task.id];
|
|
9895
|
-
if (override?.enabled === false)
|
|
9896
|
-
|
|
9897
|
-
}).map((task) => {
|
|
9898
|
-
const override = overrides[task.id];
|
|
9899
|
-
if (!override) return { ...task };
|
|
9900
|
-
return {
|
|
11073
|
+
if (override?.enabled === false) continue;
|
|
11074
|
+
merged.push({
|
|
9901
11075
|
...task,
|
|
9902
|
-
...override
|
|
9903
|
-
};
|
|
9904
|
-
}
|
|
11076
|
+
...override?.schedule !== void 0 && { schedule: override.schedule }
|
|
11077
|
+
});
|
|
11078
|
+
}
|
|
11079
|
+
for (const [id, def] of Object.entries(customs)) {
|
|
11080
|
+
const override = overrides[id];
|
|
11081
|
+
if (override?.enabled === false) continue;
|
|
11082
|
+
merged.push({
|
|
11083
|
+
id,
|
|
11084
|
+
type: def.type,
|
|
11085
|
+
description: def.description,
|
|
11086
|
+
schedule: override?.schedule ?? def.schedule,
|
|
11087
|
+
branch: def.branch,
|
|
11088
|
+
...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
|
|
11089
|
+
...def.checkScript !== void 0 && { checkScript: def.checkScript },
|
|
11090
|
+
...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
|
|
11091
|
+
...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
|
|
11092
|
+
...def.inlineSkillsBudgetTokens !== void 0 && {
|
|
11093
|
+
inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
|
|
11094
|
+
},
|
|
11095
|
+
...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
|
|
11096
|
+
...def.contextFromMaxAgeMinutes !== void 0 && {
|
|
11097
|
+
contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
|
|
11098
|
+
},
|
|
11099
|
+
...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
|
|
11100
|
+
...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
|
|
11101
|
+
isCustom: true
|
|
11102
|
+
});
|
|
11103
|
+
}
|
|
11104
|
+
return merged;
|
|
9905
11105
|
}
|
|
9906
11106
|
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
9907
11107
|
getResolvedTasks() {
|
|
@@ -10074,27 +11274,27 @@ var MaintenanceScheduler = class {
|
|
|
10074
11274
|
};
|
|
10075
11275
|
|
|
10076
11276
|
// src/maintenance/leader-elector.ts
|
|
10077
|
-
var
|
|
11277
|
+
var import_types29 = require("@harness-engineering/types");
|
|
10078
11278
|
var SingleProcessLeaderElector = class {
|
|
10079
11279
|
async electLeader() {
|
|
10080
|
-
return (0,
|
|
11280
|
+
return (0, import_types29.Ok)("claimed");
|
|
10081
11281
|
}
|
|
10082
11282
|
};
|
|
10083
11283
|
|
|
10084
11284
|
// src/maintenance/reporter.ts
|
|
10085
|
-
var
|
|
10086
|
-
var
|
|
10087
|
-
var
|
|
10088
|
-
var RunResultSchema =
|
|
10089
|
-
taskId:
|
|
10090
|
-
startedAt:
|
|
10091
|
-
completedAt:
|
|
10092
|
-
status:
|
|
10093
|
-
findings:
|
|
10094
|
-
fixed:
|
|
10095
|
-
prUrl:
|
|
10096
|
-
prUpdated:
|
|
10097
|
-
error:
|
|
11285
|
+
var fs16 = __toESM(require("fs"));
|
|
11286
|
+
var path18 = __toESM(require("path"));
|
|
11287
|
+
var import_zod17 = require("zod");
|
|
11288
|
+
var RunResultSchema = import_zod17.z.object({
|
|
11289
|
+
taskId: import_zod17.z.string(),
|
|
11290
|
+
startedAt: import_zod17.z.string(),
|
|
11291
|
+
completedAt: import_zod17.z.string(),
|
|
11292
|
+
status: import_zod17.z.enum(["success", "failure", "skipped", "no-issues"]),
|
|
11293
|
+
findings: import_zod17.z.number(),
|
|
11294
|
+
fixed: import_zod17.z.number(),
|
|
11295
|
+
prUrl: import_zod17.z.string().nullable(),
|
|
11296
|
+
prUpdated: import_zod17.z.boolean(),
|
|
11297
|
+
error: import_zod17.z.string().optional()
|
|
10098
11298
|
});
|
|
10099
11299
|
var MAX_HISTORY = 500;
|
|
10100
11300
|
var fallbackLogger = {
|
|
@@ -10118,10 +11318,10 @@ var MaintenanceReporter = class {
|
|
|
10118
11318
|
*/
|
|
10119
11319
|
async load() {
|
|
10120
11320
|
try {
|
|
10121
|
-
await
|
|
10122
|
-
const filePath =
|
|
10123
|
-
const data = await
|
|
10124
|
-
const parsed =
|
|
11321
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11322
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11323
|
+
const data = await fs16.promises.readFile(filePath, "utf-8");
|
|
11324
|
+
const parsed = import_zod17.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
10125
11325
|
if (parsed.success) {
|
|
10126
11326
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10127
11327
|
}
|
|
@@ -10154,9 +11354,9 @@ var MaintenanceReporter = class {
|
|
|
10154
11354
|
*/
|
|
10155
11355
|
async persist() {
|
|
10156
11356
|
try {
|
|
10157
|
-
await
|
|
10158
|
-
const filePath =
|
|
10159
|
-
await
|
|
11357
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11358
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11359
|
+
await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
10160
11360
|
} catch (err) {
|
|
10161
11361
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10162
11362
|
}
|
|
@@ -10172,6 +11372,9 @@ var TaskRunner = class {
|
|
|
10172
11372
|
cwd;
|
|
10173
11373
|
prManager;
|
|
10174
11374
|
baseBranch;
|
|
11375
|
+
checkScriptRunner;
|
|
11376
|
+
contextResolver;
|
|
11377
|
+
outputStore;
|
|
10175
11378
|
constructor(options) {
|
|
10176
11379
|
this.config = options.config;
|
|
10177
11380
|
this.checkRunner = options.checkRunner;
|
|
@@ -10180,27 +11383,49 @@ var TaskRunner = class {
|
|
|
10180
11383
|
this.cwd = options.cwd;
|
|
10181
11384
|
this.prManager = options.prManager ?? null;
|
|
10182
11385
|
this.baseBranch = options.baseBranch ?? "main";
|
|
11386
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
11387
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
11388
|
+
this.outputStore = options.outputStore ?? null;
|
|
10183
11389
|
}
|
|
10184
11390
|
/**
|
|
10185
11391
|
* Run a maintenance task and return the result.
|
|
10186
11392
|
* Dispatches to the appropriate execution path based on task type.
|
|
10187
11393
|
* Never throws -- errors are captured in the RunResult.
|
|
11394
|
+
*
|
|
11395
|
+
* @param task - Resolved task definition.
|
|
11396
|
+
* @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
|
|
11397
|
+
* when called from the scheduler path.
|
|
10188
11398
|
*/
|
|
10189
|
-
async run(task) {
|
|
11399
|
+
async run(task, origin = "cron") {
|
|
10190
11400
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11401
|
+
let result;
|
|
11402
|
+
let captured;
|
|
10191
11403
|
try {
|
|
10192
11404
|
switch (task.type) {
|
|
10193
|
-
case "mechanical-ai":
|
|
10194
|
-
|
|
11405
|
+
case "mechanical-ai": {
|
|
11406
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
11407
|
+
result = out.result;
|
|
11408
|
+
captured = out.captured;
|
|
11409
|
+
break;
|
|
11410
|
+
}
|
|
10195
11411
|
case "pure-ai":
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
11412
|
+
result = await this.runPureAI(task, startedAt);
|
|
11413
|
+
break;
|
|
11414
|
+
case "report-only": {
|
|
11415
|
+
const out = await this.runReportOnly(task, startedAt);
|
|
11416
|
+
result = out.result;
|
|
11417
|
+
captured = out.captured;
|
|
11418
|
+
break;
|
|
11419
|
+
}
|
|
11420
|
+
case "housekeeping": {
|
|
11421
|
+
const out = await this.runHousekeeping(task, startedAt);
|
|
11422
|
+
result = out.result;
|
|
11423
|
+
captured = out.captured;
|
|
11424
|
+
break;
|
|
11425
|
+
}
|
|
10201
11426
|
default: {
|
|
10202
11427
|
const _exhaustive = task.type;
|
|
10203
|
-
|
|
11428
|
+
result = this.failureResult(
|
|
10204
11429
|
task.id,
|
|
10205
11430
|
startedAt,
|
|
10206
11431
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -10208,85 +11433,193 @@ var TaskRunner = class {
|
|
|
10208
11433
|
}
|
|
10209
11434
|
}
|
|
10210
11435
|
} catch (err) {
|
|
10211
|
-
|
|
11436
|
+
result = this.failureResult(task.id, startedAt, String(err));
|
|
11437
|
+
}
|
|
11438
|
+
result.origin = origin;
|
|
11439
|
+
await this.persistOutput(task, result, captured, origin);
|
|
11440
|
+
return result;
|
|
11441
|
+
}
|
|
11442
|
+
async persistOutput(task, result, captured, origin) {
|
|
11443
|
+
if (!this.outputStore) return;
|
|
11444
|
+
const entry = {
|
|
11445
|
+
taskId: result.taskId,
|
|
11446
|
+
startedAt: result.startedAt,
|
|
11447
|
+
completedAt: result.completedAt,
|
|
11448
|
+
status: result.status,
|
|
11449
|
+
findings: result.findings,
|
|
11450
|
+
fixed: result.fixed,
|
|
11451
|
+
prUrl: result.prUrl,
|
|
11452
|
+
prUpdated: result.prUpdated,
|
|
11453
|
+
origin,
|
|
11454
|
+
...result.error !== void 0 && { error: result.error },
|
|
11455
|
+
...result.costUsd !== void 0 && { costUsd: result.costUsd },
|
|
11456
|
+
...captured?.stdout !== void 0 && { stdout: captured.stdout },
|
|
11457
|
+
...captured?.stderr !== void 0 && { stderr: captured.stderr },
|
|
11458
|
+
...captured?.structured !== void 0 && { structured: captured.structured },
|
|
11459
|
+
...captured?.context !== void 0 && { context: captured.context }
|
|
11460
|
+
};
|
|
11461
|
+
try {
|
|
11462
|
+
await this.outputStore.write(task.id, entry, task.outputRetention);
|
|
11463
|
+
} catch {
|
|
10212
11464
|
}
|
|
10213
11465
|
}
|
|
10214
11466
|
/**
|
|
10215
|
-
*
|
|
11467
|
+
* Run the check step using whichever runner the task asks for. Custom
|
|
11468
|
+
* tasks that declare `checkScript` go through the Hermes Phase 2
|
|
11469
|
+
* `CheckScriptRunner`; built-ins (and customs that use the legacy
|
|
11470
|
+
* `checkCommand` shape) go through the original heuristic runner.
|
|
10216
11471
|
*/
|
|
10217
|
-
async
|
|
11472
|
+
async runCheckStep(task) {
|
|
11473
|
+
if (task.checkScript) {
|
|
11474
|
+
if (!this.checkScriptRunner) {
|
|
11475
|
+
throw new Error(
|
|
11476
|
+
`task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
|
|
11477
|
+
);
|
|
11478
|
+
}
|
|
11479
|
+
const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
|
|
11480
|
+
return {
|
|
11481
|
+
passed: r2.passed,
|
|
11482
|
+
findings: r2.findings,
|
|
11483
|
+
stdout: r2.output,
|
|
11484
|
+
stderr: r2.stderr,
|
|
11485
|
+
structured: r2.structured ? r2.structured : null
|
|
11486
|
+
};
|
|
11487
|
+
}
|
|
10218
11488
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
10219
|
-
|
|
11489
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
10220
11490
|
}
|
|
11491
|
+
const r = await this.checkRunner.run(task.checkCommand, this.cwd);
|
|
11492
|
+
return {
|
|
11493
|
+
passed: r.passed,
|
|
11494
|
+
findings: r.findings,
|
|
11495
|
+
stdout: r.output,
|
|
11496
|
+
stderr: "",
|
|
11497
|
+
structured: null
|
|
11498
|
+
};
|
|
11499
|
+
}
|
|
11500
|
+
/**
|
|
11501
|
+
* Hermes Phase 2 — Compose the agent prompt-context block from inlined
|
|
11502
|
+
* skills + upstream task outputs. Returns an empty string when nothing
|
|
11503
|
+
* is configured (or when the resolver is absent), which is the safe
|
|
11504
|
+
* no-op default.
|
|
11505
|
+
*/
|
|
11506
|
+
async composePromptContext(task) {
|
|
11507
|
+
if (!this.contextResolver) return "";
|
|
11508
|
+
const skills = await this.contextResolver.resolveInlineSkills(
|
|
11509
|
+
task.inlineSkills,
|
|
11510
|
+
task.inlineSkillsBudgetTokens ?? 8e3
|
|
11511
|
+
);
|
|
11512
|
+
const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
|
|
11513
|
+
maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
|
|
11514
|
+
});
|
|
11515
|
+
return [skills, upstream].filter(Boolean).join("\n");
|
|
11516
|
+
}
|
|
11517
|
+
/**
|
|
11518
|
+
* Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
|
|
11519
|
+
* only if fixable findings exist; persist captured stdout/stderr/context
|
|
11520
|
+
* via the output store on the way out.
|
|
11521
|
+
*/
|
|
11522
|
+
async runMechanicalAI(task, startedAt) {
|
|
10221
11523
|
if (!task.fixSkill) {
|
|
10222
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
11524
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
10223
11525
|
}
|
|
10224
11526
|
if (!task.branch) {
|
|
10225
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
11527
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
11528
|
+
}
|
|
11529
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11530
|
+
return wrap(
|
|
11531
|
+
this.failureResult(
|
|
11532
|
+
task.id,
|
|
11533
|
+
startedAt,
|
|
11534
|
+
"mechanical-ai task missing checkCommand or checkScript"
|
|
11535
|
+
)
|
|
11536
|
+
);
|
|
10226
11537
|
}
|
|
10227
|
-
|
|
10228
|
-
|
|
11538
|
+
let check;
|
|
11539
|
+
try {
|
|
11540
|
+
check = await this.runCheckStep(task);
|
|
11541
|
+
} catch (err) {
|
|
11542
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11543
|
+
}
|
|
11544
|
+
const promptContext = await this.composePromptContext(task);
|
|
11545
|
+
const baseCaptured = {
|
|
11546
|
+
stdout: check.stdout,
|
|
11547
|
+
stderr: check.stderr,
|
|
11548
|
+
structured: check.structured,
|
|
11549
|
+
...promptContext ? { context: promptContext } : {}
|
|
11550
|
+
};
|
|
11551
|
+
const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
|
|
11552
|
+
if (check.findings === 0 || wakeAgentExplicitlyFalse) {
|
|
10229
11553
|
return {
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
11554
|
+
result: {
|
|
11555
|
+
taskId: task.id,
|
|
11556
|
+
startedAt,
|
|
11557
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11558
|
+
status: "no-issues",
|
|
11559
|
+
findings: check.findings,
|
|
11560
|
+
fixed: 0,
|
|
11561
|
+
prUrl: null,
|
|
11562
|
+
prUpdated: false
|
|
11563
|
+
},
|
|
11564
|
+
captured: baseCaptured
|
|
10238
11565
|
};
|
|
10239
11566
|
}
|
|
10240
11567
|
if (this.prManager) {
|
|
10241
11568
|
try {
|
|
10242
11569
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
10243
11570
|
} catch (err) {
|
|
10244
|
-
return
|
|
11571
|
+
return wrap(
|
|
11572
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11573
|
+
baseCaptured
|
|
11574
|
+
);
|
|
10245
11575
|
}
|
|
10246
11576
|
}
|
|
10247
11577
|
const backendName = this.resolveBackend(task.id);
|
|
10248
11578
|
let agentResult;
|
|
10249
11579
|
try {
|
|
10250
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
backendName,
|
|
10254
|
-
this.cwd
|
|
10255
|
-
);
|
|
11580
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11581
|
+
promptContext
|
|
11582
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10256
11583
|
} catch (err) {
|
|
10257
11584
|
return {
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
11585
|
+
result: {
|
|
11586
|
+
taskId: task.id,
|
|
11587
|
+
startedAt,
|
|
11588
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11589
|
+
status: "failure",
|
|
11590
|
+
findings: check.findings,
|
|
11591
|
+
fixed: 0,
|
|
11592
|
+
prUrl: null,
|
|
11593
|
+
prUpdated: false,
|
|
11594
|
+
error: `Agent dispatch failed: ${String(err)}`
|
|
11595
|
+
},
|
|
11596
|
+
captured: baseCaptured
|
|
10267
11597
|
};
|
|
10268
11598
|
}
|
|
10269
11599
|
let prUrl = null;
|
|
10270
11600
|
let prUpdated = false;
|
|
10271
11601
|
if (this.prManager && agentResult.producedCommits) {
|
|
10272
11602
|
try {
|
|
10273
|
-
const summary = `Findings: ${
|
|
11603
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
10274
11604
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
10275
11605
|
prUrl = prResult.prUrl;
|
|
10276
11606
|
prUpdated = prResult.prUpdated;
|
|
10277
|
-
} catch (err) {
|
|
10278
|
-
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
10279
|
-
}
|
|
10280
|
-
}
|
|
10281
|
-
return {
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
11607
|
+
} catch (err) {
|
|
11608
|
+
console.warn(`[maintenance] PR creation failed for task ${task.id}: ${String(err)}`);
|
|
11609
|
+
}
|
|
11610
|
+
}
|
|
11611
|
+
return {
|
|
11612
|
+
result: {
|
|
11613
|
+
taskId: task.id,
|
|
11614
|
+
startedAt,
|
|
11615
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11616
|
+
status: "success",
|
|
11617
|
+
findings: check.findings,
|
|
11618
|
+
fixed: agentResult.fixed,
|
|
11619
|
+
prUrl,
|
|
11620
|
+
prUpdated
|
|
11621
|
+
},
|
|
11622
|
+
captured: baseCaptured
|
|
10290
11623
|
};
|
|
10291
11624
|
}
|
|
10292
11625
|
/**
|
|
@@ -10306,15 +11639,13 @@ var TaskRunner = class {
|
|
|
10306
11639
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
10307
11640
|
}
|
|
10308
11641
|
}
|
|
11642
|
+
const promptContext = await this.composePromptContext(task);
|
|
10309
11643
|
const backendName = this.resolveBackend(task.id);
|
|
10310
11644
|
let agentResult;
|
|
10311
11645
|
try {
|
|
10312
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
backendName,
|
|
10316
|
-
this.cwd
|
|
10317
|
-
);
|
|
11646
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11647
|
+
promptContext
|
|
11648
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10318
11649
|
} catch (err) {
|
|
10319
11650
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
10320
11651
|
}
|
|
@@ -10342,7 +11673,7 @@ var TaskRunner = class {
|
|
|
10342
11673
|
};
|
|
10343
11674
|
}
|
|
10344
11675
|
/**
|
|
10345
|
-
* Report-only: run check
|
|
11676
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
10346
11677
|
*
|
|
10347
11678
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
10348
11679
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -10352,13 +11683,24 @@ var TaskRunner = class {
|
|
|
10352
11683
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
10353
11684
|
*/
|
|
10354
11685
|
async runReportOnly(task, startedAt) {
|
|
10355
|
-
if (!task.checkCommand
|
|
10356
|
-
return
|
|
11686
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11687
|
+
return wrap(
|
|
11688
|
+
this.failureResult(
|
|
11689
|
+
task.id,
|
|
11690
|
+
startedAt,
|
|
11691
|
+
"report-only task missing checkCommand or checkScript"
|
|
11692
|
+
)
|
|
11693
|
+
);
|
|
11694
|
+
}
|
|
11695
|
+
let check;
|
|
11696
|
+
try {
|
|
11697
|
+
check = await this.runCheckStep(task);
|
|
11698
|
+
} catch (err) {
|
|
11699
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
10357
11700
|
}
|
|
10358
|
-
const
|
|
10359
|
-
const parsed = parseStatusLine(checkResult.output);
|
|
11701
|
+
const parsed = parseStatusLine(check.stdout);
|
|
10360
11702
|
const status = parsed?.status ?? "success";
|
|
10361
|
-
const findings = parsed === null ?
|
|
11703
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
10362
11704
|
const result = {
|
|
10363
11705
|
taskId: task.id,
|
|
10364
11706
|
startedAt,
|
|
@@ -10372,7 +11714,10 @@ var TaskRunner = class {
|
|
|
10372
11714
|
if (parsed?.error) {
|
|
10373
11715
|
result.error = parsed.error;
|
|
10374
11716
|
}
|
|
10375
|
-
return
|
|
11717
|
+
return {
|
|
11718
|
+
result,
|
|
11719
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11720
|
+
};
|
|
10376
11721
|
}
|
|
10377
11722
|
/**
|
|
10378
11723
|
* Housekeeping: run command directly, no AI, no PR.
|
|
@@ -10383,17 +11728,39 @@ var TaskRunner = class {
|
|
|
10383
11728
|
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
10384
11729
|
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
10385
11730
|
* status: 'success', findings: 0.
|
|
11731
|
+
*
|
|
11732
|
+
* Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
|
|
11733
|
+
* tasks; the runner falls through to the same JSON-status parsing path.
|
|
10386
11734
|
*/
|
|
10387
11735
|
async runHousekeeping(task, startedAt) {
|
|
10388
|
-
if (!task.checkCommand
|
|
10389
|
-
return
|
|
11736
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11737
|
+
return wrap(
|
|
11738
|
+
this.failureResult(
|
|
11739
|
+
task.id,
|
|
11740
|
+
startedAt,
|
|
11741
|
+
"housekeeping task missing checkCommand or checkScript"
|
|
11742
|
+
)
|
|
11743
|
+
);
|
|
10390
11744
|
}
|
|
10391
11745
|
let stdout;
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
11746
|
+
let stderr = "";
|
|
11747
|
+
let structured = null;
|
|
11748
|
+
if (task.checkScript) {
|
|
11749
|
+
try {
|
|
11750
|
+
const r = await this.runCheckStep(task);
|
|
11751
|
+
stdout = r.stdout;
|
|
11752
|
+
stderr = r.stderr;
|
|
11753
|
+
structured = r.structured;
|
|
11754
|
+
} catch (err) {
|
|
11755
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11756
|
+
}
|
|
11757
|
+
} else {
|
|
11758
|
+
try {
|
|
11759
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
11760
|
+
stdout = out.stdout ?? "";
|
|
11761
|
+
} catch (err) {
|
|
11762
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11763
|
+
}
|
|
10397
11764
|
}
|
|
10398
11765
|
const parsed = parseStatusLine(stdout);
|
|
10399
11766
|
const status = parsed?.status ?? "success";
|
|
@@ -10408,7 +11775,7 @@ var TaskRunner = class {
|
|
|
10408
11775
|
prUpdated: false
|
|
10409
11776
|
};
|
|
10410
11777
|
if (parsed?.error) result.error = parsed.error;
|
|
10411
|
-
return result;
|
|
11778
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
10412
11779
|
}
|
|
10413
11780
|
/**
|
|
10414
11781
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -10433,6 +11800,9 @@ var TaskRunner = class {
|
|
|
10433
11800
|
};
|
|
10434
11801
|
}
|
|
10435
11802
|
};
|
|
11803
|
+
function wrap(result, captured) {
|
|
11804
|
+
return captured ? { result, captured } : { result };
|
|
11805
|
+
}
|
|
10436
11806
|
function parseStatusLine(output) {
|
|
10437
11807
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10438
11808
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -10470,12 +11840,561 @@ function parseStatusLine(output) {
|
|
|
10470
11840
|
return null;
|
|
10471
11841
|
}
|
|
10472
11842
|
|
|
10473
|
-
// src/
|
|
10474
|
-
|
|
10475
|
-
|
|
10476
|
-
|
|
10477
|
-
|
|
11843
|
+
// src/maintenance/check-script-runner.ts
|
|
11844
|
+
var import_node_child_process11 = require("child_process");
|
|
11845
|
+
var import_node_util3 = require("util");
|
|
11846
|
+
var path19 = __toESM(require("path"));
|
|
11847
|
+
var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process11.execFile);
|
|
11848
|
+
var CheckScriptRunner = class {
|
|
11849
|
+
constructor(cwd) {
|
|
11850
|
+
this.cwd = cwd;
|
|
11851
|
+
}
|
|
11852
|
+
cwd;
|
|
11853
|
+
async run(spec, cwd) {
|
|
11854
|
+
const projectRoot = cwd ?? this.cwd;
|
|
11855
|
+
const captured = await captureScript(spec, projectRoot);
|
|
11856
|
+
const parseJson = spec.parseStdoutJson !== false;
|
|
11857
|
+
const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
|
|
11858
|
+
if (structured) {
|
|
11859
|
+
return mapStructured(structured, captured.stdout, captured.stderr);
|
|
11860
|
+
}
|
|
11861
|
+
return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
|
|
11862
|
+
}
|
|
11863
|
+
};
|
|
11864
|
+
async function captureScript(spec, projectRoot) {
|
|
11865
|
+
const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
|
|
11866
|
+
const args = spec.args ?? [];
|
|
11867
|
+
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11868
|
+
try {
|
|
11869
|
+
const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
|
|
11870
|
+
return {
|
|
11871
|
+
stdout: String(result.stdout ?? ""),
|
|
11872
|
+
stderr: String(result.stderr ?? ""),
|
|
11873
|
+
exitedAbnormally: false
|
|
11874
|
+
};
|
|
11875
|
+
} catch (err) {
|
|
11876
|
+
const e = err;
|
|
11877
|
+
return {
|
|
11878
|
+
stdout: String(e.stdout ?? ""),
|
|
11879
|
+
stderr: String(e.stderr ?? ""),
|
|
11880
|
+
exitedAbnormally: true
|
|
11881
|
+
};
|
|
11882
|
+
}
|
|
11883
|
+
}
|
|
11884
|
+
function parseStatusEnvelope(stdout) {
|
|
11885
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11886
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11887
|
+
const env = classifyLine2(lines[i]);
|
|
11888
|
+
if (env) return env;
|
|
11889
|
+
}
|
|
11890
|
+
return null;
|
|
11891
|
+
}
|
|
11892
|
+
var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
|
|
11893
|
+
function classifyLine2(line) {
|
|
11894
|
+
const obj = tryParseJsonObject(line);
|
|
11895
|
+
if (!obj) return null;
|
|
11896
|
+
const s = obj.status;
|
|
11897
|
+
if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
|
|
11898
|
+
return buildEnvelope(s, obj);
|
|
11899
|
+
}
|
|
11900
|
+
function tryParseJsonObject(line) {
|
|
11901
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
|
|
11902
|
+
try {
|
|
11903
|
+
return JSON.parse(line);
|
|
11904
|
+
} catch {
|
|
11905
|
+
return null;
|
|
11906
|
+
}
|
|
11907
|
+
}
|
|
11908
|
+
function buildEnvelope(status, obj) {
|
|
11909
|
+
const env = { status };
|
|
11910
|
+
if (typeof obj.findings === "number") env.findings = obj.findings;
|
|
11911
|
+
if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
|
|
11912
|
+
if (typeof obj.message === "string") env.message = obj.message;
|
|
11913
|
+
if (obj.outputs && typeof obj.outputs === "object") {
|
|
11914
|
+
env.outputs = obj.outputs;
|
|
11915
|
+
}
|
|
11916
|
+
return env;
|
|
11917
|
+
}
|
|
11918
|
+
function mapStructured(env, stdout, stderr) {
|
|
11919
|
+
const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
|
|
11920
|
+
switch (env.status) {
|
|
11921
|
+
case "ok":
|
|
11922
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11923
|
+
case "findings": {
|
|
11924
|
+
const wake = env.wakeAgent ?? findings > 0;
|
|
11925
|
+
return { passed: !wake, findings, output: stdout, stderr, structured: env };
|
|
11926
|
+
}
|
|
11927
|
+
case "skip":
|
|
11928
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11929
|
+
case "error":
|
|
11930
|
+
return {
|
|
11931
|
+
passed: false,
|
|
11932
|
+
findings: Math.max(findings, 1),
|
|
11933
|
+
output: stdout,
|
|
11934
|
+
stderr,
|
|
11935
|
+
structured: env
|
|
11936
|
+
};
|
|
11937
|
+
default:
|
|
11938
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11939
|
+
}
|
|
11940
|
+
}
|
|
11941
|
+
function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
11942
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
11943
|
+
const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
11944
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
|
|
11945
|
+
return {
|
|
11946
|
+
passed: findings === 0 && !exitedAbnormally,
|
|
11947
|
+
findings,
|
|
11948
|
+
output: stdout,
|
|
11949
|
+
stderr,
|
|
11950
|
+
structured: null
|
|
11951
|
+
};
|
|
11952
|
+
}
|
|
11953
|
+
|
|
11954
|
+
// src/maintenance/output-store.ts
|
|
11955
|
+
var fs17 = __toESM(require("fs"));
|
|
11956
|
+
var path20 = __toESM(require("path"));
|
|
11957
|
+
var DEFAULT_RETENTION = {
|
|
11958
|
+
runs: 50,
|
|
11959
|
+
maxAgeDays: 30
|
|
11960
|
+
};
|
|
11961
|
+
var fallbackLogger2 = {
|
|
11962
|
+
info: () => {
|
|
11963
|
+
},
|
|
11964
|
+
warn: (m, c) => console.warn(m, c),
|
|
11965
|
+
error: (m, c) => console.error(m, c)
|
|
11966
|
+
};
|
|
11967
|
+
var TaskOutputStore = class {
|
|
11968
|
+
rootDir;
|
|
11969
|
+
retentionDefaults;
|
|
11970
|
+
logger;
|
|
11971
|
+
constructor(options) {
|
|
11972
|
+
this.rootDir = options.rootDir;
|
|
11973
|
+
this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
|
|
11974
|
+
this.logger = options.logger ?? fallbackLogger2;
|
|
11975
|
+
}
|
|
11976
|
+
/**
|
|
11977
|
+
* Reject task IDs that don't match the validator's kebab-case pattern —
|
|
11978
|
+
* defends `dirFor()` against caller-supplied path-traversal segments
|
|
11979
|
+
* (`'../foo'`) when the store is invoked from CLI surfaces that don't
|
|
11980
|
+
* round-trip through `validateCustomTasks`.
|
|
11981
|
+
*/
|
|
11982
|
+
ensureSafeTaskId(taskId) {
|
|
11983
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
|
|
11984
|
+
throw new Error(
|
|
11985
|
+
`TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
|
|
11986
|
+
);
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
/**
|
|
11990
|
+
* Persist a single run entry. Retention is applied after the write so
|
|
11991
|
+
* the latest record is durable even if pruning fails.
|
|
11992
|
+
*/
|
|
11993
|
+
async write(taskId, entry, retention) {
|
|
11994
|
+
this.ensureSafeTaskId(taskId);
|
|
11995
|
+
const dir = this.dirFor(taskId);
|
|
11996
|
+
await fs17.promises.mkdir(dir, { recursive: true });
|
|
11997
|
+
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11998
|
+
const filePath = path20.join(dir, fileName);
|
|
11999
|
+
const tmpPath = `${filePath}.tmp`;
|
|
12000
|
+
const payload = JSON.stringify(entry, null, 2);
|
|
12001
|
+
await fs17.promises.writeFile(tmpPath, payload, "utf-8");
|
|
12002
|
+
await fs17.promises.rename(tmpPath, filePath);
|
|
12003
|
+
try {
|
|
12004
|
+
await this.applyRetention(taskId, retention);
|
|
12005
|
+
} catch (err) {
|
|
12006
|
+
this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
|
|
12007
|
+
}
|
|
12008
|
+
}
|
|
12009
|
+
/**
|
|
12010
|
+
* Return the most recent persisted entry for the task, or null if none.
|
|
12011
|
+
*/
|
|
12012
|
+
async latest(taskId) {
|
|
12013
|
+
const entries = await this.list(taskId, 1, 0);
|
|
12014
|
+
return entries[0] ?? null;
|
|
12015
|
+
}
|
|
12016
|
+
/**
|
|
12017
|
+
* List entries newest-first with offset+limit pagination.
|
|
12018
|
+
*/
|
|
12019
|
+
async list(taskId, limit, offset) {
|
|
12020
|
+
this.ensureSafeTaskId(taskId);
|
|
12021
|
+
const dir = this.dirFor(taskId);
|
|
12022
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
12023
|
+
const slice = fileNames.slice(offset, offset + limit);
|
|
12024
|
+
const out = [];
|
|
12025
|
+
for (const name of slice) {
|
|
12026
|
+
const entry = await this.readEntry(path20.join(dir, name));
|
|
12027
|
+
if (entry) out.push(entry);
|
|
12028
|
+
}
|
|
12029
|
+
return out;
|
|
12030
|
+
}
|
|
12031
|
+
/**
|
|
12032
|
+
* Lookup a specific run by its file name (without the `.json` suffix) or
|
|
12033
|
+
* by its raw completion timestamp.
|
|
12034
|
+
*/
|
|
12035
|
+
async get(taskId, runId) {
|
|
12036
|
+
this.ensureSafeTaskId(taskId);
|
|
12037
|
+
if (/[\\/]|\.\./.test(runId)) {
|
|
12038
|
+
throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
|
|
12039
|
+
}
|
|
12040
|
+
const dir = this.dirFor(taskId);
|
|
12041
|
+
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
12042
|
+
return this.readEntry(path20.join(dir, fileName));
|
|
12043
|
+
}
|
|
12044
|
+
/**
|
|
12045
|
+
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
12046
|
+
* outputs from outside the store API.
|
|
12047
|
+
*/
|
|
12048
|
+
dirFor(taskId) {
|
|
12049
|
+
return path20.join(this.rootDir, taskId, "outputs");
|
|
12050
|
+
}
|
|
12051
|
+
async readEntry(filePath) {
|
|
12052
|
+
try {
|
|
12053
|
+
const buf = await fs17.promises.readFile(filePath, "utf-8");
|
|
12054
|
+
const parsed = JSON.parse(buf);
|
|
12055
|
+
return parsed;
|
|
12056
|
+
} catch {
|
|
12057
|
+
return null;
|
|
12058
|
+
}
|
|
12059
|
+
}
|
|
12060
|
+
async applyRetention(taskId, retention) {
|
|
12061
|
+
const runs = retention?.runs ?? this.retentionDefaults.runs;
|
|
12062
|
+
const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
|
|
12063
|
+
const dir = this.dirFor(taskId);
|
|
12064
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
12065
|
+
const overflow = fileNames.slice(runs);
|
|
12066
|
+
const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
12067
|
+
const aged = [];
|
|
12068
|
+
for (const name of fileNames) {
|
|
12069
|
+
const ts = parseIsoFromFileName(name);
|
|
12070
|
+
if (ts !== null && ts < cutoffMs) aged.push(name);
|
|
12071
|
+
}
|
|
12072
|
+
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
12073
|
+
for (const name of toRemove) {
|
|
12074
|
+
try {
|
|
12075
|
+
await fs17.promises.unlink(path20.join(dir, name));
|
|
12076
|
+
} catch {
|
|
12077
|
+
}
|
|
12078
|
+
}
|
|
12079
|
+
}
|
|
12080
|
+
};
|
|
12081
|
+
async function listJsonFilesDescending(dir) {
|
|
12082
|
+
let names;
|
|
12083
|
+
try {
|
|
12084
|
+
names = await fs17.promises.readdir(dir);
|
|
12085
|
+
} catch {
|
|
12086
|
+
return [];
|
|
12087
|
+
}
|
|
12088
|
+
return names.filter((n) => n.endsWith(".json")).sort().reverse();
|
|
12089
|
+
}
|
|
12090
|
+
function sanitizeIso(iso) {
|
|
12091
|
+
return iso.replace(/:/g, "-");
|
|
12092
|
+
}
|
|
12093
|
+
function parseIsoFromFileName(fileName) {
|
|
12094
|
+
const stem = fileName.replace(/\.json$/, "");
|
|
12095
|
+
const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
|
|
12096
|
+
const ms = Date.parse(restored);
|
|
12097
|
+
return Number.isFinite(ms) ? ms : null;
|
|
10478
12098
|
}
|
|
12099
|
+
|
|
12100
|
+
// src/maintenance/context-resolver.ts
|
|
12101
|
+
var ContextResolver = class {
|
|
12102
|
+
outputStore;
|
|
12103
|
+
skillReader;
|
|
12104
|
+
logger;
|
|
12105
|
+
perUpstreamMaxChars;
|
|
12106
|
+
constructor(options) {
|
|
12107
|
+
this.outputStore = options.outputStore;
|
|
12108
|
+
this.skillReader = options.skillReader ?? null;
|
|
12109
|
+
this.logger = options.logger ?? fallbackLogger3;
|
|
12110
|
+
this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
|
|
12111
|
+
}
|
|
12112
|
+
async resolveContextFrom(upstreamTaskIds, options = {}) {
|
|
12113
|
+
if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
|
|
12114
|
+
const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
|
|
12115
|
+
const now = Date.now();
|
|
12116
|
+
const sections = [];
|
|
12117
|
+
for (const id of upstreamTaskIds) {
|
|
12118
|
+
const entry = await this.outputStore.latest(id);
|
|
12119
|
+
sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
|
|
12120
|
+
}
|
|
12121
|
+
return `## Upstream context
|
|
12122
|
+
|
|
12123
|
+
${sections.join("\n\n")}
|
|
12124
|
+
`;
|
|
12125
|
+
}
|
|
12126
|
+
async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
|
|
12127
|
+
if (!skillNames || skillNames.length === 0) return "";
|
|
12128
|
+
if (!this.skillReader) return "";
|
|
12129
|
+
const charBudget = budgetTokens * 4;
|
|
12130
|
+
let used = 0;
|
|
12131
|
+
const sections = [];
|
|
12132
|
+
let truncatedAt = -1;
|
|
12133
|
+
for (let i = 0; i < skillNames.length; i++) {
|
|
12134
|
+
const name = skillNames[i];
|
|
12135
|
+
const body = await this.skillReader.read(name);
|
|
12136
|
+
if (body === null) {
|
|
12137
|
+
this.logger.warn("inlineSkills: skill not found in registry", { name });
|
|
12138
|
+
continue;
|
|
12139
|
+
}
|
|
12140
|
+
const block = `### ${name}
|
|
12141
|
+
|
|
12142
|
+
${body}`;
|
|
12143
|
+
if (used + block.length > charBudget) {
|
|
12144
|
+
truncatedAt = i;
|
|
12145
|
+
break;
|
|
12146
|
+
}
|
|
12147
|
+
used += block.length;
|
|
12148
|
+
sections.push(block);
|
|
12149
|
+
}
|
|
12150
|
+
if (truncatedAt >= 0) {
|
|
12151
|
+
this.logger.warn(
|
|
12152
|
+
`inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
|
|
12153
|
+
);
|
|
12154
|
+
}
|
|
12155
|
+
if (sections.length === 0) return "";
|
|
12156
|
+
return `## Reference skills
|
|
12157
|
+
|
|
12158
|
+
${sections.join("\n\n")}
|
|
12159
|
+
`;
|
|
12160
|
+
}
|
|
12161
|
+
formatUpstream(id, entry, now, maxAgeMs) {
|
|
12162
|
+
if (!entry) {
|
|
12163
|
+
return `### ${id}
|
|
12164
|
+
|
|
12165
|
+
_[no prior run]_`;
|
|
12166
|
+
}
|
|
12167
|
+
const completedMs = Date.parse(entry.completedAt);
|
|
12168
|
+
if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
|
|
12169
|
+
return `### ${id} (last run ${entry.completedAt}, stale)
|
|
12170
|
+
|
|
12171
|
+
_[stale: omitted]_`;
|
|
12172
|
+
}
|
|
12173
|
+
const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
|
|
12174
|
+
const body = (entry.stdout ?? "").trim();
|
|
12175
|
+
const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
|
|
12176
|
+
|
|
12177
|
+
_[truncated]_` : body;
|
|
12178
|
+
return `${head}
|
|
12179
|
+
|
|
12180
|
+
${truncated || "_[no stdout captured]_"}`;
|
|
12181
|
+
}
|
|
12182
|
+
};
|
|
12183
|
+
var fallbackLogger3 = {
|
|
12184
|
+
info: () => {
|
|
12185
|
+
},
|
|
12186
|
+
warn: () => {
|
|
12187
|
+
},
|
|
12188
|
+
error: () => {
|
|
12189
|
+
}
|
|
12190
|
+
};
|
|
12191
|
+
|
|
12192
|
+
// src/maintenance/custom-task-validator.ts
|
|
12193
|
+
var import_types30 = require("@harness-engineering/types");
|
|
12194
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
12195
|
+
var REQUIRED_FIELDS_BY_TYPE = {
|
|
12196
|
+
"mechanical-ai": ["branch", "fixSkill"],
|
|
12197
|
+
"pure-ai": ["branch", "fixSkill"],
|
|
12198
|
+
"report-only": [],
|
|
12199
|
+
housekeeping: []
|
|
12200
|
+
};
|
|
12201
|
+
function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
12202
|
+
const errors = [];
|
|
12203
|
+
if (!customTasks) return (0, import_types30.Ok)(void 0);
|
|
12204
|
+
const builtInIds = new Set(builtIns.map((t) => t.id));
|
|
12205
|
+
const customIds = Object.keys(customTasks);
|
|
12206
|
+
const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
|
|
12207
|
+
for (const id of customIds) {
|
|
12208
|
+
const task = customTasks[id];
|
|
12209
|
+
if (!task) continue;
|
|
12210
|
+
validateOne(id, task, builtInIds, allIds, deps, errors);
|
|
12211
|
+
}
|
|
12212
|
+
detectCycles(customTasks, builtIns, errors);
|
|
12213
|
+
return errors.length === 0 ? (0, import_types30.Ok)(void 0) : (0, import_types30.Err)(errors);
|
|
12214
|
+
}
|
|
12215
|
+
function validateOne(id, task, builtInIds, allIds, deps, errors) {
|
|
12216
|
+
const prefix = `customTasks.${id}`;
|
|
12217
|
+
if (!ID_PATTERN.test(id)) {
|
|
12218
|
+
errors.push({
|
|
12219
|
+
path: prefix,
|
|
12220
|
+
message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
|
|
12221
|
+
});
|
|
12222
|
+
}
|
|
12223
|
+
if (builtInIds.has(id)) {
|
|
12224
|
+
errors.push({
|
|
12225
|
+
path: prefix,
|
|
12226
|
+
message: `task ID '${id}' collides with a built-in task; choose a different name`
|
|
12227
|
+
});
|
|
12228
|
+
}
|
|
12229
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
12230
|
+
errors.push({ path: `${prefix}.description`, message: "description is required" });
|
|
12231
|
+
}
|
|
12232
|
+
if (!task.schedule || task.schedule.trim().length === 0) {
|
|
12233
|
+
errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
|
|
12234
|
+
}
|
|
12235
|
+
validateCheckShape(prefix, task, errors);
|
|
12236
|
+
validateRequiredByType(prefix, task, errors);
|
|
12237
|
+
validateContextFrom(prefix, id, task, allIds, errors);
|
|
12238
|
+
validateInlineSkills(prefix, task, deps, errors);
|
|
12239
|
+
validateScriptPath(prefix, task, deps, errors);
|
|
12240
|
+
}
|
|
12241
|
+
function validateCheckShape(prefix, task, errors) {
|
|
12242
|
+
const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
|
|
12243
|
+
const hasScript = task.checkScript !== void 0;
|
|
12244
|
+
if (hasCommand && hasScript) {
|
|
12245
|
+
errors.push({
|
|
12246
|
+
path: prefix,
|
|
12247
|
+
message: "a task may declare checkCommand OR checkScript, not both"
|
|
12248
|
+
});
|
|
12249
|
+
}
|
|
12250
|
+
const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
|
|
12251
|
+
if (needsCheck && !hasCommand && !hasScript) {
|
|
12252
|
+
errors.push({
|
|
12253
|
+
path: prefix,
|
|
12254
|
+
message: `${task.type} task must declare either checkCommand or checkScript`
|
|
12255
|
+
});
|
|
12256
|
+
}
|
|
12257
|
+
if (hasScript) {
|
|
12258
|
+
const path24 = task.checkScript?.path;
|
|
12259
|
+
if (!path24 || path24.trim().length === 0) {
|
|
12260
|
+
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
12261
|
+
}
|
|
12262
|
+
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
12263
|
+
errors.push({
|
|
12264
|
+
path: `${prefix}.checkScript.timeoutMs`,
|
|
12265
|
+
message: "timeoutMs must be a positive integer"
|
|
12266
|
+
});
|
|
12267
|
+
}
|
|
12268
|
+
}
|
|
12269
|
+
}
|
|
12270
|
+
function validateRequiredByType(prefix, task, errors) {
|
|
12271
|
+
const required = REQUIRED_FIELDS_BY_TYPE[task.type];
|
|
12272
|
+
if (!required) {
|
|
12273
|
+
errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
|
|
12274
|
+
return;
|
|
12275
|
+
}
|
|
12276
|
+
for (const field of required) {
|
|
12277
|
+
const value = task[field];
|
|
12278
|
+
if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
|
|
12279
|
+
errors.push({
|
|
12280
|
+
path: `${prefix}.${String(field)}`,
|
|
12281
|
+
message: `${task.type} task requires ${String(field)}`
|
|
12282
|
+
});
|
|
12283
|
+
}
|
|
12284
|
+
}
|
|
12285
|
+
if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
|
|
12286
|
+
errors.push({
|
|
12287
|
+
path: `${prefix}.branch`,
|
|
12288
|
+
message: `${task.type} task requires a non-null branch`
|
|
12289
|
+
});
|
|
12290
|
+
}
|
|
12291
|
+
}
|
|
12292
|
+
function validateContextFrom(prefix, selfId, task, allIds, errors) {
|
|
12293
|
+
if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
|
|
12294
|
+
errors.push({
|
|
12295
|
+
path: `${prefix}.contextFromMaxAgeMinutes`,
|
|
12296
|
+
message: "contextFromMaxAgeMinutes must be a positive integer"
|
|
12297
|
+
});
|
|
12298
|
+
}
|
|
12299
|
+
if (!task.contextFrom) return;
|
|
12300
|
+
for (let i = 0; i < task.contextFrom.length; i++) {
|
|
12301
|
+
const upstreamId = task.contextFrom[i];
|
|
12302
|
+
if (!upstreamId) continue;
|
|
12303
|
+
if (upstreamId === selfId) {
|
|
12304
|
+
errors.push({
|
|
12305
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
12306
|
+
message: `task '${selfId}' cannot reference itself in contextFrom`
|
|
12307
|
+
});
|
|
12308
|
+
}
|
|
12309
|
+
if (!allIds.has(upstreamId)) {
|
|
12310
|
+
errors.push({
|
|
12311
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
12312
|
+
message: `references unknown task '${upstreamId}'`
|
|
12313
|
+
});
|
|
12314
|
+
}
|
|
12315
|
+
}
|
|
12316
|
+
}
|
|
12317
|
+
function validateInlineSkills(prefix, task, deps, errors) {
|
|
12318
|
+
if (!task.inlineSkills) return;
|
|
12319
|
+
if (!deps.skillExists) return;
|
|
12320
|
+
for (let i = 0; i < task.inlineSkills.length; i++) {
|
|
12321
|
+
const name = task.inlineSkills[i];
|
|
12322
|
+
if (!name) continue;
|
|
12323
|
+
if (!deps.skillExists(name)) {
|
|
12324
|
+
errors.push({
|
|
12325
|
+
path: `${prefix}.inlineSkills[${i}]`,
|
|
12326
|
+
message: `skill '${name}' not found in the registry`
|
|
12327
|
+
});
|
|
12328
|
+
}
|
|
12329
|
+
}
|
|
12330
|
+
if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
|
|
12331
|
+
errors.push({
|
|
12332
|
+
path: `${prefix}.inlineSkillsBudgetTokens`,
|
|
12333
|
+
message: "inlineSkillsBudgetTokens must be a positive integer"
|
|
12334
|
+
});
|
|
12335
|
+
}
|
|
12336
|
+
}
|
|
12337
|
+
function validateScriptPath(prefix, task, deps, errors) {
|
|
12338
|
+
if (!task.checkScript?.path) return;
|
|
12339
|
+
if (!deps.scriptExists) return;
|
|
12340
|
+
if (!deps.scriptExists(task.checkScript.path)) {
|
|
12341
|
+
errors.push({
|
|
12342
|
+
path: `${prefix}.checkScript.path`,
|
|
12343
|
+
message: `executable not found: ${task.checkScript.path}`
|
|
12344
|
+
});
|
|
12345
|
+
}
|
|
12346
|
+
}
|
|
12347
|
+
function detectCycles(customTasks, builtIns, errors) {
|
|
12348
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
12349
|
+
for (const t of builtIns) adjacency.set(t.id, []);
|
|
12350
|
+
for (const [id, task] of Object.entries(customTasks)) {
|
|
12351
|
+
adjacency.set(id, (task.contextFrom ?? []).slice());
|
|
12352
|
+
}
|
|
12353
|
+
const color = /* @__PURE__ */ new Map();
|
|
12354
|
+
for (const id of adjacency.keys()) color.set(id, "white");
|
|
12355
|
+
const reported = /* @__PURE__ */ new Set();
|
|
12356
|
+
for (const id of Object.keys(customTasks)) {
|
|
12357
|
+
if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
|
|
12358
|
+
}
|
|
12359
|
+
}
|
|
12360
|
+
function visitFromRoot(start, adjacency, color, errors, reported) {
|
|
12361
|
+
const stack = [{ id: start, nextIdx: 0, path: [start] }];
|
|
12362
|
+
color.set(start, "grey");
|
|
12363
|
+
while (stack.length) {
|
|
12364
|
+
const top = stack[stack.length - 1];
|
|
12365
|
+
const neighbors = adjacency.get(top.id) ?? [];
|
|
12366
|
+
if (top.nextIdx >= neighbors.length) {
|
|
12367
|
+
color.set(top.id, "black");
|
|
12368
|
+
stack.pop();
|
|
12369
|
+
continue;
|
|
12370
|
+
}
|
|
12371
|
+
const next = neighbors[top.nextIdx++];
|
|
12372
|
+
if (!next || !adjacency.has(next)) continue;
|
|
12373
|
+
handleEdge(top, next, color, stack, errors, reported);
|
|
12374
|
+
}
|
|
12375
|
+
}
|
|
12376
|
+
function handleEdge(top, next, color, stack, errors, reported) {
|
|
12377
|
+
const nextColor = color.get(next);
|
|
12378
|
+
if (nextColor === "grey") {
|
|
12379
|
+
reportCycle(top.path, next, errors, reported);
|
|
12380
|
+
} else if (nextColor === "white") {
|
|
12381
|
+
color.set(next, "grey");
|
|
12382
|
+
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
12383
|
+
}
|
|
12384
|
+
}
|
|
12385
|
+
function reportCycle(path24, next, errors, reported) {
|
|
12386
|
+
const cycleStart = path24.indexOf(next);
|
|
12387
|
+
const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
|
|
12388
|
+
const key = cyclePath.join("\u2192");
|
|
12389
|
+
if (reported.has(key)) return;
|
|
12390
|
+
reported.add(key);
|
|
12391
|
+
errors.push({
|
|
12392
|
+
path: `customTasks.${cyclePath[0]}.contextFrom`,
|
|
12393
|
+
message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
|
|
12394
|
+
});
|
|
12395
|
+
}
|
|
12396
|
+
|
|
12397
|
+
// src/orchestrator.ts
|
|
10479
12398
|
var Orchestrator = class extends import_node_events.EventEmitter {
|
|
10480
12399
|
state;
|
|
10481
12400
|
config;
|
|
@@ -10500,6 +12419,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10500
12419
|
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
10501
12420
|
*/
|
|
10502
12421
|
backendFactory;
|
|
12422
|
+
/**
|
|
12423
|
+
* Spec B Phase 4 (D8): per-orchestrator in-process bus for
|
|
12424
|
+
* `RoutingDecision` events. Constructed alongside backendFactory when
|
|
12425
|
+
* agent.backends synthesis succeeds; null when legacy single-backend
|
|
12426
|
+
* config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
|
|
12427
|
+
* subscribe via `getRoutingDecisionBus()`.
|
|
12428
|
+
*/
|
|
12429
|
+
routingDecisionBus;
|
|
10503
12430
|
/**
|
|
10504
12431
|
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
10505
12432
|
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
@@ -10522,6 +12449,15 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10522
12449
|
* so this map is the single source of truth post-migration.
|
|
10523
12450
|
*/
|
|
10524
12451
|
localResolvers = /* @__PURE__ */ new Map();
|
|
12452
|
+
/**
|
|
12453
|
+
* Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
|
|
12454
|
+
* construction from `projectRoot/agents/skills/`. Consulted by
|
|
12455
|
+
* `buildRoutingUseCase` at dispatch start to construct
|
|
12456
|
+
* `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
|
|
12457
|
+
* Empty when the orchestrator runs outside a harness project root
|
|
12458
|
+
* (then dispatch falls through to per-tier, preserving F11/N2).
|
|
12459
|
+
*/
|
|
12460
|
+
skillCatalog;
|
|
10525
12461
|
/**
|
|
10526
12462
|
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
10527
12463
|
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
@@ -10570,7 +12506,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10570
12506
|
completionHandler;
|
|
10571
12507
|
/** Project root directory, derived from workspace root. */
|
|
10572
12508
|
get projectRoot() {
|
|
10573
|
-
return
|
|
12509
|
+
return path21.resolve(this.config.workspace.root, "..", "..");
|
|
10574
12510
|
}
|
|
10575
12511
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
10576
12512
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -10614,6 +12550,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10614
12550
|
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
10615
12551
|
);
|
|
10616
12552
|
}
|
|
12553
|
+
const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
|
|
12554
|
+
this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
|
|
12555
|
+
if (this.skillCatalog.length === 0) {
|
|
12556
|
+
this.logger.warn(
|
|
12557
|
+
`Spec B Phase 3: skill catalog discovery returned 0 entries; per-skill / per-mode routing will fall through to per-tier. Looked under ${path21.join(skillCatalogRoot, "agents/skills")}.`
|
|
12558
|
+
);
|
|
12559
|
+
}
|
|
10617
12560
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
10618
12561
|
this.workspace = new WorkspaceManager(config.workspace, {
|
|
10619
12562
|
emitEvent: (event) => {
|
|
@@ -10625,10 +12568,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10625
12568
|
this.renderer = new PromptRenderer();
|
|
10626
12569
|
this.overrideBackend = overrides?.backend ?? null;
|
|
10627
12570
|
this.interactionQueue = new InteractionQueue(
|
|
10628
|
-
|
|
12571
|
+
path21.join(config.workspace.root, "..", "interactions"),
|
|
10629
12572
|
this
|
|
10630
12573
|
);
|
|
10631
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12574
|
+
this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
|
|
10632
12575
|
const backendsMap = this.config.agent.backends ?? {};
|
|
10633
12576
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
10634
12577
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -10642,13 +12585,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10642
12585
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
10643
12586
|
}
|
|
10644
12587
|
}
|
|
10645
|
-
this.cacheMetrics = new
|
|
12588
|
+
this.cacheMetrics = new import_core16.CacheMetricsRecorder();
|
|
10646
12589
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
10647
12590
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
10648
12591
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
10649
12592
|
const routing = this.config.agent.routing ?? {
|
|
10650
12593
|
default: firstBackendName ?? "primary"
|
|
10651
12594
|
};
|
|
12595
|
+
this.routingDecisionBus = new RoutingDecisionBus({
|
|
12596
|
+
capacity: 500,
|
|
12597
|
+
logger: this.logger
|
|
12598
|
+
});
|
|
10652
12599
|
this.backendFactory = new OrchestratorBackendFactory({
|
|
10653
12600
|
backends: this.config.agent.backends,
|
|
10654
12601
|
routing,
|
|
@@ -10656,6 +12603,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10656
12603
|
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
10657
12604
|
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
10658
12605
|
cacheMetrics: this.cacheMetrics,
|
|
12606
|
+
decisionBus: this.routingDecisionBus,
|
|
10659
12607
|
getResolverModelFor: (name) => {
|
|
10660
12608
|
const resolver = this.localResolvers.get(name);
|
|
10661
12609
|
return resolver ? () => resolver.resolveModel() : void 0;
|
|
@@ -10663,6 +12611,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10663
12611
|
});
|
|
10664
12612
|
} else {
|
|
10665
12613
|
this.backendFactory = null;
|
|
12614
|
+
this.routingDecisionBus = null;
|
|
10666
12615
|
}
|
|
10667
12616
|
this.pipeline = null;
|
|
10668
12617
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
@@ -10672,7 +12621,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10672
12621
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
10673
12622
|
});
|
|
10674
12623
|
this.recorder = new StreamRecorder(
|
|
10675
|
-
|
|
12624
|
+
path21.resolve(config.workspace.root, "..", "streams"),
|
|
10676
12625
|
this.logger
|
|
10677
12626
|
);
|
|
10678
12627
|
const self = this;
|
|
@@ -10703,10 +12652,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10703
12652
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
10704
12653
|
if (config.server?.port) {
|
|
10705
12654
|
const webhookStore = new WebhookStore(
|
|
10706
|
-
|
|
12655
|
+
path21.join(this.projectRoot, ".harness", "webhooks.json")
|
|
10707
12656
|
);
|
|
10708
12657
|
this.webhookQueue = new WebhookQueue(
|
|
10709
|
-
|
|
12658
|
+
path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
10710
12659
|
);
|
|
10711
12660
|
const webhookDelivery = new WebhookDelivery({
|
|
10712
12661
|
queue: this.webhookQueue,
|
|
@@ -10722,7 +12671,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10722
12671
|
this.setupNotifications(config.notifications);
|
|
10723
12672
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
10724
12673
|
if (otlpCfg) {
|
|
10725
|
-
this.otlpExporter = new
|
|
12674
|
+
this.otlpExporter = new import_core16.OTLPExporter({
|
|
10726
12675
|
endpoint: otlpCfg.endpoint,
|
|
10727
12676
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
10728
12677
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -10744,7 +12693,16 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10744
12693
|
queue: this.webhookQueue
|
|
10745
12694
|
},
|
|
10746
12695
|
cacheMetrics: this.cacheMetrics,
|
|
10747
|
-
|
|
12696
|
+
// Spec B Phase 5: routing observability accessors. Closures so the
|
|
12697
|
+
// server re-reads on every request — stop() / start() do not
|
|
12698
|
+
// require server reconstruction. Returns null if no backendFactory
|
|
12699
|
+
// (legacy single-backend configs), and the route handler renders
|
|
12700
|
+
// 503 in that case.
|
|
12701
|
+
getBackendRouter: () => this.getBackendRouter(),
|
|
12702
|
+
getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
|
|
12703
|
+
getRoutingConfig: () => this.getRoutingConfig(),
|
|
12704
|
+
getBackends: () => this.getBackends(),
|
|
12705
|
+
plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
10748
12706
|
pipeline: this.pipeline,
|
|
10749
12707
|
analysisArchive: this.analysisArchive,
|
|
10750
12708
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -10782,7 +12740,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10782
12740
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
10783
12741
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
10784
12742
|
};
|
|
10785
|
-
const clientResult = (0,
|
|
12743
|
+
const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
|
|
10786
12744
|
if (!clientResult.ok) throw clientResult.error;
|
|
10787
12745
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
10788
12746
|
}
|
|
@@ -10800,13 +12758,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10800
12758
|
const logger = this.logger;
|
|
10801
12759
|
const checkRunner = {
|
|
10802
12760
|
run: async (command, cwd) => {
|
|
10803
|
-
const { execFile:
|
|
10804
|
-
const { promisify:
|
|
10805
|
-
const
|
|
12761
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12762
|
+
const { promisify: promisify5 } = await import("util");
|
|
12763
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10806
12764
|
const [cmd, ...args] = command;
|
|
10807
12765
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
10808
12766
|
try {
|
|
10809
|
-
const { stdout } = await
|
|
12767
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10810
12768
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
10811
12769
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
10812
12770
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -10835,13 +12793,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10835
12793
|
};
|
|
10836
12794
|
const commandExecutor = {
|
|
10837
12795
|
exec: async (command, cwd) => {
|
|
10838
|
-
const { execFile:
|
|
10839
|
-
const { promisify:
|
|
10840
|
-
const
|
|
12796
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12797
|
+
const { promisify: promisify5 } = await import("util");
|
|
12798
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10841
12799
|
const [cmd, ...args] = command;
|
|
10842
12800
|
if (!cmd) return { stdout: "" };
|
|
10843
12801
|
try {
|
|
10844
|
-
const { stdout } = await
|
|
12802
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10845
12803
|
return { stdout: String(stdout) };
|
|
10846
12804
|
} catch (err) {
|
|
10847
12805
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -10853,12 +12811,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10853
12811
|
}
|
|
10854
12812
|
}
|
|
10855
12813
|
};
|
|
12814
|
+
const outputStore = new TaskOutputStore({
|
|
12815
|
+
rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12816
|
+
logger: this.logger
|
|
12817
|
+
});
|
|
12818
|
+
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
12819
|
+
const skillReader = {
|
|
12820
|
+
// The orchestrator does not own the skill registry; CLI-side skill
|
|
12821
|
+
// resolution wires this in via direct injection. Default: skill not
|
|
12822
|
+
// resolvable from the orchestrator boundary.
|
|
12823
|
+
read: async () => null
|
|
12824
|
+
};
|
|
12825
|
+
const contextResolver = new ContextResolver({
|
|
12826
|
+
outputStore,
|
|
12827
|
+
skillReader,
|
|
12828
|
+
logger: this.logger
|
|
12829
|
+
});
|
|
10856
12830
|
return new TaskRunner({
|
|
10857
12831
|
config: maintenanceConfig,
|
|
10858
12832
|
checkRunner,
|
|
10859
12833
|
agentDispatcher,
|
|
10860
12834
|
commandExecutor,
|
|
10861
|
-
cwd: this.projectRoot
|
|
12835
|
+
cwd: this.projectRoot,
|
|
12836
|
+
checkScriptRunner,
|
|
12837
|
+
contextResolver,
|
|
12838
|
+
outputStore
|
|
10862
12839
|
});
|
|
10863
12840
|
}
|
|
10864
12841
|
/**
|
|
@@ -10866,8 +12843,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10866
12843
|
* Extracted from start() to keep function length under threshold.
|
|
10867
12844
|
*/
|
|
10868
12845
|
async initMaintenance(maintenanceConfig) {
|
|
12846
|
+
const validation = validateCustomTasks(
|
|
12847
|
+
maintenanceConfig.customTasks,
|
|
12848
|
+
BUILT_IN_TASKS
|
|
12849
|
+
);
|
|
12850
|
+
if (!validation.ok) {
|
|
12851
|
+
const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
12852
|
+
throw new Error(`Invalid maintenance.customTasks configuration:
|
|
12853
|
+
${messages}`);
|
|
12854
|
+
}
|
|
10869
12855
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
10870
|
-
persistDir:
|
|
12856
|
+
persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
10871
12857
|
logger: this.logger
|
|
10872
12858
|
});
|
|
10873
12859
|
await this.maintenanceReporter.load();
|
|
@@ -10918,10 +12904,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10918
12904
|
}
|
|
10919
12905
|
}
|
|
10920
12906
|
createIntelligencePipeline() {
|
|
12907
|
+
if (!this.backendFactory) {
|
|
12908
|
+
this.logger.warn(
|
|
12909
|
+
"intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
|
|
12910
|
+
);
|
|
12911
|
+
return null;
|
|
12912
|
+
}
|
|
10921
12913
|
const bundle = buildIntelligencePipeline({
|
|
10922
12914
|
config: this.config,
|
|
10923
12915
|
localResolvers: this.localResolvers,
|
|
10924
|
-
logger: this.logger
|
|
12916
|
+
logger: this.logger,
|
|
12917
|
+
router: this.backendFactory.getRouter()
|
|
10925
12918
|
});
|
|
10926
12919
|
if (!bundle) return null;
|
|
10927
12920
|
this.graphStore = bundle.graphStore;
|
|
@@ -10972,11 +12965,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10972
12965
|
simulationResults,
|
|
10973
12966
|
personaRecommendations
|
|
10974
12967
|
} = pipelineResult ?? {};
|
|
12968
|
+
const selfAssignee = await this.orchestratorIdPromise;
|
|
10975
12969
|
const tickEvent = {
|
|
10976
12970
|
type: "tick",
|
|
10977
12971
|
candidates,
|
|
10978
12972
|
runningStates: runningStatesResult.value,
|
|
10979
12973
|
nowMs,
|
|
12974
|
+
selfAssignee,
|
|
10980
12975
|
...concernSignals !== void 0 && { concernSignals },
|
|
10981
12976
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
10982
12977
|
...complexityScores !== void 0 && { complexityScores },
|
|
@@ -11306,12 +13301,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11306
13301
|
async postLifecycleComment(identifier, externalId, event) {
|
|
11307
13302
|
try {
|
|
11308
13303
|
if (!externalId) return;
|
|
11309
|
-
const trackerConfig = (0,
|
|
13304
|
+
const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
|
|
11310
13305
|
if (!trackerConfig) return;
|
|
11311
13306
|
const token = process.env.GITHUB_TOKEN;
|
|
11312
13307
|
if (!token) return;
|
|
11313
13308
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
11314
|
-
const adapter = new
|
|
13309
|
+
const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
11315
13310
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
11316
13311
|
const actionMap = {
|
|
11317
13312
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -11384,7 +13379,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11384
13379
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
11385
13380
|
}))
|
|
11386
13381
|
);
|
|
11387
|
-
(0,
|
|
13382
|
+
(0, import_core14.writeTaint)(
|
|
11388
13383
|
workspacePath,
|
|
11389
13384
|
issue.id,
|
|
11390
13385
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -11400,14 +13395,24 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11400
13395
|
issue,
|
|
11401
13396
|
attempt: attempt || 1
|
|
11402
13397
|
});
|
|
11403
|
-
const useCase =
|
|
13398
|
+
const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
|
|
13399
|
+
const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
|
|
13400
|
+
const routerOpts = invocationOverride ? { invocationOverride } : void 0;
|
|
13401
|
+
if (invocationOverride) {
|
|
13402
|
+
this.logger.info(
|
|
13403
|
+
`Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
|
|
13404
|
+
{ issueId: issue.id }
|
|
13405
|
+
);
|
|
13406
|
+
}
|
|
11404
13407
|
let routedBackendName;
|
|
11405
13408
|
if (this.overrideBackend !== null) {
|
|
11406
13409
|
routedBackendName = this.overrideBackend.name;
|
|
11407
13410
|
} else if (this.backendFactory !== null) {
|
|
11408
|
-
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
13411
|
+
routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
|
|
11409
13412
|
} else {
|
|
11410
|
-
|
|
13413
|
+
const routingDefault = this.config.agent.routing?.default;
|
|
13414
|
+
const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
|
|
13415
|
+
routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
|
|
11411
13416
|
}
|
|
11412
13417
|
const session = {
|
|
11413
13418
|
sessionId: `pending-${Date.now()}`,
|
|
@@ -11446,7 +13451,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11446
13451
|
if (this.overrideBackend !== null) {
|
|
11447
13452
|
agentBackend = this.overrideBackend;
|
|
11448
13453
|
} else if (this.backendFactory !== null) {
|
|
11449
|
-
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
13454
|
+
agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
|
|
11450
13455
|
} else {
|
|
11451
13456
|
throw new Error(
|
|
11452
13457
|
`Cannot dispatch ${issue.identifier}: agent.backends not synthesized (migration failed) and no override backend supplied. Migrate to agent.backends/agent.routing per docs/guides/multi-backend-routing.md.`
|
|
@@ -11736,6 +13741,8 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11736
13741
|
unsub();
|
|
11737
13742
|
}
|
|
11738
13743
|
this.localModelStatusUnsubscribes = [];
|
|
13744
|
+
this.routingDecisionBus?.clearListeners();
|
|
13745
|
+
this.routingDecisionBus = null;
|
|
11739
13746
|
for (const resolver of this.localResolvers.values()) {
|
|
11740
13747
|
resolver.stop();
|
|
11741
13748
|
}
|
|
@@ -11809,6 +13816,42 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11809
13816
|
tickActivity: this.tickActivity
|
|
11810
13817
|
};
|
|
11811
13818
|
}
|
|
13819
|
+
/**
|
|
13820
|
+
* Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
|
|
13821
|
+
* Phase 7 (dashboard WS broadcast). Returns null when the legacy
|
|
13822
|
+
* single-backend config bypassed agent.backends synthesis.
|
|
13823
|
+
*/
|
|
13824
|
+
getRoutingDecisionBus() {
|
|
13825
|
+
return this.routingDecisionBus;
|
|
13826
|
+
}
|
|
13827
|
+
/**
|
|
13828
|
+
* Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
|
|
13829
|
+
* dispatch path uses the factory-owned router directly; observability
|
|
13830
|
+
* routes (config / decisions) reach it through this accessor. Returns
|
|
13831
|
+
* null when the legacy single-backend config bypassed agent.backends
|
|
13832
|
+
* synthesis (no backendFactory built).
|
|
13833
|
+
*/
|
|
13834
|
+
getBackendRouter() {
|
|
13835
|
+
return this.backendFactory?.getRouter() ?? null;
|
|
13836
|
+
}
|
|
13837
|
+
/**
|
|
13838
|
+
* Spec B Phase 5: snapshot of the active RoutingConfig for the config
|
|
13839
|
+
* route and the trace route's bus-less router construction. Returns
|
|
13840
|
+
* null when the operator's harness.config.json carries no
|
|
13841
|
+
* `agent.routing` block.
|
|
13842
|
+
*/
|
|
13843
|
+
getRoutingConfig() {
|
|
13844
|
+
return this.config.agent.routing ?? null;
|
|
13845
|
+
}
|
|
13846
|
+
/**
|
|
13847
|
+
* Spec B Phase 5: snapshot of `agent.backends` for the config route
|
|
13848
|
+
* (existence annotations) and the trace route (bus-less router
|
|
13849
|
+
* construction). Returns null when no synthesized backends map exists
|
|
13850
|
+
* (legacy single-backend configs).
|
|
13851
|
+
*/
|
|
13852
|
+
getBackends() {
|
|
13853
|
+
return this.config.agent.backends ?? null;
|
|
13854
|
+
}
|
|
11812
13855
|
/** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
|
|
11813
13856
|
getMaintenanceStatus() {
|
|
11814
13857
|
return this.maintenanceScheduler?.getStatus() ?? null;
|
|
@@ -12043,11 +14086,11 @@ function launchTUI(orchestrator) {
|
|
|
12043
14086
|
}
|
|
12044
14087
|
|
|
12045
14088
|
// src/maintenance/sync-main.ts
|
|
12046
|
-
var
|
|
12047
|
-
var
|
|
14089
|
+
var import_node_child_process12 = require("child_process");
|
|
14090
|
+
var import_node_util4 = require("util");
|
|
12048
14091
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
12049
14092
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
12050
|
-
const exec = (0,
|
|
14093
|
+
const exec = (0, import_node_util4.promisify)(execFileFn);
|
|
12051
14094
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
12052
14095
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
12053
14096
|
}
|
|
@@ -12109,7 +14152,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
12109
14152
|
}
|
|
12110
14153
|
}
|
|
12111
14154
|
async function syncMain(repoRoot, opts = {}) {
|
|
12112
|
-
const execFileFn = opts.execFileFn ??
|
|
14155
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
|
|
12113
14156
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
12114
14157
|
try {
|
|
12115
14158
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
@@ -12187,10 +14230,10 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
12187
14230
|
}
|
|
12188
14231
|
|
|
12189
14232
|
// src/sessions/search-index.ts
|
|
12190
|
-
var
|
|
12191
|
-
var
|
|
14233
|
+
var fs18 = __toESM(require("fs"));
|
|
14234
|
+
var path22 = __toESM(require("path"));
|
|
12192
14235
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
12193
|
-
var
|
|
14236
|
+
var import_types31 = require("@harness-engineering/types");
|
|
12194
14237
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
12195
14238
|
var SCHEMA_SQL2 = `
|
|
12196
14239
|
CREATE TABLE IF NOT EXISTS session_docs (
|
|
@@ -12233,7 +14276,7 @@ function normalizeFts5Query(query) {
|
|
|
12233
14276
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
12234
14277
|
}
|
|
12235
14278
|
function searchIndexPath(projectPath) {
|
|
12236
|
-
return
|
|
14279
|
+
return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
12237
14280
|
}
|
|
12238
14281
|
var FILE_KIND_TO_FILENAME = {
|
|
12239
14282
|
summary: "summary.md",
|
|
@@ -12248,7 +14291,7 @@ var SqliteSearchIndex = class {
|
|
|
12248
14291
|
removeSessionStmt;
|
|
12249
14292
|
totalStmt;
|
|
12250
14293
|
constructor(dbPath) {
|
|
12251
|
-
|
|
14294
|
+
fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
|
|
12252
14295
|
this.db = new import_better_sqlite32.default(dbPath);
|
|
12253
14296
|
this.db.pragma("journal_mode = WAL");
|
|
12254
14297
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -12348,19 +14391,19 @@ function openSearchIndex(projectPath) {
|
|
|
12348
14391
|
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
12349
14392
|
}
|
|
12350
14393
|
function indexSessionDirectory(idx, args) {
|
|
12351
|
-
const kinds = args.fileKinds ?? [...
|
|
14394
|
+
const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
|
|
12352
14395
|
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
12353
14396
|
let docsWritten = 0;
|
|
12354
14397
|
for (const kind of kinds) {
|
|
12355
14398
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
12356
|
-
const filePath =
|
|
12357
|
-
if (!
|
|
12358
|
-
let body =
|
|
14399
|
+
const filePath = path22.join(args.sessionDir, fileName);
|
|
14400
|
+
if (!fs18.existsSync(filePath)) continue;
|
|
14401
|
+
let body = fs18.readFileSync(filePath, "utf8");
|
|
12359
14402
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
12360
14403
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
12361
14404
|
}
|
|
12362
|
-
const stat =
|
|
12363
|
-
const relPath =
|
|
14405
|
+
const stat = fs18.statSync(filePath);
|
|
14406
|
+
const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
12364
14407
|
idx.upsertSessionDoc({
|
|
12365
14408
|
sessionId: args.sessionId,
|
|
12366
14409
|
archived: args.archived,
|
|
@@ -12375,17 +14418,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
12375
14418
|
}
|
|
12376
14419
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
12377
14420
|
const start = Date.now();
|
|
12378
|
-
const archiveBase =
|
|
14421
|
+
const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
|
|
12379
14422
|
const idx = openSearchIndex(projectPath);
|
|
12380
14423
|
try {
|
|
12381
14424
|
idx.resetArchived();
|
|
12382
14425
|
let sessionsIndexed = 0;
|
|
12383
14426
|
let docsWritten = 0;
|
|
12384
|
-
if (
|
|
12385
|
-
const entries =
|
|
14427
|
+
if (fs18.existsSync(archiveBase)) {
|
|
14428
|
+
const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
|
|
12386
14429
|
for (const entry of entries) {
|
|
12387
14430
|
if (!entry.isDirectory()) continue;
|
|
12388
|
-
const sessionDir =
|
|
14431
|
+
const sessionDir = path22.join(archiveBase, entry.name);
|
|
12389
14432
|
const result = indexSessionDirectory(idx, {
|
|
12390
14433
|
sessionId: entry.name,
|
|
12391
14434
|
sessionDir,
|
|
@@ -12405,10 +14448,10 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
12405
14448
|
}
|
|
12406
14449
|
|
|
12407
14450
|
// src/sessions/summarize.ts
|
|
12408
|
-
var
|
|
12409
|
-
var
|
|
12410
|
-
var
|
|
12411
|
-
var
|
|
14451
|
+
var fs19 = __toESM(require("fs"));
|
|
14452
|
+
var path23 = __toESM(require("path"));
|
|
14453
|
+
var import_types32 = require("@harness-engineering/types");
|
|
14454
|
+
var import_types33 = require("@harness-engineering/types");
|
|
12412
14455
|
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
12413
14456
|
var SUMMARY_INPUT_FILES = [
|
|
12414
14457
|
{ filename: "summary.md", kind: "summary" },
|
|
@@ -12434,10 +14477,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
12434
14477
|
function readInputCorpus(archiveDir) {
|
|
12435
14478
|
const parts = [];
|
|
12436
14479
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
12437
|
-
const p =
|
|
12438
|
-
if (!
|
|
14480
|
+
const p = path23.join(archiveDir, filename);
|
|
14481
|
+
if (!fs19.existsSync(p)) continue;
|
|
12439
14482
|
try {
|
|
12440
|
-
const content =
|
|
14483
|
+
const content = fs19.readFileSync(p, "utf8");
|
|
12441
14484
|
if (content.trim().length === 0) continue;
|
|
12442
14485
|
parts.push(`## FILE: ${kind}
|
|
12443
14486
|
|
|
@@ -12488,7 +14531,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
12488
14531
|
return lines.join("\n");
|
|
12489
14532
|
}
|
|
12490
14533
|
function writeStubMarkdown(archiveDir, reason) {
|
|
12491
|
-
const filePath =
|
|
14534
|
+
const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
|
|
12492
14535
|
const body = `---
|
|
12493
14536
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
12494
14537
|
schemaVersion: 1
|
|
@@ -12499,17 +14542,17 @@ status: failed
|
|
|
12499
14542
|
|
|
12500
14543
|
- reason: ${reason}
|
|
12501
14544
|
`;
|
|
12502
|
-
|
|
14545
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
12503
14546
|
return filePath;
|
|
12504
14547
|
}
|
|
12505
14548
|
async function summarizeArchivedSession(ctx) {
|
|
12506
14549
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
12507
|
-
if (!
|
|
12508
|
-
return (0,
|
|
14550
|
+
if (!fs19.existsSync(ctx.archiveDir)) {
|
|
14551
|
+
return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
12509
14552
|
}
|
|
12510
14553
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
12511
14554
|
if (corpus.trim().length === 0) {
|
|
12512
|
-
return (0,
|
|
14555
|
+
return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
12513
14556
|
}
|
|
12514
14557
|
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
12515
14558
|
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
@@ -12518,7 +14561,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12518
14561
|
const analyzeOpts = {
|
|
12519
14562
|
prompt,
|
|
12520
14563
|
systemPrompt: SYSTEM_PROMPT,
|
|
12521
|
-
responseSchema:
|
|
14564
|
+
responseSchema: import_types32.SessionSummarySchema,
|
|
12522
14565
|
...ctx.config?.model && { model: ctx.config.model }
|
|
12523
14566
|
};
|
|
12524
14567
|
let response;
|
|
@@ -12542,11 +14585,11 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12542
14585
|
} catch {
|
|
12543
14586
|
}
|
|
12544
14587
|
}
|
|
12545
|
-
return (0,
|
|
14588
|
+
return (0, import_types33.Err)(
|
|
12546
14589
|
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
12547
14590
|
);
|
|
12548
14591
|
}
|
|
12549
|
-
const parsed =
|
|
14592
|
+
const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
|
|
12550
14593
|
if (!parsed.success) {
|
|
12551
14594
|
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
12552
14595
|
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
@@ -12556,7 +14599,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12556
14599
|
} catch {
|
|
12557
14600
|
}
|
|
12558
14601
|
}
|
|
12559
|
-
return (0,
|
|
14602
|
+
return (0, import_types33.Err)(new Error(reason));
|
|
12560
14603
|
}
|
|
12561
14604
|
const meta = {
|
|
12562
14605
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12565,10 +14608,10 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12565
14608
|
outputTokens: response.tokenUsage.outputTokens,
|
|
12566
14609
|
schemaVersion: 1
|
|
12567
14610
|
};
|
|
12568
|
-
const filePath =
|
|
14611
|
+
const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
12569
14612
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
12570
|
-
|
|
12571
|
-
return (0,
|
|
14613
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
14614
|
+
return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
|
|
12572
14615
|
}
|
|
12573
14616
|
function isSummaryEnabled(config) {
|
|
12574
14617
|
if (!config) return false;
|
|
@@ -12645,8 +14688,11 @@ function buildArchiveHooks(opts) {
|
|
|
12645
14688
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12646
14689
|
0 && (module.exports = {
|
|
12647
14690
|
AnalysisArchive,
|
|
14691
|
+
BUILT_IN_TASKS,
|
|
12648
14692
|
BackendRouter,
|
|
12649
14693
|
ClaimManager,
|
|
14694
|
+
GateNotReadyError,
|
|
14695
|
+
GateRunError,
|
|
12650
14696
|
InteractionQueue,
|
|
12651
14697
|
LinearGraphQLStub,
|
|
12652
14698
|
MAX_ATTEMPTS,
|
|
@@ -12655,6 +14701,7 @@ function buildArchiveHooks(opts) {
|
|
|
12655
14701
|
Orchestrator,
|
|
12656
14702
|
OrchestratorBackendFactory,
|
|
12657
14703
|
PRDetector,
|
|
14704
|
+
PromotionError,
|
|
12658
14705
|
PromptRenderer,
|
|
12659
14706
|
RETRY_DELAYS_MS,
|
|
12660
14707
|
RoadmapTrackerAdapter,
|
|
@@ -12663,6 +14710,7 @@ function buildArchiveHooks(opts) {
|
|
|
12663
14710
|
SlackSink,
|
|
12664
14711
|
SqliteSearchIndex,
|
|
12665
14712
|
StreamRecorder,
|
|
14713
|
+
TaskOutputStore,
|
|
12666
14714
|
TokenStore,
|
|
12667
14715
|
WebhookQueue,
|
|
12668
14716
|
WorkflowLoader,
|
|
@@ -12676,7 +14724,13 @@ function buildArchiveHooks(opts) {
|
|
|
12676
14724
|
computeRateLimitDelay,
|
|
12677
14725
|
createBackend,
|
|
12678
14726
|
createEmptyState,
|
|
14727
|
+
crossFieldRoutingIssues,
|
|
12679
14728
|
detectScopeTier,
|
|
14729
|
+
discoverSkillCatalog,
|
|
14730
|
+
discoverSkillCatalogNames,
|
|
14731
|
+
emitProposalApproved,
|
|
14732
|
+
emitProposalCreated,
|
|
14733
|
+
emitProposalRejected,
|
|
12680
14734
|
extractHighlights,
|
|
12681
14735
|
extractTitlePrefix,
|
|
12682
14736
|
getAvailableSlots,
|
|
@@ -12690,6 +14744,7 @@ function buildArchiveHooks(opts) {
|
|
|
12690
14744
|
migrateAgentConfig,
|
|
12691
14745
|
normalizeFts5Query,
|
|
12692
14746
|
openSearchIndex,
|
|
14747
|
+
promote,
|
|
12693
14748
|
reconcile,
|
|
12694
14749
|
reindexFromArchive,
|
|
12695
14750
|
renderAnalysisComment,
|
|
@@ -12698,6 +14753,8 @@ function buildArchiveHooks(opts) {
|
|
|
12698
14753
|
resolveEscalationConfig,
|
|
12699
14754
|
resolveOrchestratorId,
|
|
12700
14755
|
routeIssue,
|
|
14756
|
+
routingWarnings,
|
|
14757
|
+
runGate,
|
|
12701
14758
|
savePublishedIndex,
|
|
12702
14759
|
searchIndexPath,
|
|
12703
14760
|
selectCandidates,
|
|
@@ -12706,6 +14763,7 @@ function buildArchiveHooks(opts) {
|
|
|
12706
14763
|
syncMain,
|
|
12707
14764
|
triageIssue,
|
|
12708
14765
|
truncateForBudget,
|
|
14766
|
+
validateCustomTasks,
|
|
12709
14767
|
validateWorkflowConfig,
|
|
12710
14768
|
wireNotificationSinks,
|
|
12711
14769
|
wrapAsEnvelope
|