@harness-engineering/orchestrator 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +310 -49
- package/dist/index.d.ts +310 -49
- package/dist/index.js +994 -362
- package/dist/index.mjs +1000 -371
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ function sortCandidates(issues) {
|
|
|
27
27
|
return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
function isEligible(issue, state, activeStates, terminalStates) {
|
|
30
|
+
function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
|
|
31
31
|
if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
@@ -49,6 +49,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
49
49
|
if (state.completed.has(issue.id)) {
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
|
+
if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
52
55
|
if (normalizedState === "todo" && issue.blockedBy.length > 0) {
|
|
53
56
|
const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
|
|
54
57
|
if (blocker.state === null) return true;
|
|
@@ -60,9 +63,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
60
63
|
}
|
|
61
64
|
return true;
|
|
62
65
|
}
|
|
63
|
-
function selectCandidates(issues, state, activeStates, terminalStates) {
|
|
66
|
+
function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
|
|
64
67
|
const sorted = sortCandidates(issues);
|
|
65
|
-
return sorted.filter(
|
|
68
|
+
return sorted.filter(
|
|
69
|
+
(issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
|
|
70
|
+
);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// src/core/concurrency.ts
|
|
@@ -658,7 +663,8 @@ function handleTick(state, event, config) {
|
|
|
658
663
|
candidates,
|
|
659
664
|
next,
|
|
660
665
|
config.tracker.activeStates,
|
|
661
|
-
config.tracker.terminalStates
|
|
666
|
+
config.tracker.terminalStates,
|
|
667
|
+
event.selfAssignee
|
|
662
668
|
);
|
|
663
669
|
const escalationConfig = resolveEscalationConfig(config);
|
|
664
670
|
for (const issue of eligible) {
|
|
@@ -1144,7 +1150,7 @@ var ClaimManager = class {
|
|
|
1144
1150
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1145
1151
|
if (!claimResult.ok) return claimResult;
|
|
1146
1152
|
if (this.verifyDelayMs > 0) {
|
|
1147
|
-
await new Promise((
|
|
1153
|
+
await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
|
|
1148
1154
|
}
|
|
1149
1155
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1150
1156
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1796,7 +1802,8 @@ function formatFilesList(files) {
|
|
|
1796
1802
|
}
|
|
1797
1803
|
|
|
1798
1804
|
// src/workflow/loader.ts
|
|
1799
|
-
import * as
|
|
1805
|
+
import * as fs7 from "fs/promises";
|
|
1806
|
+
import * as path7 from "path";
|
|
1800
1807
|
import { parse } from "yaml";
|
|
1801
1808
|
import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
|
|
1802
1809
|
|
|
@@ -1804,7 +1811,8 @@ import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
|
|
|
1804
1811
|
import { z as z2 } from "zod";
|
|
1805
1812
|
import {
|
|
1806
1813
|
Ok as Ok2,
|
|
1807
|
-
Err
|
|
1814
|
+
Err,
|
|
1815
|
+
STANDARD_COGNITIVE_MODES
|
|
1808
1816
|
} from "@harness-engineering/types";
|
|
1809
1817
|
|
|
1810
1818
|
// src/workflow/schema.ts
|
|
@@ -1852,16 +1860,29 @@ var BackendDefSchema = z.discriminatedUnion("type", [
|
|
|
1852
1860
|
probeIntervalMs: z.number().int().min(1e3).optional()
|
|
1853
1861
|
}).strict()
|
|
1854
1862
|
]);
|
|
1863
|
+
var RoutingValueSchema = z.union([
|
|
1864
|
+
z.string().min(1),
|
|
1865
|
+
z.array(z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
|
|
1866
|
+
]);
|
|
1855
1867
|
var RoutingConfigSchema = z.object({
|
|
1856
|
-
default:
|
|
1857
|
-
"quick-fix":
|
|
1858
|
-
"guided-change":
|
|
1859
|
-
"full-exploration":
|
|
1860
|
-
diagnostic:
|
|
1868
|
+
default: RoutingValueSchema,
|
|
1869
|
+
"quick-fix": RoutingValueSchema.optional(),
|
|
1870
|
+
"guided-change": RoutingValueSchema.optional(),
|
|
1871
|
+
"full-exploration": RoutingValueSchema.optional(),
|
|
1872
|
+
diagnostic: RoutingValueSchema.optional(),
|
|
1861
1873
|
intelligence: z.object({
|
|
1862
|
-
sel:
|
|
1863
|
-
pesl:
|
|
1864
|
-
}).strict().optional()
|
|
1874
|
+
sel: RoutingValueSchema.optional(),
|
|
1875
|
+
pesl: RoutingValueSchema.optional()
|
|
1876
|
+
}).strict().optional(),
|
|
1877
|
+
// --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
|
|
1878
|
+
isolation: z.object({
|
|
1879
|
+
none: RoutingValueSchema.optional(),
|
|
1880
|
+
container: RoutingValueSchema.optional(),
|
|
1881
|
+
"remote-sandbox": RoutingValueSchema.optional()
|
|
1882
|
+
}).strict().optional(),
|
|
1883
|
+
// --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
|
|
1884
|
+
skills: z.record(z.string().min(1), RoutingValueSchema).optional(),
|
|
1885
|
+
modes: z.record(z.string().min(1), RoutingValueSchema).optional()
|
|
1865
1886
|
}).strict();
|
|
1866
1887
|
|
|
1867
1888
|
// src/workflow/config.ts
|
|
@@ -1870,13 +1891,17 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
|
1870
1891
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1871
1892
|
const issues = [];
|
|
1872
1893
|
const names = new Set(Object.keys(backends));
|
|
1873
|
-
const checkRef = (
|
|
1874
|
-
if (
|
|
1894
|
+
const checkRef = (path24, value) => {
|
|
1895
|
+
if (value === void 0) return;
|
|
1896
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
1897
|
+
entries.forEach((name, idx) => {
|
|
1898
|
+
if (names.has(name)) return;
|
|
1899
|
+
const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
|
|
1875
1900
|
issues.push({
|
|
1876
|
-
path:
|
|
1877
|
-
message: `routing.${
|
|
1901
|
+
path: pathWithIdx,
|
|
1902
|
+
message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1878
1903
|
});
|
|
1879
|
-
}
|
|
1904
|
+
});
|
|
1880
1905
|
};
|
|
1881
1906
|
checkRef(["default"], routing.default);
|
|
1882
1907
|
checkRef(["quick-fix"], routing["quick-fix"]);
|
|
@@ -1885,9 +1910,44 @@ function crossFieldRoutingIssues(backends, routing) {
|
|
|
1885
1910
|
checkRef(["diagnostic"], routing.diagnostic);
|
|
1886
1911
|
checkRef(["intelligence", "sel"], routing.intelligence?.sel);
|
|
1887
1912
|
checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
|
|
1913
|
+
checkRef(["isolation", "none"], routing.isolation?.none);
|
|
1914
|
+
checkRef(["isolation", "container"], routing.isolation?.container);
|
|
1915
|
+
checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
|
|
1916
|
+
if (routing.skills) {
|
|
1917
|
+
for (const [skill, value] of Object.entries(routing.skills)) {
|
|
1918
|
+
checkRef(["skills", skill], value);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (routing.modes) {
|
|
1922
|
+
for (const [mode, value] of Object.entries(routing.modes)) {
|
|
1923
|
+
checkRef(["modes", mode], value);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1888
1926
|
return issues;
|
|
1889
1927
|
}
|
|
1890
|
-
function
|
|
1928
|
+
function routingWarnings(routing, knownSkillNames) {
|
|
1929
|
+
const warnings = [];
|
|
1930
|
+
if (knownSkillNames.length > 0 && routing.skills) {
|
|
1931
|
+
const known = new Set(knownSkillNames);
|
|
1932
|
+
for (const name of Object.keys(routing.skills)) {
|
|
1933
|
+
if (known.has(name)) continue;
|
|
1934
|
+
warnings.push(
|
|
1935
|
+
`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.`
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (routing.modes) {
|
|
1940
|
+
const standardModes = new Set(STANDARD_COGNITIVE_MODES);
|
|
1941
|
+
for (const mode of Object.keys(routing.modes)) {
|
|
1942
|
+
if (standardModes.has(mode)) continue;
|
|
1943
|
+
warnings.push(
|
|
1944
|
+
`routing.modes.${mode} is not in STANDARD_COGNITIVE_MODES (${[...STANDARD_COGNITIVE_MODES].join(", ")}). Custom cognitive modes are allowed but uncommon; verify this is not a typo.`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return warnings;
|
|
1949
|
+
}
|
|
1950
|
+
function validateWorkflowConfig(config, options = {}) {
|
|
1891
1951
|
if (!config || typeof config !== "object")
|
|
1892
1952
|
return Err(new Error("Config is missing or not an object"));
|
|
1893
1953
|
const c = config;
|
|
@@ -1903,6 +1963,7 @@ function validateWorkflowConfig(config) {
|
|
|
1903
1963
|
if (!hasLegacyBackend && !hasModernBackends) {
|
|
1904
1964
|
return Err(new Error("Config must define agent.backend or agent.backends."));
|
|
1905
1965
|
}
|
|
1966
|
+
const warnings = [];
|
|
1906
1967
|
if (hasModernBackends) {
|
|
1907
1968
|
const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
|
|
1908
1969
|
if (!backendsParsed.success) {
|
|
@@ -1913,9 +1974,10 @@ function validateWorkflowConfig(config) {
|
|
|
1913
1974
|
return Err(new Error(`agent.routing: ${routingParsed.error.message}`));
|
|
1914
1975
|
}
|
|
1915
1976
|
if (routingParsed.data) {
|
|
1977
|
+
const routingData = routingParsed.data;
|
|
1916
1978
|
const cross = crossFieldRoutingIssues(
|
|
1917
1979
|
backendsParsed.data,
|
|
1918
|
-
|
|
1980
|
+
routingData
|
|
1919
1981
|
);
|
|
1920
1982
|
if (cross.length > 0) {
|
|
1921
1983
|
return Err(
|
|
@@ -1924,9 +1986,10 @@ function validateWorkflowConfig(config) {
|
|
|
1924
1986
|
)
|
|
1925
1987
|
);
|
|
1926
1988
|
}
|
|
1989
|
+
warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
|
|
1927
1990
|
}
|
|
1928
1991
|
}
|
|
1929
|
-
return Ok2(config);
|
|
1992
|
+
return Ok2({ config, warnings });
|
|
1930
1993
|
}
|
|
1931
1994
|
function getDefaultConfig() {
|
|
1932
1995
|
return {
|
|
@@ -1979,11 +2042,55 @@ function getDefaultConfig() {
|
|
|
1979
2042
|
};
|
|
1980
2043
|
}
|
|
1981
2044
|
|
|
2045
|
+
// src/workflow/skill-catalog.ts
|
|
2046
|
+
import * as fs6 from "fs";
|
|
2047
|
+
import * as path6 from "path";
|
|
2048
|
+
import { parse as parseYaml } from "yaml";
|
|
2049
|
+
function discoverSkillCatalog(projectRoot) {
|
|
2050
|
+
const skillsRoot = path6.join(projectRoot, "agents", "skills");
|
|
2051
|
+
if (!fs6.existsSync(skillsRoot)) return [];
|
|
2052
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2053
|
+
let hosts;
|
|
2054
|
+
try {
|
|
2055
|
+
hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
|
|
2056
|
+
} catch {
|
|
2057
|
+
return [];
|
|
2058
|
+
}
|
|
2059
|
+
for (const host of hosts) {
|
|
2060
|
+
if (!host.isDirectory()) continue;
|
|
2061
|
+
const hostDir = path6.join(skillsRoot, host.name);
|
|
2062
|
+
let skills;
|
|
2063
|
+
try {
|
|
2064
|
+
skills = fs6.readdirSync(hostDir, { withFileTypes: true });
|
|
2065
|
+
} catch {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
for (const skill of skills) {
|
|
2069
|
+
if (!skill.isDirectory()) continue;
|
|
2070
|
+
const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
|
|
2071
|
+
if (!fs6.existsSync(skillYamlPath)) continue;
|
|
2072
|
+
try {
|
|
2073
|
+
const content = fs6.readFileSync(skillYamlPath, "utf-8");
|
|
2074
|
+
const parsed = parseYaml(content);
|
|
2075
|
+
if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
|
|
2076
|
+
const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
|
|
2077
|
+
byName.set(parsed.name, entry);
|
|
2078
|
+
}
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2084
|
+
}
|
|
2085
|
+
function discoverSkillCatalogNames(projectRoot) {
|
|
2086
|
+
return discoverSkillCatalog(projectRoot).map((e) => e.name);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1982
2089
|
// src/workflow/loader.ts
|
|
1983
2090
|
var WorkflowLoader = class {
|
|
1984
2091
|
async loadWorkflow(filePath) {
|
|
1985
2092
|
try {
|
|
1986
|
-
const content = await
|
|
2093
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
1987
2094
|
const parts = content.split("---");
|
|
1988
2095
|
if (parts.length < 3) {
|
|
1989
2096
|
return Err2(
|
|
@@ -1995,13 +2102,16 @@ var WorkflowLoader = class {
|
|
|
1995
2102
|
const yamlContent = parts[1].trim();
|
|
1996
2103
|
const promptTemplate = parts.slice(2).join("---").trim();
|
|
1997
2104
|
const configData = parse(yamlContent);
|
|
1998
|
-
const
|
|
2105
|
+
const projectRoot = path7.dirname(path7.resolve(filePath));
|
|
2106
|
+
const knownSkillNames = discoverSkillCatalogNames(projectRoot);
|
|
2107
|
+
const configResult = validateWorkflowConfig(configData, { knownSkillNames });
|
|
1999
2108
|
if (!configResult.ok) {
|
|
2000
2109
|
return Err2(configResult.error);
|
|
2001
2110
|
}
|
|
2002
2111
|
return Ok3({
|
|
2003
|
-
config: configResult.value,
|
|
2004
|
-
promptTemplate
|
|
2112
|
+
config: configResult.value.config,
|
|
2113
|
+
promptTemplate,
|
|
2114
|
+
warnings: configResult.value.warnings
|
|
2005
2115
|
});
|
|
2006
2116
|
} catch (error) {
|
|
2007
2117
|
return Err2(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2010,7 +2120,7 @@ var WorkflowLoader = class {
|
|
|
2010
2120
|
};
|
|
2011
2121
|
|
|
2012
2122
|
// src/tracker/adapters/roadmap.ts
|
|
2013
|
-
import * as
|
|
2123
|
+
import * as fs8 from "fs/promises";
|
|
2014
2124
|
import { createHash as createHash2 } from "crypto";
|
|
2015
2125
|
import {
|
|
2016
2126
|
parseRoadmap,
|
|
@@ -2047,7 +2157,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2047
2157
|
async fetchIssuesByStates(stateNames) {
|
|
2048
2158
|
try {
|
|
2049
2159
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2050
|
-
const content = await
|
|
2160
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2051
2161
|
const roadmapResult = parseRoadmap(content);
|
|
2052
2162
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2053
2163
|
const issues = [];
|
|
@@ -2079,7 +2189,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2079
2189
|
if (!terminal) {
|
|
2080
2190
|
return Err3(new Error("Tracker config has no terminalStates; cannot mark complete"));
|
|
2081
2191
|
}
|
|
2082
|
-
const content = await
|
|
2192
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2083
2193
|
const roadmapResult = parseRoadmap(content);
|
|
2084
2194
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2085
2195
|
const roadmap = roadmapResult.value;
|
|
@@ -2088,7 +2198,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2088
2198
|
const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
|
|
2089
2199
|
if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
|
|
2090
2200
|
target.status = terminal;
|
|
2091
|
-
await
|
|
2201
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2092
2202
|
return Ok4(void 0);
|
|
2093
2203
|
} catch (error) {
|
|
2094
2204
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2102,19 +2212,22 @@ var RoadmapTrackerAdapter = class {
|
|
|
2102
2212
|
async claimIssue(issueId, orchestratorId) {
|
|
2103
2213
|
try {
|
|
2104
2214
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2105
|
-
const content = await
|
|
2215
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2106
2216
|
const roadmapResult = parseRoadmap(content);
|
|
2107
2217
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2108
2218
|
const roadmap = roadmapResult.value;
|
|
2109
2219
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2110
2220
|
if (!target) return Ok4(void 0);
|
|
2221
|
+
if (target.assignee != null && target.assignee !== orchestratorId) {
|
|
2222
|
+
return Ok4(void 0);
|
|
2223
|
+
}
|
|
2111
2224
|
if (target.status === "in-progress" && target.assignee === orchestratorId) {
|
|
2112
2225
|
return Ok4(void 0);
|
|
2113
2226
|
}
|
|
2114
2227
|
target.status = "in-progress";
|
|
2115
2228
|
target.assignee = orchestratorId;
|
|
2116
2229
|
target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2117
|
-
await
|
|
2230
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2118
2231
|
return Ok4(void 0);
|
|
2119
2232
|
} catch (error) {
|
|
2120
2233
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2131,7 +2244,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2131
2244
|
if (!activeState) {
|
|
2132
2245
|
return Err3(new Error("Tracker config has no activeStates; cannot release"));
|
|
2133
2246
|
}
|
|
2134
|
-
const content = await
|
|
2247
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2135
2248
|
const roadmapResult = parseRoadmap(content);
|
|
2136
2249
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2137
2250
|
const roadmap = roadmapResult.value;
|
|
@@ -2143,7 +2256,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2143
2256
|
target.status = activeState;
|
|
2144
2257
|
target.assignee = null;
|
|
2145
2258
|
target.updatedAt = null;
|
|
2146
|
-
await
|
|
2259
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2147
2260
|
return Ok4(void 0);
|
|
2148
2261
|
} catch (error) {
|
|
2149
2262
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2165,7 +2278,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2165
2278
|
async fetchIssueStatesByIds(issueIds) {
|
|
2166
2279
|
try {
|
|
2167
2280
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2168
|
-
const content = await
|
|
2281
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2169
2282
|
const roadmapResult = parseRoadmap(content);
|
|
2170
2283
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2171
2284
|
const issueMap = /* @__PURE__ */ new Map();
|
|
@@ -2230,8 +2343,8 @@ var LinearGraphQLStub = class {
|
|
|
2230
2343
|
};
|
|
2231
2344
|
|
|
2232
2345
|
// src/workspace/manager.ts
|
|
2233
|
-
import * as
|
|
2234
|
-
import * as
|
|
2346
|
+
import * as fs9 from "fs/promises";
|
|
2347
|
+
import * as path8 from "path";
|
|
2235
2348
|
import { execFile as execFile2 } from "child_process";
|
|
2236
2349
|
import { promisify as promisify2 } from "util";
|
|
2237
2350
|
import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
|
|
@@ -2262,15 +2375,15 @@ var WorkspaceManager = class {
|
|
|
2262
2375
|
*/
|
|
2263
2376
|
resolvePath(identifier) {
|
|
2264
2377
|
const sanitized = this.sanitizeIdentifier(identifier);
|
|
2265
|
-
return
|
|
2378
|
+
return path8.join(this.config.root, sanitized);
|
|
2266
2379
|
}
|
|
2267
2380
|
/**
|
|
2268
2381
|
* Discovers the git repository root from the workspace root directory.
|
|
2269
2382
|
*/
|
|
2270
2383
|
async getRepoRoot() {
|
|
2271
2384
|
if (this.repoRoot) return this.repoRoot;
|
|
2272
|
-
const root =
|
|
2273
|
-
await
|
|
2385
|
+
const root = path8.resolve(this.config.root);
|
|
2386
|
+
await fs9.mkdir(root, { recursive: true });
|
|
2274
2387
|
const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
|
|
2275
2388
|
this.repoRoot = stdout.trim();
|
|
2276
2389
|
return this.repoRoot;
|
|
@@ -2281,23 +2394,23 @@ var WorkspaceManager = class {
|
|
|
2281
2394
|
*/
|
|
2282
2395
|
async ensureWorkspace(identifier) {
|
|
2283
2396
|
try {
|
|
2284
|
-
const workspacePath =
|
|
2397
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2285
2398
|
try {
|
|
2286
|
-
await
|
|
2399
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2287
2400
|
const repoRoot2 = await this.getRepoRoot();
|
|
2288
2401
|
try {
|
|
2289
2402
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2290
2403
|
} catch {
|
|
2291
|
-
await
|
|
2404
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2292
2405
|
}
|
|
2293
2406
|
} catch {
|
|
2294
2407
|
try {
|
|
2295
|
-
await
|
|
2408
|
+
await fs9.access(workspacePath);
|
|
2296
2409
|
const repoRoot2 = await this.getRepoRoot();
|
|
2297
2410
|
try {
|
|
2298
2411
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2299
2412
|
} catch {
|
|
2300
|
-
await
|
|
2413
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2301
2414
|
}
|
|
2302
2415
|
} catch {
|
|
2303
2416
|
}
|
|
@@ -2393,7 +2506,7 @@ var WorkspaceManager = class {
|
|
|
2393
2506
|
async exists(identifier) {
|
|
2394
2507
|
try {
|
|
2395
2508
|
const workspacePath = this.resolvePath(identifier);
|
|
2396
|
-
await
|
|
2509
|
+
await fs9.access(workspacePath);
|
|
2397
2510
|
return true;
|
|
2398
2511
|
} catch {
|
|
2399
2512
|
return false;
|
|
@@ -2406,9 +2519,9 @@ var WorkspaceManager = class {
|
|
|
2406
2519
|
*/
|
|
2407
2520
|
async findPushedBranch(identifier) {
|
|
2408
2521
|
try {
|
|
2409
|
-
const workspacePath =
|
|
2522
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2410
2523
|
try {
|
|
2411
|
-
await
|
|
2524
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2412
2525
|
} catch {
|
|
2413
2526
|
return null;
|
|
2414
2527
|
}
|
|
@@ -2514,12 +2627,12 @@ var WorkspaceManager = class {
|
|
|
2514
2627
|
*/
|
|
2515
2628
|
async removeWorkspace(identifier) {
|
|
2516
2629
|
try {
|
|
2517
|
-
const workspacePath =
|
|
2630
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2518
2631
|
try {
|
|
2519
2632
|
const repoRoot = await this.getRepoRoot();
|
|
2520
2633
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
|
|
2521
2634
|
} catch {
|
|
2522
|
-
await
|
|
2635
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2523
2636
|
}
|
|
2524
2637
|
return Ok6(void 0);
|
|
2525
2638
|
} catch (error) {
|
|
@@ -2544,7 +2657,7 @@ var WorkspaceHooks = class {
|
|
|
2544
2657
|
if (!command) {
|
|
2545
2658
|
return Ok7(void 0);
|
|
2546
2659
|
}
|
|
2547
|
-
return new Promise((
|
|
2660
|
+
return new Promise((resolve8) => {
|
|
2548
2661
|
const filteredEnv = {};
|
|
2549
2662
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2550
2663
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2557,19 +2670,19 @@ var WorkspaceHooks = class {
|
|
|
2557
2670
|
});
|
|
2558
2671
|
const timeout = setTimeout(() => {
|
|
2559
2672
|
child.kill();
|
|
2560
|
-
|
|
2673
|
+
resolve8(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2561
2674
|
}, this.config.timeoutMs);
|
|
2562
2675
|
child.on("exit", (code) => {
|
|
2563
2676
|
clearTimeout(timeout);
|
|
2564
2677
|
if (code === 0) {
|
|
2565
|
-
|
|
2678
|
+
resolve8(Ok7(void 0));
|
|
2566
2679
|
} else {
|
|
2567
|
-
|
|
2680
|
+
resolve8(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2568
2681
|
}
|
|
2569
2682
|
});
|
|
2570
2683
|
child.on("error", (error) => {
|
|
2571
2684
|
clearTimeout(timeout);
|
|
2572
|
-
|
|
2685
|
+
resolve8(Err5(error));
|
|
2573
2686
|
});
|
|
2574
2687
|
});
|
|
2575
2688
|
}
|
|
@@ -2609,7 +2722,7 @@ var MockBackend = class {
|
|
|
2609
2722
|
content: "Thinking...",
|
|
2610
2723
|
sessionId: session.sessionId
|
|
2611
2724
|
};
|
|
2612
|
-
await new Promise((
|
|
2725
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
2613
2726
|
yield {
|
|
2614
2727
|
type: "thought",
|
|
2615
2728
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2661,12 +2774,12 @@ var PromptRenderer = class {
|
|
|
2661
2774
|
|
|
2662
2775
|
// src/orchestrator.ts
|
|
2663
2776
|
import { EventEmitter } from "events";
|
|
2664
|
-
import * as
|
|
2777
|
+
import * as path21 from "path";
|
|
2665
2778
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2666
2779
|
import { writeTaint } from "@harness-engineering/core";
|
|
2667
2780
|
|
|
2668
2781
|
// src/intelligence/pipeline-runner.ts
|
|
2669
|
-
import * as
|
|
2782
|
+
import * as path9 from "path";
|
|
2670
2783
|
import { weightedRecommendPersona, refreshProfiles } from "@harness-engineering/intelligence";
|
|
2671
2784
|
import {
|
|
2672
2785
|
GitHubIssuesSyncAdapter,
|
|
@@ -2788,7 +2901,7 @@ var IntelligencePipelineRunner = class {
|
|
|
2788
2901
|
}
|
|
2789
2902
|
async loadGraphStore() {
|
|
2790
2903
|
try {
|
|
2791
|
-
const graphDir =
|
|
2904
|
+
const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
|
|
2792
2905
|
const loaded = await this.ctx.graphStore.load(graphDir);
|
|
2793
2906
|
if (loaded) {
|
|
2794
2907
|
this.ctx.logger.info("Graph store loaded from disk");
|
|
@@ -3066,7 +3179,7 @@ var IntelligencePipelineRunner = class {
|
|
|
3066
3179
|
};
|
|
3067
3180
|
|
|
3068
3181
|
// src/completion/handler.ts
|
|
3069
|
-
import * as
|
|
3182
|
+
import * as path10 from "path";
|
|
3070
3183
|
import { GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter2, loadTrackerSyncConfig as loadTrackerSyncConfig2 } from "@harness-engineering/core";
|
|
3071
3184
|
var CompletionHandler = class {
|
|
3072
3185
|
ctx;
|
|
@@ -3149,7 +3262,7 @@ var CompletionHandler = class {
|
|
|
3149
3262
|
result: outcome.result
|
|
3150
3263
|
});
|
|
3151
3264
|
if (this.ctx.graphStore) {
|
|
3152
|
-
const graphDir =
|
|
3265
|
+
const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
|
|
3153
3266
|
await this.ctx.graphStore.save(graphDir);
|
|
3154
3267
|
}
|
|
3155
3268
|
} catch (err) {
|
|
@@ -3635,11 +3748,11 @@ function detectLegacyFields(agent) {
|
|
|
3635
3748
|
}
|
|
3636
3749
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3637
3750
|
const warnings = [];
|
|
3638
|
-
for (const
|
|
3639
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3640
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3751
|
+
for (const path24 of presentLegacy) {
|
|
3752
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
|
|
3753
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
|
|
3641
3754
|
warnings.push(
|
|
3642
|
-
`Ignoring legacy field '${
|
|
3755
|
+
`Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3643
3756
|
);
|
|
3644
3757
|
}
|
|
3645
3758
|
return warnings;
|
|
@@ -3667,7 +3780,7 @@ function migrateAgentConfig(agent) {
|
|
|
3667
3780
|
}
|
|
3668
3781
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3669
3782
|
const warnings = presentLegacy.map(
|
|
3670
|
-
(
|
|
3783
|
+
(path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3671
3784
|
);
|
|
3672
3785
|
return {
|
|
3673
3786
|
config: { ...agent, backends, routing },
|
|
@@ -3730,61 +3843,160 @@ function synthesizeLocal(agent) {
|
|
|
3730
3843
|
}
|
|
3731
3844
|
|
|
3732
3845
|
// src/agent/backend-router.ts
|
|
3846
|
+
function toArray(value) {
|
|
3847
|
+
return Array.isArray(value) ? value : [value];
|
|
3848
|
+
}
|
|
3733
3849
|
var BackendRouter = class {
|
|
3734
3850
|
backends;
|
|
3735
3851
|
routing;
|
|
3852
|
+
decisionBus;
|
|
3736
3853
|
constructor(opts) {
|
|
3737
3854
|
this.backends = opts.backends;
|
|
3738
3855
|
this.routing = opts.routing;
|
|
3856
|
+
this.decisionBus = opts.decisionBus;
|
|
3739
3857
|
this.validateReferences();
|
|
3740
3858
|
}
|
|
3741
3859
|
/**
|
|
3742
|
-
*
|
|
3860
|
+
* Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
|
|
3861
|
+
*
|
|
3862
|
+
* @param useCase the routing query
|
|
3863
|
+
* @param opts.invocationOverride if set and the named backend exists,
|
|
3864
|
+
* beats all other sources (D7 — the `--backend <name>` escape hatch)
|
|
3865
|
+
*/
|
|
3866
|
+
resolve(useCase, opts) {
|
|
3867
|
+
const startedAt = performance.now();
|
|
3868
|
+
const path24 = [];
|
|
3869
|
+
const tryChain = (source, value) => {
|
|
3870
|
+
if (value === void 0) return void 0;
|
|
3871
|
+
for (const name of toArray(value)) {
|
|
3872
|
+
const step = { source, candidate: name, outcome: "considered" };
|
|
3873
|
+
path24.push(step);
|
|
3874
|
+
if (this.backends[name]) {
|
|
3875
|
+
step.outcome = "chosen";
|
|
3876
|
+
return name;
|
|
3877
|
+
}
|
|
3878
|
+
step.outcome = "unknown-backend";
|
|
3879
|
+
}
|
|
3880
|
+
return void 0;
|
|
3881
|
+
};
|
|
3882
|
+
const decide = (backendName) => {
|
|
3883
|
+
const def = this.backends[backendName];
|
|
3884
|
+
if (!def) {
|
|
3885
|
+
throw new Error(
|
|
3886
|
+
`BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3891
|
+
useCase,
|
|
3892
|
+
resolutionPath: path24,
|
|
3893
|
+
backendName,
|
|
3894
|
+
backendType: def.type,
|
|
3895
|
+
durationMs: performance.now() - startedAt
|
|
3896
|
+
};
|
|
3897
|
+
};
|
|
3898
|
+
const emitAndReturn = (decision) => {
|
|
3899
|
+
this.decisionBus?.emit(decision);
|
|
3900
|
+
return decision;
|
|
3901
|
+
};
|
|
3902
|
+
const fromInvocation = tryChain(
|
|
3903
|
+
"invocation",
|
|
3904
|
+
opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
|
|
3905
|
+
);
|
|
3906
|
+
if (fromInvocation) return emitAndReturn(decide(fromInvocation));
|
|
3907
|
+
if (useCase.kind === "skill") {
|
|
3908
|
+
const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
|
|
3909
|
+
if (fromSkill) return emitAndReturn(decide(fromSkill));
|
|
3910
|
+
}
|
|
3911
|
+
const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
|
|
3912
|
+
if (mode !== void 0) {
|
|
3913
|
+
const fromMode = tryChain("mode", this.routing.modes?.[mode]);
|
|
3914
|
+
if (fromMode) return emitAndReturn(decide(fromMode));
|
|
3915
|
+
}
|
|
3916
|
+
const fromExisting = this.resolveExistingUseCase(useCase);
|
|
3917
|
+
if (fromExisting !== void 0) {
|
|
3918
|
+
const chained = tryChain("tier", fromExisting);
|
|
3919
|
+
if (chained) return emitAndReturn(decide(chained));
|
|
3920
|
+
}
|
|
3921
|
+
const fromDefault = tryChain("default", this.routing.default);
|
|
3922
|
+
if (fromDefault) return emitAndReturn(decide(fromDefault));
|
|
3923
|
+
const knownList = Object.keys(this.backends).join(", ") || "(none)";
|
|
3924
|
+
throw new Error(
|
|
3925
|
+
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Returns the {@link BackendDef} reference for the resolved name.
|
|
3930
|
+
* Identity-equal to the entry in `backends` (no copy) so callers
|
|
3931
|
+
* relying on reference equality (SC21) continue to work.
|
|
3932
|
+
*/
|
|
3933
|
+
resolveDefinition(useCase, opts) {
|
|
3934
|
+
const decision = this.resolve(useCase, opts);
|
|
3935
|
+
const def = this.backends[decision.backendName];
|
|
3936
|
+
if (!def) {
|
|
3937
|
+
throw new Error(
|
|
3938
|
+
`BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
return def;
|
|
3942
|
+
}
|
|
3943
|
+
/**
|
|
3944
|
+
* Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
|
|
3945
|
+
* for callers that need both. Replaces the previous pattern of
|
|
3946
|
+
* `resolveDefinition(useCase) + resolve(useCase)` which produced two
|
|
3947
|
+
* RoutingDecision emissions per dispatch — doubling routing-decision
|
|
3948
|
+
* log volume now that Phase 4 emits.
|
|
3743
3949
|
*
|
|
3744
|
-
* - `
|
|
3745
|
-
*
|
|
3746
|
-
* falling back to `routing.default`.
|
|
3747
|
-
* - `maintenance` / `chat`: always `routing.default`.
|
|
3950
|
+
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
3951
|
+
* reference equality (SC21) continue to work.
|
|
3748
3952
|
*/
|
|
3749
|
-
|
|
3953
|
+
resolveDecisionAndDef(useCase, opts) {
|
|
3954
|
+
const decision = this.resolve(useCase, opts);
|
|
3955
|
+
const def = this.backends[decision.backendName];
|
|
3956
|
+
if (!def) {
|
|
3957
|
+
throw new Error(
|
|
3958
|
+
`BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3959
|
+
);
|
|
3960
|
+
}
|
|
3961
|
+
return { decision, def };
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* The pre-Spec-B resolution helper: returns the configured
|
|
3965
|
+
* {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
|
|
3966
|
+
* use cases (or `undefined` for skill/mode use cases, which are owned
|
|
3967
|
+
* by the per-skill / per-mode steps in {@link resolve}). Returning
|
|
3968
|
+
* `undefined` lets the caller fall through to `routing.default`.
|
|
3969
|
+
*/
|
|
3970
|
+
resolveExistingUseCase(useCase) {
|
|
3750
3971
|
switch (useCase.kind) {
|
|
3751
3972
|
case "tier": {
|
|
3752
|
-
const
|
|
3753
|
-
return
|
|
3973
|
+
const tierMap = this.routing;
|
|
3974
|
+
return tierMap[useCase.tier];
|
|
3754
3975
|
}
|
|
3755
3976
|
case "intelligence": {
|
|
3756
3977
|
const intel = this.routing.intelligence;
|
|
3757
|
-
return intel?.[useCase.layer]
|
|
3978
|
+
return intel?.[useCase.layer];
|
|
3758
3979
|
}
|
|
3759
3980
|
case "isolation": {
|
|
3760
3981
|
const iso = this.routing.isolation;
|
|
3761
|
-
return iso?.[useCase.tier]
|
|
3982
|
+
return iso?.[useCase.tier];
|
|
3762
3983
|
}
|
|
3763
3984
|
case "maintenance":
|
|
3764
3985
|
case "chat":
|
|
3765
|
-
return
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
* Returns the BackendDef reference for the resolved name. Returns the
|
|
3770
|
-
* exact reference held in `backends` (no copy) so identity comparisons
|
|
3771
|
-
* succeed (SC21).
|
|
3772
|
-
*/
|
|
3773
|
-
resolveDefinition(useCase) {
|
|
3774
|
-
const name = this.resolve(useCase);
|
|
3775
|
-
const def = this.backends[name];
|
|
3776
|
-
if (!def) {
|
|
3777
|
-
throw new Error(
|
|
3778
|
-
`BackendRouter.resolveDefinition: routing target '${name}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3779
|
-
);
|
|
3986
|
+
return void 0;
|
|
3987
|
+
case "skill":
|
|
3988
|
+
case "mode":
|
|
3989
|
+
return void 0;
|
|
3780
3990
|
}
|
|
3781
|
-
return def;
|
|
3782
3991
|
}
|
|
3783
3992
|
validateReferences() {
|
|
3784
3993
|
const known = new Set(Object.keys(this.backends));
|
|
3785
3994
|
const missing = [];
|
|
3786
|
-
const check = (
|
|
3787
|
-
if (
|
|
3995
|
+
const check = (label, value) => {
|
|
3996
|
+
if (value === void 0) return;
|
|
3997
|
+
for (const name of toArray(value)) {
|
|
3998
|
+
if (!known.has(name)) missing.push({ path: label, name });
|
|
3999
|
+
}
|
|
3788
4000
|
};
|
|
3789
4001
|
check("default", this.routing.default);
|
|
3790
4002
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3796,8 +4008,14 @@ var BackendRouter = class {
|
|
|
3796
4008
|
check("isolation.none", this.routing.isolation?.none);
|
|
3797
4009
|
check("isolation.container", this.routing.isolation?.container);
|
|
3798
4010
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
4011
|
+
for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
|
|
4012
|
+
check(`skills.${skill}`, value);
|
|
4013
|
+
}
|
|
4014
|
+
for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
|
|
4015
|
+
check(`modes.${mode}`, value);
|
|
4016
|
+
}
|
|
3799
4017
|
if (missing.length > 0) {
|
|
3800
|
-
const detail = missing.map(({ path:
|
|
4018
|
+
const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
|
|
3801
4019
|
const known_ = [...known].join(", ") || "(none)";
|
|
3802
4020
|
throw new Error(
|
|
3803
4021
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3814,11 +4032,11 @@ import {
|
|
|
3814
4032
|
Ok as Ok10,
|
|
3815
4033
|
Err as Err7
|
|
3816
4034
|
} from "@harness-engineering/types";
|
|
3817
|
-
function resolveExitCode(code, command,
|
|
4035
|
+
function resolveExitCode(code, command, resolve8) {
|
|
3818
4036
|
if (code === 0) {
|
|
3819
|
-
|
|
4037
|
+
resolve8(Ok10(void 0));
|
|
3820
4038
|
} else {
|
|
3821
|
-
|
|
4039
|
+
resolve8(
|
|
3822
4040
|
Err7({
|
|
3823
4041
|
category: "agent_not_found",
|
|
3824
4042
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3826,8 +4044,8 @@ function resolveExitCode(code, command, resolve7) {
|
|
|
3826
4044
|
);
|
|
3827
4045
|
}
|
|
3828
4046
|
}
|
|
3829
|
-
function resolveSpawnError(command,
|
|
3830
|
-
|
|
4047
|
+
function resolveSpawnError(command, resolve8) {
|
|
4048
|
+
resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3831
4049
|
}
|
|
3832
4050
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3833
4051
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4140,10 +4358,10 @@ var ClaudeBackend = class {
|
|
|
4140
4358
|
errRl.close();
|
|
4141
4359
|
}
|
|
4142
4360
|
if (exitCode === null) {
|
|
4143
|
-
await new Promise((
|
|
4361
|
+
await new Promise((resolve8) => {
|
|
4144
4362
|
child.on("exit", (code) => {
|
|
4145
4363
|
exitCode = code;
|
|
4146
|
-
|
|
4364
|
+
resolve8(null);
|
|
4147
4365
|
});
|
|
4148
4366
|
});
|
|
4149
4367
|
}
|
|
@@ -4165,10 +4383,10 @@ var ClaudeBackend = class {
|
|
|
4165
4383
|
return Ok10(void 0);
|
|
4166
4384
|
}
|
|
4167
4385
|
async healthCheck() {
|
|
4168
|
-
return new Promise((
|
|
4386
|
+
return new Promise((resolve8) => {
|
|
4169
4387
|
const child = spawn2(this.command, ["--version"]);
|
|
4170
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4171
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4388
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
|
|
4389
|
+
child.on("error", () => resolveSpawnError(this.command, resolve8));
|
|
4172
4390
|
});
|
|
4173
4391
|
}
|
|
4174
4392
|
};
|
|
@@ -5100,14 +5318,14 @@ var SshBackend = class {
|
|
|
5100
5318
|
async healthCheck() {
|
|
5101
5319
|
const args = [...this.buildSshArgs()];
|
|
5102
5320
|
args[args.length - 1] = "true";
|
|
5103
|
-
return new Promise((
|
|
5321
|
+
return new Promise((resolve8) => {
|
|
5104
5322
|
let child;
|
|
5105
5323
|
try {
|
|
5106
5324
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5107
5325
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5108
5326
|
});
|
|
5109
5327
|
} catch (err) {
|
|
5110
|
-
|
|
5328
|
+
resolve8(
|
|
5111
5329
|
Err13({
|
|
5112
5330
|
category: "agent_not_found",
|
|
5113
5331
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5128,9 +5346,9 @@ var SshBackend = class {
|
|
|
5128
5346
|
child.on("close", (code) => {
|
|
5129
5347
|
clearTimeout(timer);
|
|
5130
5348
|
if (code === 0) {
|
|
5131
|
-
|
|
5349
|
+
resolve8(Ok16(void 0));
|
|
5132
5350
|
} else {
|
|
5133
|
-
|
|
5351
|
+
resolve8(
|
|
5134
5352
|
Err13({
|
|
5135
5353
|
category: "agent_not_found",
|
|
5136
5354
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5140,7 +5358,7 @@ var SshBackend = class {
|
|
|
5140
5358
|
});
|
|
5141
5359
|
child.on("error", (err) => {
|
|
5142
5360
|
clearTimeout(timer);
|
|
5143
|
-
|
|
5361
|
+
resolve8(Err13({ category: "agent_not_found", message: err.message }));
|
|
5144
5362
|
});
|
|
5145
5363
|
});
|
|
5146
5364
|
}
|
|
@@ -5188,13 +5406,13 @@ async function* readLines(stream) {
|
|
|
5188
5406
|
if (buffer.length > 0) yield buffer;
|
|
5189
5407
|
}
|
|
5190
5408
|
function waitForExit(child) {
|
|
5191
|
-
return new Promise((
|
|
5409
|
+
return new Promise((resolve8) => {
|
|
5192
5410
|
if (child.exitCode !== null) {
|
|
5193
|
-
|
|
5411
|
+
resolve8(child.exitCode);
|
|
5194
5412
|
return;
|
|
5195
5413
|
}
|
|
5196
|
-
child.once("close", (code) =>
|
|
5197
|
-
child.once("error", () =>
|
|
5414
|
+
child.once("close", (code) => resolve8(code));
|
|
5415
|
+
child.once("error", () => resolve8(null));
|
|
5198
5416
|
});
|
|
5199
5417
|
}
|
|
5200
5418
|
|
|
@@ -5384,14 +5602,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5384
5602
|
return out;
|
|
5385
5603
|
}
|
|
5386
5604
|
runOneShot(binary, args) {
|
|
5387
|
-
return new Promise((
|
|
5605
|
+
return new Promise((resolve8) => {
|
|
5388
5606
|
let child;
|
|
5389
5607
|
try {
|
|
5390
5608
|
child = this.spawnImpl(binary, args, {
|
|
5391
5609
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5392
5610
|
});
|
|
5393
5611
|
} catch (err) {
|
|
5394
|
-
|
|
5612
|
+
resolve8(
|
|
5395
5613
|
Err14({
|
|
5396
5614
|
category: "agent_not_found",
|
|
5397
5615
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5416,9 +5634,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5416
5634
|
child.on("close", (code) => {
|
|
5417
5635
|
clearTimeout(timer);
|
|
5418
5636
|
if (code === 0) {
|
|
5419
|
-
|
|
5637
|
+
resolve8(Ok17(stdout));
|
|
5420
5638
|
} else {
|
|
5421
|
-
|
|
5639
|
+
resolve8(
|
|
5422
5640
|
Err14({
|
|
5423
5641
|
category: "response_error",
|
|
5424
5642
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5428,7 +5646,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5428
5646
|
});
|
|
5429
5647
|
child.on("error", (err) => {
|
|
5430
5648
|
clearTimeout(timer);
|
|
5431
|
-
|
|
5649
|
+
resolve8(Err14({ category: "agent_not_found", message: err.message }));
|
|
5432
5650
|
});
|
|
5433
5651
|
});
|
|
5434
5652
|
}
|
|
@@ -5488,13 +5706,13 @@ async function* readLines2(stream) {
|
|
|
5488
5706
|
if (buffer.length > 0) yield buffer;
|
|
5489
5707
|
}
|
|
5490
5708
|
function waitForExit2(child) {
|
|
5491
|
-
return new Promise((
|
|
5709
|
+
return new Promise((resolve8) => {
|
|
5492
5710
|
if (child.exitCode !== null) {
|
|
5493
|
-
|
|
5711
|
+
resolve8(child.exitCode);
|
|
5494
5712
|
return;
|
|
5495
5713
|
}
|
|
5496
|
-
child.once("close", (code) =>
|
|
5497
|
-
child.once("error", () =>
|
|
5714
|
+
child.once("close", (code) => resolve8(code));
|
|
5715
|
+
child.once("error", () => resolve8(null));
|
|
5498
5716
|
});
|
|
5499
5717
|
}
|
|
5500
5718
|
|
|
@@ -5694,13 +5912,13 @@ var ContainerBackend = class {
|
|
|
5694
5912
|
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
5695
5913
|
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5696
5914
|
function dockerExec(args) {
|
|
5697
|
-
return new Promise((
|
|
5915
|
+
return new Promise((resolve8, reject) => {
|
|
5698
5916
|
execFile3("docker", args, (error, stdout) => {
|
|
5699
5917
|
if (error) {
|
|
5700
5918
|
reject(error);
|
|
5701
5919
|
return;
|
|
5702
5920
|
}
|
|
5703
|
-
|
|
5921
|
+
resolve8(stdout.trim());
|
|
5704
5922
|
});
|
|
5705
5923
|
});
|
|
5706
5924
|
}
|
|
@@ -5759,11 +5977,11 @@ var DockerRuntime = class {
|
|
|
5759
5977
|
} finally {
|
|
5760
5978
|
rl.close();
|
|
5761
5979
|
}
|
|
5762
|
-
const exitCode = await new Promise((
|
|
5980
|
+
const exitCode = await new Promise((resolve8) => {
|
|
5763
5981
|
if (child.exitCode !== null) {
|
|
5764
|
-
|
|
5982
|
+
resolve8(child.exitCode);
|
|
5765
5983
|
} else {
|
|
5766
|
-
child.on("exit", (code) =>
|
|
5984
|
+
child.on("exit", (code) => resolve8(code ?? 1));
|
|
5767
5985
|
}
|
|
5768
5986
|
});
|
|
5769
5987
|
return exitCode;
|
|
@@ -5822,13 +6040,13 @@ var EnvSecretBackend = class {
|
|
|
5822
6040
|
import { execFile as execFile4 } from "child_process";
|
|
5823
6041
|
import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
|
|
5824
6042
|
function opExec(args) {
|
|
5825
|
-
return new Promise((
|
|
6043
|
+
return new Promise((resolve8, reject) => {
|
|
5826
6044
|
execFile4("op", args, (error, stdout) => {
|
|
5827
6045
|
if (error) {
|
|
5828
6046
|
reject(error);
|
|
5829
6047
|
return;
|
|
5830
6048
|
}
|
|
5831
|
-
|
|
6049
|
+
resolve8(stdout.trim());
|
|
5832
6050
|
});
|
|
5833
6051
|
});
|
|
5834
6052
|
}
|
|
@@ -5871,13 +6089,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5871
6089
|
import { execFile as execFile5 } from "child_process";
|
|
5872
6090
|
import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
|
|
5873
6091
|
function vaultExec(args, env) {
|
|
5874
|
-
return new Promise((
|
|
6092
|
+
return new Promise((resolve8, reject) => {
|
|
5875
6093
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5876
6094
|
if (error) {
|
|
5877
6095
|
reject(error);
|
|
5878
6096
|
return;
|
|
5879
6097
|
}
|
|
5880
|
-
|
|
6098
|
+
resolve8(stdout.trim());
|
|
5881
6099
|
});
|
|
5882
6100
|
});
|
|
5883
6101
|
}
|
|
@@ -5952,7 +6170,11 @@ var OrchestratorBackendFactory = class {
|
|
|
5952
6170
|
opts;
|
|
5953
6171
|
constructor(opts) {
|
|
5954
6172
|
this.opts = opts;
|
|
5955
|
-
this.router = new BackendRouter({
|
|
6173
|
+
this.router = new BackendRouter({
|
|
6174
|
+
backends: opts.backends,
|
|
6175
|
+
routing: opts.routing,
|
|
6176
|
+
...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
|
|
6177
|
+
});
|
|
5956
6178
|
}
|
|
5957
6179
|
/**
|
|
5958
6180
|
* Resolve `useCase` to a backend name, materialize a fresh
|
|
@@ -5971,12 +6193,21 @@ var OrchestratorBackendFactory = class {
|
|
|
5971
6193
|
* is `undefined` for pure-modern configs. Threading the routed name
|
|
5972
6194
|
* through dispatch eliminates that gap.
|
|
5973
6195
|
*/
|
|
5974
|
-
resolveName(useCase) {
|
|
5975
|
-
return this.router.resolve(useCase);
|
|
6196
|
+
resolveName(useCase, opts) {
|
|
6197
|
+
return this.router.resolve(useCase, opts).backendName;
|
|
6198
|
+
}
|
|
6199
|
+
/**
|
|
6200
|
+
* Spec B Phase 1: expose the underlying router for callers that need
|
|
6201
|
+
* it directly (e.g., {@link buildIntelligencePipeline} for the
|
|
6202
|
+
* I1 SEL/PESL comparison fix). Read-only access; consumers must not
|
|
6203
|
+
* mutate router state.
|
|
6204
|
+
*/
|
|
6205
|
+
getRouter() {
|
|
6206
|
+
return this.router;
|
|
5976
6207
|
}
|
|
5977
|
-
forUseCase(useCase) {
|
|
5978
|
-
const def = this.router.
|
|
5979
|
-
const name =
|
|
6208
|
+
forUseCase(useCase, opts) {
|
|
6209
|
+
const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
|
|
6210
|
+
const name = decision.backendName;
|
|
5980
6211
|
let backend;
|
|
5981
6212
|
const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
|
|
5982
6213
|
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
@@ -6149,15 +6380,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
6149
6380
|
|
|
6150
6381
|
// src/agent/intelligence-factory.ts
|
|
6151
6382
|
function buildIntelligencePipeline(deps) {
|
|
6152
|
-
const { config } = deps;
|
|
6383
|
+
const { config, router } = deps;
|
|
6153
6384
|
const intel = config.intelligence;
|
|
6154
6385
|
if (!intel?.enabled) return null;
|
|
6155
6386
|
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
6156
6387
|
if (!selProvider) return null;
|
|
6157
|
-
const
|
|
6158
|
-
const
|
|
6159
|
-
const
|
|
6160
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6388
|
+
const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
|
|
6389
|
+
const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
|
|
6390
|
+
const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6161
6391
|
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
6162
6392
|
const graphStore = new GraphStore();
|
|
6163
6393
|
const pipeline = new IntelligencePipeline(selProvider, graphStore, {
|
|
@@ -6174,7 +6404,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6174
6404
|
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
6175
6405
|
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
6176
6406
|
}
|
|
6177
|
-
const routed = resolveRoutedBackend(layer,
|
|
6407
|
+
const routed = resolveRoutedBackend(layer, deps);
|
|
6178
6408
|
if (!routed) return null;
|
|
6179
6409
|
const { name, def } = routed;
|
|
6180
6410
|
const resolver = localResolvers.get(name);
|
|
@@ -6199,20 +6429,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6199
6429
|
logger
|
|
6200
6430
|
});
|
|
6201
6431
|
}
|
|
6202
|
-
function resolveRoutedBackend(layer,
|
|
6203
|
-
const
|
|
6432
|
+
function resolveRoutedBackend(layer, deps) {
|
|
6433
|
+
const { config, router, logger } = deps;
|
|
6204
6434
|
const backends = config.agent.backends;
|
|
6205
|
-
if (!
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6435
|
+
if (!backends || !router) return null;
|
|
6436
|
+
try {
|
|
6437
|
+
const decision = router.resolve({ kind: "intelligence", layer });
|
|
6438
|
+
const def = backends[decision.backendName];
|
|
6439
|
+
if (!def) {
|
|
6440
|
+
logger.warn(
|
|
6441
|
+
`Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
|
|
6442
|
+
);
|
|
6443
|
+
return null;
|
|
6444
|
+
}
|
|
6445
|
+
return { name: decision.backendName, def };
|
|
6446
|
+
} catch (err) {
|
|
6210
6447
|
logger.warn(
|
|
6211
|
-
`Intelligence pipeline:
|
|
6448
|
+
`Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
|
|
6212
6449
|
);
|
|
6213
6450
|
return null;
|
|
6214
6451
|
}
|
|
6215
|
-
return { name, def };
|
|
6216
6452
|
}
|
|
6217
6453
|
function buildExplicitProvider(provider, selModel, config) {
|
|
6218
6454
|
if (provider.kind === "anthropic") {
|
|
@@ -6247,9 +6483,104 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6247
6483
|
});
|
|
6248
6484
|
}
|
|
6249
6485
|
|
|
6486
|
+
// src/routing/decision-bus.ts
|
|
6487
|
+
var RoutingDecisionBus = class {
|
|
6488
|
+
ringBuffer = [];
|
|
6489
|
+
listeners = /* @__PURE__ */ new Set();
|
|
6490
|
+
capacity;
|
|
6491
|
+
logger;
|
|
6492
|
+
constructor(opts) {
|
|
6493
|
+
this.capacity = opts?.capacity ?? 500;
|
|
6494
|
+
this.logger = opts?.logger;
|
|
6495
|
+
}
|
|
6496
|
+
emit(decision) {
|
|
6497
|
+
this.ringBuffer.push(decision);
|
|
6498
|
+
if (this.ringBuffer.length > this.capacity) {
|
|
6499
|
+
this.ringBuffer.shift();
|
|
6500
|
+
}
|
|
6501
|
+
if (this.logger) {
|
|
6502
|
+
this.logger.info("routing-decision", {
|
|
6503
|
+
useCase: decision.useCase,
|
|
6504
|
+
backendName: decision.backendName,
|
|
6505
|
+
resolutionPathLength: decision.resolutionPath.length,
|
|
6506
|
+
durationMs: decision.durationMs
|
|
6507
|
+
});
|
|
6508
|
+
}
|
|
6509
|
+
for (const listener of this.listeners) {
|
|
6510
|
+
try {
|
|
6511
|
+
listener(decision);
|
|
6512
|
+
} catch (err) {
|
|
6513
|
+
if (this.logger) {
|
|
6514
|
+
this.logger.warn("RoutingDecisionBus subscriber threw", {
|
|
6515
|
+
error: String(err)
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
recent(filter) {
|
|
6522
|
+
let out = this.ringBuffer.slice();
|
|
6523
|
+
if (filter?.skillName !== void 0) {
|
|
6524
|
+
out = out.filter(
|
|
6525
|
+
(d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
|
|
6526
|
+
);
|
|
6527
|
+
}
|
|
6528
|
+
if (filter?.mode !== void 0) {
|
|
6529
|
+
const m = filter.mode;
|
|
6530
|
+
out = out.filter(
|
|
6531
|
+
(d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
|
|
6532
|
+
);
|
|
6533
|
+
}
|
|
6534
|
+
if (filter?.backendName !== void 0) {
|
|
6535
|
+
out = out.filter((d) => d.backendName === filter.backendName);
|
|
6536
|
+
}
|
|
6537
|
+
if (filter?.limit !== void 0) {
|
|
6538
|
+
out = out.slice(-filter.limit).reverse();
|
|
6539
|
+
} else {
|
|
6540
|
+
out = out.reverse();
|
|
6541
|
+
}
|
|
6542
|
+
return out;
|
|
6543
|
+
}
|
|
6544
|
+
subscribe(listener) {
|
|
6545
|
+
this.listeners.add(listener);
|
|
6546
|
+
return () => {
|
|
6547
|
+
this.listeners.delete(listener);
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
/**
|
|
6551
|
+
* Spec B Phase 5 (review-S2 fix): release all subscriber references so
|
|
6552
|
+
* teardown can complete without anchoring closures. Called from
|
|
6553
|
+
* `Orchestrator.stop()` before nulling the bus reference. The bus
|
|
6554
|
+
* remains usable after clear — `subscribe()` works as normal.
|
|
6555
|
+
*/
|
|
6556
|
+
clearListeners() {
|
|
6557
|
+
this.listeners.clear();
|
|
6558
|
+
}
|
|
6559
|
+
};
|
|
6560
|
+
|
|
6561
|
+
// src/agent/triage-skill-mapping.ts
|
|
6562
|
+
function resolveSkillForTriage(triageSkill, catalog) {
|
|
6563
|
+
const expected = `harness-${triageSkill}`;
|
|
6564
|
+
const match = catalog.find((e) => e.name === expected);
|
|
6565
|
+
if (!match) return void 0;
|
|
6566
|
+
return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6569
|
+
// src/agent/use-case-builder.ts
|
|
6570
|
+
function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
6571
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
6572
|
+
const decision = triageIssue(issue, {});
|
|
6573
|
+
const resolved = resolveSkillForTriage(decision.skill, catalog);
|
|
6574
|
+
if (resolved) {
|
|
6575
|
+
return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
|
|
6576
|
+
}
|
|
6577
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
6578
|
+
return { kind: "tier", tier };
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6250
6581
|
// src/server/http.ts
|
|
6251
6582
|
import * as http from "http";
|
|
6252
|
-
import * as
|
|
6583
|
+
import * as path17 from "path";
|
|
6253
6584
|
import { assertPortUsable } from "@harness-engineering/core";
|
|
6254
6585
|
|
|
6255
6586
|
// src/server/websocket.ts
|
|
@@ -6312,7 +6643,7 @@ import { z as z3 } from "zod";
|
|
|
6312
6643
|
// src/server/utils.ts
|
|
6313
6644
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6314
6645
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6315
|
-
return new Promise((
|
|
6646
|
+
return new Promise((resolve8, reject) => {
|
|
6316
6647
|
let body = "";
|
|
6317
6648
|
let bytes = 0;
|
|
6318
6649
|
req.on("data", (chunk) => {
|
|
@@ -6324,7 +6655,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6324
6655
|
}
|
|
6325
6656
|
body += String(chunk);
|
|
6326
6657
|
});
|
|
6327
|
-
req.on("end", () =>
|
|
6658
|
+
req.on("end", () => resolve8(body));
|
|
6328
6659
|
req.on("error", reject);
|
|
6329
6660
|
});
|
|
6330
6661
|
}
|
|
@@ -6445,8 +6776,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
|
6445
6776
|
|
|
6446
6777
|
// src/server/routes/plans.ts
|
|
6447
6778
|
import { z as z5 } from "zod";
|
|
6448
|
-
import * as
|
|
6449
|
-
import * as
|
|
6779
|
+
import * as fs10 from "fs/promises";
|
|
6780
|
+
import * as path11 from "path";
|
|
6450
6781
|
var PlanWriteSchema = z5.object({
|
|
6451
6782
|
filename: z5.string().min(1),
|
|
6452
6783
|
content: z5.string().min(1)
|
|
@@ -6466,7 +6797,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6466
6797
|
return;
|
|
6467
6798
|
}
|
|
6468
6799
|
const parsed = result.data;
|
|
6469
|
-
const basename3 =
|
|
6800
|
+
const basename3 = path11.basename(parsed.filename);
|
|
6470
6801
|
if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
|
|
6471
6802
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6472
6803
|
res.end(
|
|
@@ -6474,9 +6805,9 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6474
6805
|
);
|
|
6475
6806
|
return;
|
|
6476
6807
|
}
|
|
6477
|
-
await
|
|
6478
|
-
const filePath =
|
|
6479
|
-
await
|
|
6808
|
+
await fs10.mkdir(plansDir, { recursive: true });
|
|
6809
|
+
const filePath = path11.join(plansDir, basename3);
|
|
6810
|
+
await fs10.writeFile(filePath, parsed.content, "utf-8");
|
|
6480
6811
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
6481
6812
|
res.end(JSON.stringify({ ok: true, filename: basename3 }));
|
|
6482
6813
|
} catch {
|
|
@@ -6851,8 +7182,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6851
7182
|
}
|
|
6852
7183
|
|
|
6853
7184
|
// src/server/routes/roadmap-actions.ts
|
|
6854
|
-
import * as
|
|
6855
|
-
import * as
|
|
7185
|
+
import * as fs11 from "fs/promises";
|
|
7186
|
+
import * as path12 from "path";
|
|
6856
7187
|
import {
|
|
6857
7188
|
parseRoadmap as parseRoadmap2,
|
|
6858
7189
|
serializeRoadmap as serializeRoadmap2,
|
|
@@ -6888,7 +7219,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6888
7219
|
sendJSON2(res, 503, { error: "Roadmap path not configured" });
|
|
6889
7220
|
return;
|
|
6890
7221
|
}
|
|
6891
|
-
const projectRoot =
|
|
7222
|
+
const projectRoot = path12.dirname(path12.dirname(roadmapPath));
|
|
6892
7223
|
const mode = loadProjectRoadmapMode(projectRoot);
|
|
6893
7224
|
if (mode === "file-less") {
|
|
6894
7225
|
const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
|
|
@@ -6941,7 +7272,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6941
7272
|
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
6942
7273
|
return;
|
|
6943
7274
|
}
|
|
6944
|
-
const content = await
|
|
7275
|
+
const content = await fs11.readFile(roadmapPath, "utf-8");
|
|
6945
7276
|
const roadmapResult = parseRoadmap2(content);
|
|
6946
7277
|
if (!roadmapResult.ok) {
|
|
6947
7278
|
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
@@ -6972,8 +7303,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6972
7303
|
roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
|
|
6973
7304
|
const tmpPath = roadmapPath + ".tmp";
|
|
6974
7305
|
const serialized = serializeRoadmap2(roadmap);
|
|
6975
|
-
await
|
|
6976
|
-
await
|
|
7306
|
+
await fs11.writeFile(tmpPath, serialized, "utf-8");
|
|
7307
|
+
await fs11.rename(tmpPath, roadmapPath);
|
|
6977
7308
|
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
6978
7309
|
} catch (err) {
|
|
6979
7310
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
@@ -7473,7 +7804,7 @@ import {
|
|
|
7473
7804
|
} from "@harness-engineering/types";
|
|
7474
7805
|
|
|
7475
7806
|
// src/proposals/gate.ts
|
|
7476
|
-
import { parse as
|
|
7807
|
+
import { parse as parseYaml2 } from "yaml";
|
|
7477
7808
|
import {
|
|
7478
7809
|
getProposal,
|
|
7479
7810
|
updateProposal,
|
|
@@ -7490,7 +7821,7 @@ function checkSkillYaml(yaml) {
|
|
|
7490
7821
|
const findings = [];
|
|
7491
7822
|
let doc;
|
|
7492
7823
|
try {
|
|
7493
|
-
doc =
|
|
7824
|
+
doc = parseYaml2(yaml);
|
|
7494
7825
|
} catch (err) {
|
|
7495
7826
|
findings.push({
|
|
7496
7827
|
severity: "error",
|
|
@@ -7613,9 +7944,9 @@ async function runGate(projectPath, proposalId) {
|
|
|
7613
7944
|
}
|
|
7614
7945
|
|
|
7615
7946
|
// src/proposals/promote.ts
|
|
7616
|
-
import * as
|
|
7617
|
-
import * as
|
|
7618
|
-
import { parse as
|
|
7947
|
+
import * as fs12 from "fs";
|
|
7948
|
+
import * as path13 from "path";
|
|
7949
|
+
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
7619
7950
|
import {
|
|
7620
7951
|
getProposal as getProposal2,
|
|
7621
7952
|
updateProposal as updateProposal2,
|
|
@@ -7635,11 +7966,11 @@ var PromotionError = class extends Error {
|
|
|
7635
7966
|
};
|
|
7636
7967
|
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
7637
7968
|
function skillDir(projectPath, name) {
|
|
7638
|
-
return
|
|
7969
|
+
return path13.join(projectPath, "agents", "skills", "claude-code", name);
|
|
7639
7970
|
}
|
|
7640
7971
|
function readIfExists(p) {
|
|
7641
7972
|
try {
|
|
7642
|
-
return
|
|
7973
|
+
return fs12.readFileSync(p, "utf-8");
|
|
7643
7974
|
} catch {
|
|
7644
7975
|
return null;
|
|
7645
7976
|
}
|
|
@@ -7647,7 +7978,7 @@ function readIfExists(p) {
|
|
|
7647
7978
|
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
7648
7979
|
let doc;
|
|
7649
7980
|
try {
|
|
7650
|
-
doc =
|
|
7981
|
+
doc = parseYaml3(yamlText);
|
|
7651
7982
|
} catch (err) {
|
|
7652
7983
|
throw new PromotionError(
|
|
7653
7984
|
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7685,15 +8016,15 @@ function assertGateReady(proposal) {
|
|
|
7685
8016
|
}
|
|
7686
8017
|
async function promoteNewSkill(projectPath, proposal) {
|
|
7687
8018
|
const target = skillDir(projectPath, proposal.content.name);
|
|
7688
|
-
if (
|
|
8019
|
+
if (fs12.existsSync(target)) {
|
|
7689
8020
|
throw new PromotionError(
|
|
7690
8021
|
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
7691
8022
|
);
|
|
7692
8023
|
}
|
|
7693
|
-
|
|
8024
|
+
fs12.mkdirSync(target, { recursive: true });
|
|
7694
8025
|
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
7695
|
-
|
|
7696
|
-
|
|
8026
|
+
fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
|
|
8027
|
+
fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
7697
8028
|
return { skillPath: target };
|
|
7698
8029
|
}
|
|
7699
8030
|
async function promoteRefinement(projectPath, proposal) {
|
|
@@ -7701,12 +8032,12 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
7701
8032
|
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
7702
8033
|
}
|
|
7703
8034
|
const target = skillDir(projectPath, proposal.targetSkill);
|
|
7704
|
-
if (!
|
|
8035
|
+
if (!fs12.existsSync(target)) {
|
|
7705
8036
|
throw new PromotionError(
|
|
7706
8037
|
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
7707
8038
|
);
|
|
7708
8039
|
}
|
|
7709
|
-
const yamlPath =
|
|
8040
|
+
const yamlPath = path13.join(target, "skill.yaml");
|
|
7710
8041
|
const before = readIfExists(yamlPath) ?? "";
|
|
7711
8042
|
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
7712
8043
|
if (after === before) {
|
|
@@ -7714,7 +8045,7 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
7714
8045
|
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
7715
8046
|
);
|
|
7716
8047
|
}
|
|
7717
|
-
|
|
8048
|
+
fs12.writeFileSync(yamlPath, after);
|
|
7718
8049
|
return { skillPath: target };
|
|
7719
8050
|
}
|
|
7720
8051
|
async function promote(projectPath, proposalId, decidedBy) {
|
|
@@ -8001,35 +8332,185 @@ function handleV1ProposalsRoute(req, res, deps) {
|
|
|
8001
8332
|
return false;
|
|
8002
8333
|
}
|
|
8003
8334
|
|
|
8004
|
-
// src/server/routes/
|
|
8005
|
-
import * as fs12 from "fs/promises";
|
|
8006
|
-
import * as path12 from "path";
|
|
8335
|
+
// src/server/routes/v1/routing.ts
|
|
8007
8336
|
import { z as z14 } from "zod";
|
|
8008
|
-
var
|
|
8009
|
-
|
|
8337
|
+
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
8338
|
+
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
8339
|
+
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
8340
|
+
function sendJSON9(res, status, body) {
|
|
8341
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8342
|
+
res.end(JSON.stringify(body));
|
|
8343
|
+
}
|
|
8344
|
+
function unavailable(res) {
|
|
8345
|
+
sendJSON9(res, 503, { error: "BackendRouter not available" });
|
|
8346
|
+
return true;
|
|
8347
|
+
}
|
|
8348
|
+
function resolveChain(value, backends) {
|
|
8349
|
+
return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
|
|
8350
|
+
}
|
|
8351
|
+
function buildResolvedChains(routing, backends) {
|
|
8352
|
+
const out = {};
|
|
8353
|
+
out["default"] = resolveChain(routing.default, backends);
|
|
8354
|
+
for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
|
|
8355
|
+
const v = routing[tier];
|
|
8356
|
+
if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
|
|
8357
|
+
}
|
|
8358
|
+
if (routing.intelligence) {
|
|
8359
|
+
for (const [layer, v] of Object.entries(routing.intelligence)) {
|
|
8360
|
+
if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
if (routing.isolation) {
|
|
8364
|
+
for (const [tier, v] of Object.entries(routing.isolation)) {
|
|
8365
|
+
if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
if (routing.skills) {
|
|
8369
|
+
for (const [name, v] of Object.entries(routing.skills)) {
|
|
8370
|
+
if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
if (routing.modes) {
|
|
8374
|
+
for (const [mode, v] of Object.entries(routing.modes)) {
|
|
8375
|
+
if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
return out;
|
|
8379
|
+
}
|
|
8380
|
+
function handleConfig(res, deps) {
|
|
8381
|
+
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
8382
|
+
sendJSON9(res, 200, {
|
|
8383
|
+
routing: deps.routing,
|
|
8384
|
+
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
8385
|
+
backends: Object.keys(deps.backends)
|
|
8386
|
+
});
|
|
8387
|
+
return true;
|
|
8388
|
+
}
|
|
8389
|
+
function parseDecisionsQuery(url) {
|
|
8390
|
+
const qIdx = url.indexOf("?");
|
|
8391
|
+
if (qIdx === -1) return {};
|
|
8392
|
+
const p = new URLSearchParams(url.slice(qIdx + 1));
|
|
8393
|
+
const filter = {};
|
|
8394
|
+
const skill = p.get("skill");
|
|
8395
|
+
const mode = p.get("mode");
|
|
8396
|
+
const backend = p.get("backend");
|
|
8397
|
+
const limit = p.get("limit");
|
|
8398
|
+
if (skill) filter.skillName = skill;
|
|
8399
|
+
if (mode) filter.mode = mode;
|
|
8400
|
+
if (backend) filter.backendName = backend;
|
|
8401
|
+
if (limit) {
|
|
8402
|
+
const n = Number(limit);
|
|
8403
|
+
if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
|
|
8404
|
+
}
|
|
8405
|
+
return filter;
|
|
8406
|
+
}
|
|
8407
|
+
function handleDecisions(req, res, deps) {
|
|
8408
|
+
if (!deps.bus) return unavailable(res);
|
|
8409
|
+
const filter = parseDecisionsQuery(req.url ?? "");
|
|
8410
|
+
sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
|
|
8411
|
+
return true;
|
|
8412
|
+
}
|
|
8413
|
+
var UseCaseSchema = z14.discriminatedUnion("kind", [
|
|
8414
|
+
z14.object({
|
|
8415
|
+
kind: z14.literal("tier"),
|
|
8416
|
+
tier: z14.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
|
|
8417
|
+
}),
|
|
8418
|
+
z14.object({ kind: z14.literal("intelligence"), layer: z14.enum(["sel", "pesl"]) }),
|
|
8419
|
+
z14.object({ kind: z14.literal("isolation"), tier: z14.string() }),
|
|
8420
|
+
z14.object({ kind: z14.literal("maintenance") }),
|
|
8421
|
+
z14.object({ kind: z14.literal("chat") }),
|
|
8422
|
+
z14.object({
|
|
8423
|
+
kind: z14.literal("skill"),
|
|
8424
|
+
skillName: z14.string().min(1),
|
|
8425
|
+
cognitiveMode: z14.string().optional()
|
|
8426
|
+
}),
|
|
8427
|
+
z14.object({ kind: z14.literal("mode"), cognitiveMode: z14.string().min(1) })
|
|
8428
|
+
]);
|
|
8429
|
+
var TraceBodySchema = z14.object({
|
|
8430
|
+
useCase: UseCaseSchema,
|
|
8431
|
+
invocationOverride: z14.string().min(1).optional()
|
|
8432
|
+
});
|
|
8433
|
+
async function handleTrace(req, res, deps) {
|
|
8434
|
+
if (!deps.routing || !deps.backends) {
|
|
8435
|
+
unavailable(res);
|
|
8436
|
+
return true;
|
|
8437
|
+
}
|
|
8438
|
+
let raw;
|
|
8439
|
+
try {
|
|
8440
|
+
raw = await readBody(req);
|
|
8441
|
+
} catch {
|
|
8442
|
+
sendJSON9(res, 400, { error: "body read failed" });
|
|
8443
|
+
return true;
|
|
8444
|
+
}
|
|
8445
|
+
let parsed;
|
|
8446
|
+
try {
|
|
8447
|
+
parsed = JSON.parse(raw);
|
|
8448
|
+
} catch {
|
|
8449
|
+
sendJSON9(res, 400, { error: "invalid JSON body" });
|
|
8450
|
+
return true;
|
|
8451
|
+
}
|
|
8452
|
+
const r = TraceBodySchema.safeParse(parsed);
|
|
8453
|
+
if (!r.success) {
|
|
8454
|
+
sendJSON9(res, 400, { error: r.error.message });
|
|
8455
|
+
return true;
|
|
8456
|
+
}
|
|
8457
|
+
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
8458
|
+
try {
|
|
8459
|
+
const dryRunRouter = new BackendRouter({
|
|
8460
|
+
backends: deps.backends,
|
|
8461
|
+
routing: deps.routing
|
|
8462
|
+
});
|
|
8463
|
+
const { decision, def } = dryRunRouter.resolveDecisionAndDef(
|
|
8464
|
+
r.data.useCase,
|
|
8465
|
+
opts
|
|
8466
|
+
);
|
|
8467
|
+
sendJSON9(res, 200, { decision, def: { type: def.type } });
|
|
8468
|
+
} catch (err) {
|
|
8469
|
+
sendJSON9(res, 500, { error: String(err) });
|
|
8470
|
+
}
|
|
8471
|
+
return true;
|
|
8472
|
+
}
|
|
8473
|
+
function handleV1RoutingRoute(req, res, deps) {
|
|
8474
|
+
const url = req.url ?? "";
|
|
8475
|
+
const method = req.method ?? "GET";
|
|
8476
|
+
if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
|
|
8477
|
+
if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
|
|
8478
|
+
if (method === "POST" && TRACE_RE.test(url)) {
|
|
8479
|
+
void handleTrace(req, res, deps);
|
|
8480
|
+
return true;
|
|
8481
|
+
}
|
|
8482
|
+
return false;
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
// src/server/routes/sessions.ts
|
|
8486
|
+
import * as fs13 from "fs/promises";
|
|
8487
|
+
import * as path14 from "path";
|
|
8488
|
+
import { z as z15 } from "zod";
|
|
8489
|
+
var SessionCreateSchema = z15.object({
|
|
8490
|
+
sessionId: z15.string().min(1)
|
|
8010
8491
|
}).passthrough();
|
|
8011
8492
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8012
8493
|
function isSafeId(id) {
|
|
8013
|
-
return UUID_RE2.test(id) ||
|
|
8494
|
+
return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
|
|
8014
8495
|
}
|
|
8015
8496
|
function jsonResponse(res, status, data) {
|
|
8016
8497
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8017
8498
|
res.end(JSON.stringify(data));
|
|
8018
8499
|
}
|
|
8019
8500
|
function extractSessionId(url) {
|
|
8020
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
8501
|
+
const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
|
|
8021
8502
|
const id = segments.pop();
|
|
8022
8503
|
return id && id !== "sessions" ? id : null;
|
|
8023
8504
|
}
|
|
8024
8505
|
async function handleList2(res, sessionsDir) {
|
|
8025
8506
|
try {
|
|
8026
|
-
const entries = await
|
|
8507
|
+
const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
|
|
8027
8508
|
const sessions = [];
|
|
8028
8509
|
for (const entry of entries) {
|
|
8029
8510
|
if (!entry.isDirectory()) continue;
|
|
8030
8511
|
try {
|
|
8031
|
-
const content = await
|
|
8032
|
-
|
|
8512
|
+
const content = await fs13.readFile(
|
|
8513
|
+
path14.join(sessionsDir, entry.name, "session.json"),
|
|
8033
8514
|
"utf-8"
|
|
8034
8515
|
);
|
|
8035
8516
|
sessions.push(JSON.parse(content));
|
|
@@ -8054,7 +8535,7 @@ async function handleGet2(res, id, sessionsDir) {
|
|
|
8054
8535
|
return;
|
|
8055
8536
|
}
|
|
8056
8537
|
try {
|
|
8057
|
-
const content = await
|
|
8538
|
+
const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
|
|
8058
8539
|
jsonResponse(res, 200, JSON.parse(content));
|
|
8059
8540
|
} catch (err) {
|
|
8060
8541
|
if (err.code === "ENOENT") {
|
|
@@ -8077,9 +8558,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
8077
8558
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
8078
8559
|
return;
|
|
8079
8560
|
}
|
|
8080
|
-
const sessionDir =
|
|
8081
|
-
await
|
|
8082
|
-
await
|
|
8561
|
+
const sessionDir = path14.join(sessionsDir, session.sessionId);
|
|
8562
|
+
await fs13.mkdir(sessionDir, { recursive: true });
|
|
8563
|
+
await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
8083
8564
|
jsonResponse(res, 200, { ok: true });
|
|
8084
8565
|
} catch {
|
|
8085
8566
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -8093,10 +8574,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
8093
8574
|
return;
|
|
8094
8575
|
}
|
|
8095
8576
|
const body = await readBody(req);
|
|
8096
|
-
const updates =
|
|
8097
|
-
const sessionFilePath =
|
|
8098
|
-
const current = JSON.parse(await
|
|
8099
|
-
await
|
|
8577
|
+
const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
|
|
8578
|
+
const sessionFilePath = path14.join(sessionsDir, id, "session.json");
|
|
8579
|
+
const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
|
|
8580
|
+
await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
8100
8581
|
jsonResponse(res, 200, { ok: true });
|
|
8101
8582
|
} catch {
|
|
8102
8583
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -8109,7 +8590,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
8109
8590
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
8110
8591
|
return;
|
|
8111
8592
|
}
|
|
8112
|
-
await
|
|
8593
|
+
await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
|
|
8113
8594
|
jsonResponse(res, 200, { ok: true });
|
|
8114
8595
|
} catch {
|
|
8115
8596
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -8213,20 +8694,20 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
8213
8694
|
}
|
|
8214
8695
|
|
|
8215
8696
|
// src/server/routes/auth.ts
|
|
8216
|
-
import { z as
|
|
8697
|
+
import { z as z16 } from "zod";
|
|
8217
8698
|
import {
|
|
8218
8699
|
TokenScopeSchema,
|
|
8219
8700
|
BridgeKindSchema,
|
|
8220
8701
|
AuthTokenPublicSchema
|
|
8221
8702
|
} from "@harness-engineering/types";
|
|
8222
|
-
var CreateBodySchema =
|
|
8223
|
-
name:
|
|
8224
|
-
scopes:
|
|
8703
|
+
var CreateBodySchema = z16.object({
|
|
8704
|
+
name: z16.string().min(1).max(100),
|
|
8705
|
+
scopes: z16.array(TokenScopeSchema).min(1),
|
|
8225
8706
|
bridgeKind: BridgeKindSchema.optional(),
|
|
8226
|
-
tenantId:
|
|
8227
|
-
expiresAt:
|
|
8707
|
+
tenantId: z16.string().optional(),
|
|
8708
|
+
expiresAt: z16.string().datetime().optional()
|
|
8228
8709
|
});
|
|
8229
|
-
function
|
|
8710
|
+
function sendJSON10(res, status, body) {
|
|
8230
8711
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8231
8712
|
res.end(JSON.stringify(body));
|
|
8232
8713
|
}
|
|
@@ -8236,19 +8717,19 @@ async function handlePost(req, res, store) {
|
|
|
8236
8717
|
raw = await readBody(req);
|
|
8237
8718
|
} catch (err) {
|
|
8238
8719
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
8239
|
-
|
|
8720
|
+
sendJSON10(res, 413, { error: msg });
|
|
8240
8721
|
return;
|
|
8241
8722
|
}
|
|
8242
8723
|
let json;
|
|
8243
8724
|
try {
|
|
8244
8725
|
json = JSON.parse(raw);
|
|
8245
8726
|
} catch {
|
|
8246
|
-
|
|
8727
|
+
sendJSON10(res, 400, { error: "Invalid JSON body" });
|
|
8247
8728
|
return;
|
|
8248
8729
|
}
|
|
8249
8730
|
const parsed = CreateBodySchema.safeParse(json);
|
|
8250
8731
|
if (!parsed.success) {
|
|
8251
|
-
|
|
8732
|
+
sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
8252
8733
|
return;
|
|
8253
8734
|
}
|
|
8254
8735
|
try {
|
|
@@ -8261,37 +8742,37 @@ async function handlePost(req, res, store) {
|
|
|
8261
8742
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
8262
8743
|
const result = await store.create(input);
|
|
8263
8744
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
8264
|
-
|
|
8745
|
+
sendJSON10(res, 200, {
|
|
8265
8746
|
...publicRecord,
|
|
8266
8747
|
token: result.token
|
|
8267
8748
|
});
|
|
8268
8749
|
} catch (err) {
|
|
8269
8750
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
8270
8751
|
if (msg.includes("already exists")) {
|
|
8271
|
-
|
|
8752
|
+
sendJSON10(res, 409, { error: msg });
|
|
8272
8753
|
return;
|
|
8273
8754
|
}
|
|
8274
|
-
|
|
8755
|
+
sendJSON10(res, 500, { error: "Internal error creating token" });
|
|
8275
8756
|
}
|
|
8276
8757
|
}
|
|
8277
8758
|
async function handleList3(res, store) {
|
|
8278
8759
|
try {
|
|
8279
8760
|
const list = await store.list();
|
|
8280
|
-
|
|
8761
|
+
sendJSON10(res, 200, list);
|
|
8281
8762
|
} catch {
|
|
8282
|
-
|
|
8763
|
+
sendJSON10(res, 500, { error: "Internal error listing tokens" });
|
|
8283
8764
|
}
|
|
8284
8765
|
}
|
|
8285
8766
|
async function handleDelete2(res, store, id) {
|
|
8286
8767
|
try {
|
|
8287
8768
|
const ok = await store.revoke(id);
|
|
8288
8769
|
if (!ok) {
|
|
8289
|
-
|
|
8770
|
+
sendJSON10(res, 404, { error: "Token not found" });
|
|
8290
8771
|
return;
|
|
8291
8772
|
}
|
|
8292
|
-
|
|
8773
|
+
sendJSON10(res, 200, { deleted: true });
|
|
8293
8774
|
} catch {
|
|
8294
|
-
|
|
8775
|
+
sendJSON10(res, 500, { error: "Internal error revoking token" });
|
|
8295
8776
|
}
|
|
8296
8777
|
}
|
|
8297
8778
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -8316,12 +8797,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
8316
8797
|
return true;
|
|
8317
8798
|
}
|
|
8318
8799
|
}
|
|
8319
|
-
|
|
8800
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
8320
8801
|
return true;
|
|
8321
8802
|
}
|
|
8322
8803
|
|
|
8323
8804
|
// src/server/routes/local-model.ts
|
|
8324
|
-
function
|
|
8805
|
+
function sendJSON11(res, status, body) {
|
|
8325
8806
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8326
8807
|
res.end(JSON.stringify(body));
|
|
8327
8808
|
}
|
|
@@ -8329,36 +8810,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
8329
8810
|
const { method, url } = req;
|
|
8330
8811
|
if (url !== "/api/v1/local-model/status") return false;
|
|
8331
8812
|
if (method !== "GET") {
|
|
8332
|
-
|
|
8813
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
8333
8814
|
return true;
|
|
8334
8815
|
}
|
|
8335
8816
|
if (!getStatus) {
|
|
8336
|
-
|
|
8817
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
8337
8818
|
return true;
|
|
8338
8819
|
}
|
|
8339
8820
|
const status = getStatus();
|
|
8340
8821
|
if (!status) {
|
|
8341
|
-
|
|
8822
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
8342
8823
|
return true;
|
|
8343
8824
|
}
|
|
8344
|
-
|
|
8825
|
+
sendJSON11(res, 200, status);
|
|
8345
8826
|
return true;
|
|
8346
8827
|
}
|
|
8347
8828
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
8348
8829
|
const { method, url } = req;
|
|
8349
8830
|
if (url !== "/api/v1/local-models/status") return false;
|
|
8350
8831
|
if (method !== "GET") {
|
|
8351
|
-
|
|
8832
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
8352
8833
|
return true;
|
|
8353
8834
|
}
|
|
8354
8835
|
const statuses = getStatuses ? getStatuses() : [];
|
|
8355
|
-
|
|
8836
|
+
sendJSON11(res, 200, statuses);
|
|
8356
8837
|
return true;
|
|
8357
8838
|
}
|
|
8358
8839
|
|
|
8359
8840
|
// src/server/static.ts
|
|
8360
|
-
import * as
|
|
8361
|
-
import * as
|
|
8841
|
+
import * as fs14 from "fs";
|
|
8842
|
+
import * as path15 from "path";
|
|
8362
8843
|
var MIME_TYPES = {
|
|
8363
8844
|
".html": "text/html; charset=utf-8",
|
|
8364
8845
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -8378,29 +8859,29 @@ var MIME_TYPES = {
|
|
|
8378
8859
|
function handleStaticFile(req, res, dashboardDir) {
|
|
8379
8860
|
const { method, url } = req;
|
|
8380
8861
|
if (method !== "GET") return false;
|
|
8381
|
-
const apiPrefix =
|
|
8382
|
-
const wsPath =
|
|
8862
|
+
const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
|
|
8863
|
+
const wsPath = path15.posix.join(path15.posix.sep, "ws");
|
|
8383
8864
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
8384
8865
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
8385
|
-
const requestedPath =
|
|
8386
|
-
const resolved =
|
|
8387
|
-
if (!resolved.startsWith(
|
|
8388
|
-
return serveFile(
|
|
8866
|
+
const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8867
|
+
const resolved = path15.resolve(requestedPath);
|
|
8868
|
+
if (!resolved.startsWith(path15.resolve(dashboardDir))) {
|
|
8869
|
+
return serveFile(path15.join(dashboardDir, "index.html"), res);
|
|
8389
8870
|
}
|
|
8390
|
-
if (
|
|
8871
|
+
if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
|
|
8391
8872
|
return serveFile(resolved, res);
|
|
8392
8873
|
}
|
|
8393
|
-
const indexPath =
|
|
8394
|
-
if (
|
|
8874
|
+
const indexPath = path15.join(dashboardDir, "index.html");
|
|
8875
|
+
if (fs14.existsSync(indexPath)) {
|
|
8395
8876
|
return serveFile(indexPath, res);
|
|
8396
8877
|
}
|
|
8397
8878
|
return false;
|
|
8398
8879
|
}
|
|
8399
8880
|
function serveFile(filePath, res) {
|
|
8400
|
-
const ext =
|
|
8881
|
+
const ext = path15.extname(filePath).toLowerCase();
|
|
8401
8882
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
8402
8883
|
try {
|
|
8403
|
-
const content =
|
|
8884
|
+
const content = fs14.readFileSync(filePath);
|
|
8404
8885
|
res.writeHead(200, { "Content-Type": contentType });
|
|
8405
8886
|
res.end(content);
|
|
8406
8887
|
return true;
|
|
@@ -8410,8 +8891,8 @@ function serveFile(filePath, res) {
|
|
|
8410
8891
|
}
|
|
8411
8892
|
|
|
8412
8893
|
// src/server/plan-watcher.ts
|
|
8413
|
-
import * as
|
|
8414
|
-
import * as
|
|
8894
|
+
import * as fs15 from "fs";
|
|
8895
|
+
import * as path16 from "path";
|
|
8415
8896
|
var PlanWatcher = class {
|
|
8416
8897
|
plansDir;
|
|
8417
8898
|
queue;
|
|
@@ -8425,11 +8906,11 @@ var PlanWatcher = class {
|
|
|
8425
8906
|
* Creates the directory if it does not exist.
|
|
8426
8907
|
*/
|
|
8427
8908
|
start() {
|
|
8428
|
-
|
|
8429
|
-
this.watcher =
|
|
8909
|
+
fs15.mkdirSync(this.plansDir, { recursive: true });
|
|
8910
|
+
this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
|
|
8430
8911
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
8431
|
-
const filePath =
|
|
8432
|
-
if (
|
|
8912
|
+
const filePath = path16.join(this.plansDir, filename);
|
|
8913
|
+
if (fs15.existsSync(filePath)) {
|
|
8433
8914
|
void this.handleNewPlan(filename);
|
|
8434
8915
|
}
|
|
8435
8916
|
}
|
|
@@ -8462,7 +8943,7 @@ var PlanWatcher = class {
|
|
|
8462
8943
|
// src/auth/tokens.ts
|
|
8463
8944
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
8464
8945
|
import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
|
|
8465
|
-
import { dirname as
|
|
8946
|
+
import { dirname as dirname5 } from "path";
|
|
8466
8947
|
import bcrypt from "bcryptjs";
|
|
8467
8948
|
import {
|
|
8468
8949
|
AuthTokenSchema,
|
|
@@ -8482,8 +8963,8 @@ function parseToken(raw) {
|
|
|
8482
8963
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
8483
8964
|
}
|
|
8484
8965
|
var TokenStore = class {
|
|
8485
|
-
constructor(
|
|
8486
|
-
this.path =
|
|
8966
|
+
constructor(path24) {
|
|
8967
|
+
this.path = path24;
|
|
8487
8968
|
}
|
|
8488
8969
|
path;
|
|
8489
8970
|
cache = null;
|
|
@@ -8504,7 +8985,7 @@ var TokenStore = class {
|
|
|
8504
8985
|
return this.cache;
|
|
8505
8986
|
}
|
|
8506
8987
|
async persist(records) {
|
|
8507
|
-
await mkdir7(
|
|
8988
|
+
await mkdir7(dirname5(this.path), { recursive: true });
|
|
8508
8989
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
|
|
8509
8990
|
await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
8510
8991
|
await rename2(tmp, this.path);
|
|
@@ -8587,11 +9068,11 @@ var TokenStore = class {
|
|
|
8587
9068
|
|
|
8588
9069
|
// src/auth/audit.ts
|
|
8589
9070
|
import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
8590
|
-
import { dirname as
|
|
9071
|
+
import { dirname as dirname6 } from "path";
|
|
8591
9072
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
8592
9073
|
var AuditLogger = class {
|
|
8593
|
-
constructor(
|
|
8594
|
-
this.path =
|
|
9074
|
+
constructor(path24, opts = {}) {
|
|
9075
|
+
this.path = path24;
|
|
8595
9076
|
this.opts = opts;
|
|
8596
9077
|
}
|
|
8597
9078
|
path;
|
|
@@ -8618,7 +9099,7 @@ var AuditLogger = class {
|
|
|
8618
9099
|
async writeLine(line) {
|
|
8619
9100
|
try {
|
|
8620
9101
|
if (this.opts.createDir !== false && !this.dirEnsured) {
|
|
8621
|
-
await mkdir8(
|
|
9102
|
+
await mkdir8(dirname6(this.path), { recursive: true });
|
|
8622
9103
|
this.dirEnsured = true;
|
|
8623
9104
|
}
|
|
8624
9105
|
await appendFile(this.path, line, "utf8");
|
|
@@ -8718,14 +9199,36 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8718
9199
|
pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
|
|
8719
9200
|
scope: "read-telemetry",
|
|
8720
9201
|
description: "Prompt-cache hit/miss snapshot (rolling window)."
|
|
9202
|
+
},
|
|
9203
|
+
// ── Spec B Phase 5 routing observability ──
|
|
9204
|
+
// D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
|
|
9205
|
+
// precedent (read-only observability). A dedicated `read-routing`
|
|
9206
|
+
// scope was rejected to avoid a TokenScopeSchema + ADR cascade.
|
|
9207
|
+
{
|
|
9208
|
+
method: "GET",
|
|
9209
|
+
pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
|
|
9210
|
+
scope: "read-telemetry",
|
|
9211
|
+
description: "Current routing config + resolved fallback chains + known backends."
|
|
9212
|
+
},
|
|
9213
|
+
{
|
|
9214
|
+
method: "GET",
|
|
9215
|
+
pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
|
|
9216
|
+
scope: "read-telemetry",
|
|
9217
|
+
description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
|
|
9218
|
+
},
|
|
9219
|
+
{
|
|
9220
|
+
method: "POST",
|
|
9221
|
+
pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
|
|
9222
|
+
scope: "read-telemetry",
|
|
9223
|
+
description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
|
|
8721
9224
|
}
|
|
8722
9225
|
];
|
|
8723
9226
|
function isV1Bridge(method, url) {
|
|
8724
9227
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8725
9228
|
}
|
|
8726
|
-
function requiredBridgeScope(method,
|
|
9229
|
+
function requiredBridgeScope(method, path24) {
|
|
8727
9230
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8728
|
-
if (r.method === method && r.pattern.test(
|
|
9231
|
+
if (r.method === method && r.pattern.test(path24)) return r.scope;
|
|
8729
9232
|
}
|
|
8730
9233
|
return null;
|
|
8731
9234
|
}
|
|
@@ -8735,24 +9238,24 @@ function hasScope(held, required) {
|
|
|
8735
9238
|
if (held.includes("admin")) return true;
|
|
8736
9239
|
return held.includes(required);
|
|
8737
9240
|
}
|
|
8738
|
-
function requiredScopeForRoute(method,
|
|
8739
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
9241
|
+
function requiredScopeForRoute(method, path24) {
|
|
9242
|
+
const bridgeScope = requiredBridgeScope(method, path24);
|
|
8740
9243
|
if (bridgeScope) return bridgeScope;
|
|
8741
|
-
if (
|
|
8742
|
-
if (
|
|
8743
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
8744
|
-
if ((
|
|
8745
|
-
if (
|
|
8746
|
-
if (
|
|
8747
|
-
if (
|
|
8748
|
-
if (
|
|
8749
|
-
if (
|
|
8750
|
-
if (
|
|
9244
|
+
if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
9245
|
+
if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
9246
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
|
|
9247
|
+
if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
|
|
9248
|
+
if (path24.startsWith("/api/interactions")) return "resolve-interaction";
|
|
9249
|
+
if (path24.startsWith("/api/plans")) return "read-status";
|
|
9250
|
+
if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
|
|
9251
|
+
if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
9252
|
+
if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
9253
|
+
if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
|
|
8751
9254
|
return "read-status";
|
|
8752
|
-
if (
|
|
8753
|
-
if (
|
|
8754
|
-
if (
|
|
8755
|
-
if (
|
|
9255
|
+
if (path24.startsWith("/api/maintenance")) return "trigger-job";
|
|
9256
|
+
if (path24.startsWith("/api/streams")) return "read-status";
|
|
9257
|
+
if (path24.startsWith("/api/sessions")) return "read-status";
|
|
9258
|
+
if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
8756
9259
|
return null;
|
|
8757
9260
|
}
|
|
8758
9261
|
|
|
@@ -8816,6 +9319,15 @@ var OrchestratorServer = class {
|
|
|
8816
9319
|
getLocalModelStatuses = null;
|
|
8817
9320
|
webhooks;
|
|
8818
9321
|
cacheMetrics;
|
|
9322
|
+
// Spec B Phase 5 — routing observability accessor closures + the WS
|
|
9323
|
+
// broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
|
|
9324
|
+
// calls it explicitly; clearListeners in Orchestrator.stop() is the
|
|
9325
|
+
// belt-and-suspenders second line).
|
|
9326
|
+
getBackendRouterFn = null;
|
|
9327
|
+
getRoutingDecisionBusFn = null;
|
|
9328
|
+
getRoutingConfigFn = null;
|
|
9329
|
+
getBackendsFn = null;
|
|
9330
|
+
routingDecisionUnsubscribe = null;
|
|
8819
9331
|
recorder = null;
|
|
8820
9332
|
planWatcher = null;
|
|
8821
9333
|
tokenStore;
|
|
@@ -8828,8 +9340,8 @@ var OrchestratorServer = class {
|
|
|
8828
9340
|
this.orchestrator = orchestrator;
|
|
8829
9341
|
this.port = port;
|
|
8830
9342
|
this.initDependencies(deps);
|
|
8831
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8832
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
9343
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
|
|
9344
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
|
|
8833
9345
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8834
9346
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8835
9347
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8842,20 +9354,24 @@ var OrchestratorServer = class {
|
|
|
8842
9354
|
}
|
|
8843
9355
|
initDependencies(deps) {
|
|
8844
9356
|
this.interactionQueue = deps?.interactionQueue;
|
|
8845
|
-
this.plansDir = deps?.plansDir ??
|
|
8846
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
9357
|
+
this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
|
|
9358
|
+
this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
|
|
8847
9359
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8848
9360
|
this.pipeline = deps?.pipeline ?? null;
|
|
8849
9361
|
this.analysisArchive = deps?.analysisArchive;
|
|
8850
9362
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8851
9363
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8852
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
9364
|
+
this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
|
|
8853
9365
|
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8854
9366
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8855
9367
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8856
9368
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
8857
9369
|
this.webhooks = deps?.webhooks;
|
|
8858
9370
|
this.cacheMetrics = deps?.cacheMetrics;
|
|
9371
|
+
this.getBackendRouterFn = deps?.getBackendRouter ?? null;
|
|
9372
|
+
this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
|
|
9373
|
+
this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
|
|
9374
|
+
this.getBackendsFn = deps?.getBackends ?? null;
|
|
8859
9375
|
}
|
|
8860
9376
|
wireEvents() {
|
|
8861
9377
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -8866,6 +9382,12 @@ var OrchestratorServer = class {
|
|
|
8866
9382
|
};
|
|
8867
9383
|
this.orchestrator.on("state_change", this.stateChangeListener);
|
|
8868
9384
|
this.orchestrator.on("agent_event", this.agentEventListener);
|
|
9385
|
+
const bus = this.getRoutingDecisionBusFn?.() ?? null;
|
|
9386
|
+
if (bus) {
|
|
9387
|
+
this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
|
|
9388
|
+
this.broadcaster.broadcast("routing:decision", decision);
|
|
9389
|
+
});
|
|
9390
|
+
}
|
|
8869
9391
|
}
|
|
8870
9392
|
/**
|
|
8871
9393
|
* Broadcast a new interaction to all WebSocket clients.
|
|
@@ -9021,6 +9543,14 @@ var OrchestratorServer = class {
|
|
|
9021
9543
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
9022
9544
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
9023
9545
|
}),
|
|
9546
|
+
// Spec B Phase 5 — routing observability. Returns 503 when the
|
|
9547
|
+
// backendFactory is null (legacy single-backend configs).
|
|
9548
|
+
(req, res) => handleV1RoutingRoute(req, res, {
|
|
9549
|
+
router: this.getBackendRouterFn?.() ?? null,
|
|
9550
|
+
bus: this.getRoutingDecisionBusFn?.() ?? null,
|
|
9551
|
+
routing: this.getRoutingConfigFn?.() ?? null,
|
|
9552
|
+
backends: this.getBackendsFn?.() ?? null
|
|
9553
|
+
}),
|
|
9024
9554
|
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9025
9555
|
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9026
9556
|
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
@@ -9117,17 +9647,21 @@ var OrchestratorServer = class {
|
|
|
9117
9647
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
9118
9648
|
this.planWatcher.start();
|
|
9119
9649
|
}
|
|
9120
|
-
return new Promise((
|
|
9650
|
+
return new Promise((resolve8) => {
|
|
9121
9651
|
const host = getBindHost();
|
|
9122
9652
|
this.httpServer.listen(this.port, host, () => {
|
|
9123
9653
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
9124
|
-
|
|
9654
|
+
resolve8();
|
|
9125
9655
|
});
|
|
9126
9656
|
});
|
|
9127
9657
|
}
|
|
9128
9658
|
stop() {
|
|
9129
9659
|
this.orchestrator.removeListener("state_change", this.stateChangeListener);
|
|
9130
9660
|
this.orchestrator.removeListener("agent_event", this.agentEventListener);
|
|
9661
|
+
if (this.routingDecisionUnsubscribe) {
|
|
9662
|
+
this.routingDecisionUnsubscribe();
|
|
9663
|
+
this.routingDecisionUnsubscribe = null;
|
|
9664
|
+
}
|
|
9131
9665
|
if (this.planWatcher) {
|
|
9132
9666
|
this.planWatcher.stop();
|
|
9133
9667
|
this.planWatcher = null;
|
|
@@ -9140,7 +9674,7 @@ var OrchestratorServer = class {
|
|
|
9140
9674
|
// src/gateway/webhooks/store.ts
|
|
9141
9675
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
9142
9676
|
import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
|
|
9143
|
-
import { dirname as
|
|
9677
|
+
import { dirname as dirname7 } from "path";
|
|
9144
9678
|
import { WebhookSubscriptionSchema } from "@harness-engineering/types";
|
|
9145
9679
|
|
|
9146
9680
|
// src/gateway/webhooks/signer.ts
|
|
@@ -9171,8 +9705,8 @@ function genSecret2() {
|
|
|
9171
9705
|
return randomBytes4(32).toString("base64url");
|
|
9172
9706
|
}
|
|
9173
9707
|
var WebhookStore = class {
|
|
9174
|
-
constructor(
|
|
9175
|
-
this.path =
|
|
9708
|
+
constructor(path24) {
|
|
9709
|
+
this.path = path24;
|
|
9176
9710
|
}
|
|
9177
9711
|
path;
|
|
9178
9712
|
cache = null;
|
|
@@ -9195,7 +9729,7 @@ var WebhookStore = class {
|
|
|
9195
9729
|
async persist(records) {
|
|
9196
9730
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
|
|
9197
9731
|
try {
|
|
9198
|
-
await mkdir9(
|
|
9732
|
+
await mkdir9(dirname7(this.path), { recursive: true });
|
|
9199
9733
|
await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
|
|
9200
9734
|
await rename3(tmp, this.path);
|
|
9201
9735
|
await chmod(this.path, 384);
|
|
@@ -10164,8 +10698,8 @@ var StructuredLogger = class {
|
|
|
10164
10698
|
};
|
|
10165
10699
|
|
|
10166
10700
|
// src/workspace/config-scanner.ts
|
|
10167
|
-
import { existsSync as
|
|
10168
|
-
import { join as
|
|
10701
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
10702
|
+
import { join as join15, relative } from "path";
|
|
10169
10703
|
import {
|
|
10170
10704
|
scanForInjection,
|
|
10171
10705
|
SecurityScanner,
|
|
@@ -10189,10 +10723,10 @@ function adjustFindingSeverity(findings) {
|
|
|
10189
10723
|
});
|
|
10190
10724
|
}
|
|
10191
10725
|
async function scanSingleFile(filePath, targetDir, scanner) {
|
|
10192
|
-
if (!
|
|
10726
|
+
if (!existsSync6(filePath)) return null;
|
|
10193
10727
|
let content;
|
|
10194
10728
|
try {
|
|
10195
|
-
content =
|
|
10729
|
+
content = readFileSync6(filePath, "utf8");
|
|
10196
10730
|
} catch {
|
|
10197
10731
|
return null;
|
|
10198
10732
|
}
|
|
@@ -10211,7 +10745,7 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
10211
10745
|
const scanner = new SecurityScanner(parseSecurityConfig({}));
|
|
10212
10746
|
const results = [];
|
|
10213
10747
|
for (const configFile of CONFIG_FILES) {
|
|
10214
|
-
const result = await scanSingleFile(
|
|
10748
|
+
const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
|
|
10215
10749
|
if (result) results.push(result);
|
|
10216
10750
|
}
|
|
10217
10751
|
return { exitCode: computeScanExitCode(results), results };
|
|
@@ -10725,19 +11259,19 @@ var SingleProcessLeaderElector = class {
|
|
|
10725
11259
|
};
|
|
10726
11260
|
|
|
10727
11261
|
// src/maintenance/reporter.ts
|
|
10728
|
-
import * as
|
|
10729
|
-
import * as
|
|
10730
|
-
import { z as
|
|
10731
|
-
var RunResultSchema =
|
|
10732
|
-
taskId:
|
|
10733
|
-
startedAt:
|
|
10734
|
-
completedAt:
|
|
10735
|
-
status:
|
|
10736
|
-
findings:
|
|
10737
|
-
fixed:
|
|
10738
|
-
prUrl:
|
|
10739
|
-
prUpdated:
|
|
10740
|
-
error:
|
|
11262
|
+
import * as fs16 from "fs";
|
|
11263
|
+
import * as path18 from "path";
|
|
11264
|
+
import { z as z17 } from "zod";
|
|
11265
|
+
var RunResultSchema = z17.object({
|
|
11266
|
+
taskId: z17.string(),
|
|
11267
|
+
startedAt: z17.string(),
|
|
11268
|
+
completedAt: z17.string(),
|
|
11269
|
+
status: z17.enum(["success", "failure", "skipped", "no-issues"]),
|
|
11270
|
+
findings: z17.number(),
|
|
11271
|
+
fixed: z17.number(),
|
|
11272
|
+
prUrl: z17.string().nullable(),
|
|
11273
|
+
prUpdated: z17.boolean(),
|
|
11274
|
+
error: z17.string().optional()
|
|
10741
11275
|
});
|
|
10742
11276
|
var MAX_HISTORY = 500;
|
|
10743
11277
|
var fallbackLogger = {
|
|
@@ -10761,10 +11295,10 @@ var MaintenanceReporter = class {
|
|
|
10761
11295
|
*/
|
|
10762
11296
|
async load() {
|
|
10763
11297
|
try {
|
|
10764
|
-
await
|
|
10765
|
-
const filePath =
|
|
10766
|
-
const data = await
|
|
10767
|
-
const parsed =
|
|
11298
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11299
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11300
|
+
const data = await fs16.promises.readFile(filePath, "utf-8");
|
|
11301
|
+
const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
10768
11302
|
if (parsed.success) {
|
|
10769
11303
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10770
11304
|
}
|
|
@@ -10797,9 +11331,9 @@ var MaintenanceReporter = class {
|
|
|
10797
11331
|
*/
|
|
10798
11332
|
async persist() {
|
|
10799
11333
|
try {
|
|
10800
|
-
await
|
|
10801
|
-
const filePath =
|
|
10802
|
-
await
|
|
11334
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11335
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11336
|
+
await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
10803
11337
|
} catch (err) {
|
|
10804
11338
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10805
11339
|
}
|
|
@@ -11286,7 +11820,7 @@ function parseStatusLine(output) {
|
|
|
11286
11820
|
// src/maintenance/check-script-runner.ts
|
|
11287
11821
|
import { execFile as execFile6 } from "child_process";
|
|
11288
11822
|
import { promisify as promisify3 } from "util";
|
|
11289
|
-
import * as
|
|
11823
|
+
import * as path19 from "path";
|
|
11290
11824
|
var execFileAsync = promisify3(execFile6);
|
|
11291
11825
|
var CheckScriptRunner = class {
|
|
11292
11826
|
constructor(cwd) {
|
|
@@ -11305,7 +11839,7 @@ var CheckScriptRunner = class {
|
|
|
11305
11839
|
}
|
|
11306
11840
|
};
|
|
11307
11841
|
async function captureScript(spec, projectRoot) {
|
|
11308
|
-
const resolved =
|
|
11842
|
+
const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
|
|
11309
11843
|
const args = spec.args ?? [];
|
|
11310
11844
|
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11311
11845
|
try {
|
|
@@ -11395,8 +11929,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
|
11395
11929
|
}
|
|
11396
11930
|
|
|
11397
11931
|
// src/maintenance/output-store.ts
|
|
11398
|
-
import * as
|
|
11399
|
-
import * as
|
|
11932
|
+
import * as fs17 from "fs";
|
|
11933
|
+
import * as path20 from "path";
|
|
11400
11934
|
var DEFAULT_RETENTION = {
|
|
11401
11935
|
runs: 50,
|
|
11402
11936
|
maxAgeDays: 30
|
|
@@ -11436,13 +11970,13 @@ var TaskOutputStore = class {
|
|
|
11436
11970
|
async write(taskId, entry, retention) {
|
|
11437
11971
|
this.ensureSafeTaskId(taskId);
|
|
11438
11972
|
const dir = this.dirFor(taskId);
|
|
11439
|
-
await
|
|
11973
|
+
await fs17.promises.mkdir(dir, { recursive: true });
|
|
11440
11974
|
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11441
|
-
const filePath =
|
|
11975
|
+
const filePath = path20.join(dir, fileName);
|
|
11442
11976
|
const tmpPath = `${filePath}.tmp`;
|
|
11443
11977
|
const payload = JSON.stringify(entry, null, 2);
|
|
11444
|
-
await
|
|
11445
|
-
await
|
|
11978
|
+
await fs17.promises.writeFile(tmpPath, payload, "utf-8");
|
|
11979
|
+
await fs17.promises.rename(tmpPath, filePath);
|
|
11446
11980
|
try {
|
|
11447
11981
|
await this.applyRetention(taskId, retention);
|
|
11448
11982
|
} catch (err) {
|
|
@@ -11466,7 +12000,7 @@ var TaskOutputStore = class {
|
|
|
11466
12000
|
const slice = fileNames.slice(offset, offset + limit);
|
|
11467
12001
|
const out = [];
|
|
11468
12002
|
for (const name of slice) {
|
|
11469
|
-
const entry = await this.readEntry(
|
|
12003
|
+
const entry = await this.readEntry(path20.join(dir, name));
|
|
11470
12004
|
if (entry) out.push(entry);
|
|
11471
12005
|
}
|
|
11472
12006
|
return out;
|
|
@@ -11482,18 +12016,18 @@ var TaskOutputStore = class {
|
|
|
11482
12016
|
}
|
|
11483
12017
|
const dir = this.dirFor(taskId);
|
|
11484
12018
|
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
11485
|
-
return this.readEntry(
|
|
12019
|
+
return this.readEntry(path20.join(dir, fileName));
|
|
11486
12020
|
}
|
|
11487
12021
|
/**
|
|
11488
12022
|
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
11489
12023
|
* outputs from outside the store API.
|
|
11490
12024
|
*/
|
|
11491
12025
|
dirFor(taskId) {
|
|
11492
|
-
return
|
|
12026
|
+
return path20.join(this.rootDir, taskId, "outputs");
|
|
11493
12027
|
}
|
|
11494
12028
|
async readEntry(filePath) {
|
|
11495
12029
|
try {
|
|
11496
|
-
const buf = await
|
|
12030
|
+
const buf = await fs17.promises.readFile(filePath, "utf-8");
|
|
11497
12031
|
const parsed = JSON.parse(buf);
|
|
11498
12032
|
return parsed;
|
|
11499
12033
|
} catch {
|
|
@@ -11515,7 +12049,7 @@ var TaskOutputStore = class {
|
|
|
11515
12049
|
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
11516
12050
|
for (const name of toRemove) {
|
|
11517
12051
|
try {
|
|
11518
|
-
await
|
|
12052
|
+
await fs17.promises.unlink(path20.join(dir, name));
|
|
11519
12053
|
} catch {
|
|
11520
12054
|
}
|
|
11521
12055
|
}
|
|
@@ -11524,7 +12058,7 @@ var TaskOutputStore = class {
|
|
|
11524
12058
|
async function listJsonFilesDescending(dir) {
|
|
11525
12059
|
let names;
|
|
11526
12060
|
try {
|
|
11527
|
-
names = await
|
|
12061
|
+
names = await fs17.promises.readdir(dir);
|
|
11528
12062
|
} catch {
|
|
11529
12063
|
return [];
|
|
11530
12064
|
}
|
|
@@ -11698,8 +12232,8 @@ function validateCheckShape(prefix, task, errors) {
|
|
|
11698
12232
|
});
|
|
11699
12233
|
}
|
|
11700
12234
|
if (hasScript) {
|
|
11701
|
-
const
|
|
11702
|
-
if (!
|
|
12235
|
+
const path24 = task.checkScript?.path;
|
|
12236
|
+
if (!path24 || path24.trim().length === 0) {
|
|
11703
12237
|
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
11704
12238
|
}
|
|
11705
12239
|
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
@@ -11825,9 +12359,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
|
|
|
11825
12359
|
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
11826
12360
|
}
|
|
11827
12361
|
}
|
|
11828
|
-
function reportCycle(
|
|
11829
|
-
const cycleStart =
|
|
11830
|
-
const cyclePath = cycleStart >= 0 ? [...
|
|
12362
|
+
function reportCycle(path24, next, errors, reported) {
|
|
12363
|
+
const cycleStart = path24.indexOf(next);
|
|
12364
|
+
const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
|
|
11831
12365
|
const key = cyclePath.join("\u2192");
|
|
11832
12366
|
if (reported.has(key)) return;
|
|
11833
12367
|
reported.add(key);
|
|
@@ -11838,11 +12372,6 @@ function reportCycle(path22, next, errors, reported) {
|
|
|
11838
12372
|
}
|
|
11839
12373
|
|
|
11840
12374
|
// src/orchestrator.ts
|
|
11841
|
-
function useCaseForBackendParam(issue, backendParam) {
|
|
11842
|
-
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
11843
|
-
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
11844
|
-
return { kind: "tier", tier };
|
|
11845
|
-
}
|
|
11846
12375
|
var Orchestrator = class extends EventEmitter {
|
|
11847
12376
|
state;
|
|
11848
12377
|
config;
|
|
@@ -11867,6 +12396,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11867
12396
|
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
11868
12397
|
*/
|
|
11869
12398
|
backendFactory;
|
|
12399
|
+
/**
|
|
12400
|
+
* Spec B Phase 4 (D8): per-orchestrator in-process bus for
|
|
12401
|
+
* `RoutingDecision` events. Constructed alongside backendFactory when
|
|
12402
|
+
* agent.backends synthesis succeeds; null when legacy single-backend
|
|
12403
|
+
* config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
|
|
12404
|
+
* subscribe via `getRoutingDecisionBus()`.
|
|
12405
|
+
*/
|
|
12406
|
+
routingDecisionBus;
|
|
11870
12407
|
/**
|
|
11871
12408
|
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
11872
12409
|
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
@@ -11889,6 +12426,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11889
12426
|
* so this map is the single source of truth post-migration.
|
|
11890
12427
|
*/
|
|
11891
12428
|
localResolvers = /* @__PURE__ */ new Map();
|
|
12429
|
+
/**
|
|
12430
|
+
* Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
|
|
12431
|
+
* construction from `projectRoot/agents/skills/`. Consulted by
|
|
12432
|
+
* `buildRoutingUseCase` at dispatch start to construct
|
|
12433
|
+
* `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
|
|
12434
|
+
* Empty when the orchestrator runs outside a harness project root
|
|
12435
|
+
* (then dispatch falls through to per-tier, preserving F11/N2).
|
|
12436
|
+
*/
|
|
12437
|
+
skillCatalog;
|
|
11892
12438
|
/**
|
|
11893
12439
|
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
11894
12440
|
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
@@ -11937,7 +12483,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11937
12483
|
completionHandler;
|
|
11938
12484
|
/** Project root directory, derived from workspace root. */
|
|
11939
12485
|
get projectRoot() {
|
|
11940
|
-
return
|
|
12486
|
+
return path21.resolve(this.config.workspace.root, "..", "..");
|
|
11941
12487
|
}
|
|
11942
12488
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
11943
12489
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -11981,6 +12527,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11981
12527
|
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
11982
12528
|
);
|
|
11983
12529
|
}
|
|
12530
|
+
const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
|
|
12531
|
+
this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
|
|
12532
|
+
if (this.skillCatalog.length === 0) {
|
|
12533
|
+
this.logger.warn(
|
|
12534
|
+
`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")}.`
|
|
12535
|
+
);
|
|
12536
|
+
}
|
|
11984
12537
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
11985
12538
|
this.workspace = new WorkspaceManager(config.workspace, {
|
|
11986
12539
|
emitEvent: (event) => {
|
|
@@ -11992,10 +12545,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11992
12545
|
this.renderer = new PromptRenderer();
|
|
11993
12546
|
this.overrideBackend = overrides?.backend ?? null;
|
|
11994
12547
|
this.interactionQueue = new InteractionQueue(
|
|
11995
|
-
|
|
12548
|
+
path21.join(config.workspace.root, "..", "interactions"),
|
|
11996
12549
|
this
|
|
11997
12550
|
);
|
|
11998
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12551
|
+
this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
|
|
11999
12552
|
const backendsMap = this.config.agent.backends ?? {};
|
|
12000
12553
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
12001
12554
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -12016,6 +12569,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12016
12569
|
const routing = this.config.agent.routing ?? {
|
|
12017
12570
|
default: firstBackendName ?? "primary"
|
|
12018
12571
|
};
|
|
12572
|
+
this.routingDecisionBus = new RoutingDecisionBus({
|
|
12573
|
+
capacity: 500,
|
|
12574
|
+
logger: this.logger
|
|
12575
|
+
});
|
|
12019
12576
|
this.backendFactory = new OrchestratorBackendFactory({
|
|
12020
12577
|
backends: this.config.agent.backends,
|
|
12021
12578
|
routing,
|
|
@@ -12023,6 +12580,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12023
12580
|
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
12024
12581
|
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
12025
12582
|
cacheMetrics: this.cacheMetrics,
|
|
12583
|
+
decisionBus: this.routingDecisionBus,
|
|
12026
12584
|
getResolverModelFor: (name) => {
|
|
12027
12585
|
const resolver = this.localResolvers.get(name);
|
|
12028
12586
|
return resolver ? () => resolver.resolveModel() : void 0;
|
|
@@ -12030,6 +12588,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12030
12588
|
});
|
|
12031
12589
|
} else {
|
|
12032
12590
|
this.backendFactory = null;
|
|
12591
|
+
this.routingDecisionBus = null;
|
|
12033
12592
|
}
|
|
12034
12593
|
this.pipeline = null;
|
|
12035
12594
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
@@ -12039,7 +12598,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12039
12598
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
12040
12599
|
});
|
|
12041
12600
|
this.recorder = new StreamRecorder(
|
|
12042
|
-
|
|
12601
|
+
path21.resolve(config.workspace.root, "..", "streams"),
|
|
12043
12602
|
this.logger
|
|
12044
12603
|
);
|
|
12045
12604
|
const self = this;
|
|
@@ -12070,10 +12629,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12070
12629
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
12071
12630
|
if (config.server?.port) {
|
|
12072
12631
|
const webhookStore = new WebhookStore(
|
|
12073
|
-
|
|
12632
|
+
path21.join(this.projectRoot, ".harness", "webhooks.json")
|
|
12074
12633
|
);
|
|
12075
12634
|
this.webhookQueue = new WebhookQueue(
|
|
12076
|
-
|
|
12635
|
+
path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
12077
12636
|
);
|
|
12078
12637
|
const webhookDelivery = new WebhookDelivery({
|
|
12079
12638
|
queue: this.webhookQueue,
|
|
@@ -12111,7 +12670,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12111
12670
|
queue: this.webhookQueue
|
|
12112
12671
|
},
|
|
12113
12672
|
cacheMetrics: this.cacheMetrics,
|
|
12114
|
-
|
|
12673
|
+
// Spec B Phase 5: routing observability accessors. Closures so the
|
|
12674
|
+
// server re-reads on every request — stop() / start() do not
|
|
12675
|
+
// require server reconstruction. Returns null if no backendFactory
|
|
12676
|
+
// (legacy single-backend configs), and the route handler renders
|
|
12677
|
+
// 503 in that case.
|
|
12678
|
+
getBackendRouter: () => this.getBackendRouter(),
|
|
12679
|
+
getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
|
|
12680
|
+
getRoutingConfig: () => this.getRoutingConfig(),
|
|
12681
|
+
getBackends: () => this.getBackends(),
|
|
12682
|
+
plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
12115
12683
|
pipeline: this.pipeline,
|
|
12116
12684
|
analysisArchive: this.analysisArchive,
|
|
12117
12685
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -12221,7 +12789,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12221
12789
|
}
|
|
12222
12790
|
};
|
|
12223
12791
|
const outputStore = new TaskOutputStore({
|
|
12224
|
-
rootDir:
|
|
12792
|
+
rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12225
12793
|
logger: this.logger
|
|
12226
12794
|
});
|
|
12227
12795
|
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
@@ -12262,7 +12830,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12262
12830
|
${messages}`);
|
|
12263
12831
|
}
|
|
12264
12832
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
12265
|
-
persistDir:
|
|
12833
|
+
persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12266
12834
|
logger: this.logger
|
|
12267
12835
|
});
|
|
12268
12836
|
await this.maintenanceReporter.load();
|
|
@@ -12313,10 +12881,17 @@ ${messages}`);
|
|
|
12313
12881
|
}
|
|
12314
12882
|
}
|
|
12315
12883
|
createIntelligencePipeline() {
|
|
12884
|
+
if (!this.backendFactory) {
|
|
12885
|
+
this.logger.warn(
|
|
12886
|
+
"intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
|
|
12887
|
+
);
|
|
12888
|
+
return null;
|
|
12889
|
+
}
|
|
12316
12890
|
const bundle = buildIntelligencePipeline({
|
|
12317
12891
|
config: this.config,
|
|
12318
12892
|
localResolvers: this.localResolvers,
|
|
12319
|
-
logger: this.logger
|
|
12893
|
+
logger: this.logger,
|
|
12894
|
+
router: this.backendFactory.getRouter()
|
|
12320
12895
|
});
|
|
12321
12896
|
if (!bundle) return null;
|
|
12322
12897
|
this.graphStore = bundle.graphStore;
|
|
@@ -12367,11 +12942,13 @@ ${messages}`);
|
|
|
12367
12942
|
simulationResults,
|
|
12368
12943
|
personaRecommendations
|
|
12369
12944
|
} = pipelineResult ?? {};
|
|
12945
|
+
const selfAssignee = await this.orchestratorIdPromise;
|
|
12370
12946
|
const tickEvent = {
|
|
12371
12947
|
type: "tick",
|
|
12372
12948
|
candidates,
|
|
12373
12949
|
runningStates: runningStatesResult.value,
|
|
12374
12950
|
nowMs,
|
|
12951
|
+
selfAssignee,
|
|
12375
12952
|
...concernSignals !== void 0 && { concernSignals },
|
|
12376
12953
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
12377
12954
|
...complexityScores !== void 0 && { complexityScores },
|
|
@@ -12795,14 +13372,24 @@ ${messages}`);
|
|
|
12795
13372
|
issue,
|
|
12796
13373
|
attempt: attempt || 1
|
|
12797
13374
|
});
|
|
12798
|
-
const useCase =
|
|
13375
|
+
const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
|
|
13376
|
+
const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
|
|
13377
|
+
const routerOpts = invocationOverride ? { invocationOverride } : void 0;
|
|
13378
|
+
if (invocationOverride) {
|
|
13379
|
+
this.logger.info(
|
|
13380
|
+
`Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
|
|
13381
|
+
{ issueId: issue.id }
|
|
13382
|
+
);
|
|
13383
|
+
}
|
|
12799
13384
|
let routedBackendName;
|
|
12800
13385
|
if (this.overrideBackend !== null) {
|
|
12801
13386
|
routedBackendName = this.overrideBackend.name;
|
|
12802
13387
|
} else if (this.backendFactory !== null) {
|
|
12803
|
-
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
13388
|
+
routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
|
|
12804
13389
|
} else {
|
|
12805
|
-
|
|
13390
|
+
const routingDefault = this.config.agent.routing?.default;
|
|
13391
|
+
const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
|
|
13392
|
+
routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
|
|
12806
13393
|
}
|
|
12807
13394
|
const session = {
|
|
12808
13395
|
sessionId: `pending-${Date.now()}`,
|
|
@@ -12841,7 +13428,7 @@ ${messages}`);
|
|
|
12841
13428
|
if (this.overrideBackend !== null) {
|
|
12842
13429
|
agentBackend = this.overrideBackend;
|
|
12843
13430
|
} else if (this.backendFactory !== null) {
|
|
12844
|
-
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
13431
|
+
agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
|
|
12845
13432
|
} else {
|
|
12846
13433
|
throw new Error(
|
|
12847
13434
|
`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.`
|
|
@@ -13131,6 +13718,8 @@ ${messages}`);
|
|
|
13131
13718
|
unsub();
|
|
13132
13719
|
}
|
|
13133
13720
|
this.localModelStatusUnsubscribes = [];
|
|
13721
|
+
this.routingDecisionBus?.clearListeners();
|
|
13722
|
+
this.routingDecisionBus = null;
|
|
13134
13723
|
for (const resolver of this.localResolvers.values()) {
|
|
13135
13724
|
resolver.stop();
|
|
13136
13725
|
}
|
|
@@ -13204,6 +13793,42 @@ ${messages}`);
|
|
|
13204
13793
|
tickActivity: this.tickActivity
|
|
13205
13794
|
};
|
|
13206
13795
|
}
|
|
13796
|
+
/**
|
|
13797
|
+
* Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
|
|
13798
|
+
* Phase 7 (dashboard WS broadcast). Returns null when the legacy
|
|
13799
|
+
* single-backend config bypassed agent.backends synthesis.
|
|
13800
|
+
*/
|
|
13801
|
+
getRoutingDecisionBus() {
|
|
13802
|
+
return this.routingDecisionBus;
|
|
13803
|
+
}
|
|
13804
|
+
/**
|
|
13805
|
+
* Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
|
|
13806
|
+
* dispatch path uses the factory-owned router directly; observability
|
|
13807
|
+
* routes (config / decisions) reach it through this accessor. Returns
|
|
13808
|
+
* null when the legacy single-backend config bypassed agent.backends
|
|
13809
|
+
* synthesis (no backendFactory built).
|
|
13810
|
+
*/
|
|
13811
|
+
getBackendRouter() {
|
|
13812
|
+
return this.backendFactory?.getRouter() ?? null;
|
|
13813
|
+
}
|
|
13814
|
+
/**
|
|
13815
|
+
* Spec B Phase 5: snapshot of the active RoutingConfig for the config
|
|
13816
|
+
* route and the trace route's bus-less router construction. Returns
|
|
13817
|
+
* null when the operator's harness.config.json carries no
|
|
13818
|
+
* `agent.routing` block.
|
|
13819
|
+
*/
|
|
13820
|
+
getRoutingConfig() {
|
|
13821
|
+
return this.config.agent.routing ?? null;
|
|
13822
|
+
}
|
|
13823
|
+
/**
|
|
13824
|
+
* Spec B Phase 5: snapshot of `agent.backends` for the config route
|
|
13825
|
+
* (existence annotations) and the trace route (bus-less router
|
|
13826
|
+
* construction). Returns null when no synthesized backends map exists
|
|
13827
|
+
* (legacy single-backend configs).
|
|
13828
|
+
*/
|
|
13829
|
+
getBackends() {
|
|
13830
|
+
return this.config.agent.backends ?? null;
|
|
13831
|
+
}
|
|
13207
13832
|
/** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
|
|
13208
13833
|
getMaintenanceStatus() {
|
|
13209
13834
|
return this.maintenanceScheduler?.getStatus() ?? null;
|
|
@@ -13582,8 +14207,8 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
13582
14207
|
}
|
|
13583
14208
|
|
|
13584
14209
|
// src/sessions/search-index.ts
|
|
13585
|
-
import * as
|
|
13586
|
-
import * as
|
|
14210
|
+
import * as fs18 from "fs";
|
|
14211
|
+
import * as path22 from "path";
|
|
13587
14212
|
import Database2 from "better-sqlite3";
|
|
13588
14213
|
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
13589
14214
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
@@ -13628,7 +14253,7 @@ function normalizeFts5Query(query) {
|
|
|
13628
14253
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
13629
14254
|
}
|
|
13630
14255
|
function searchIndexPath(projectPath) {
|
|
13631
|
-
return
|
|
14256
|
+
return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
13632
14257
|
}
|
|
13633
14258
|
var FILE_KIND_TO_FILENAME = {
|
|
13634
14259
|
summary: "summary.md",
|
|
@@ -13643,7 +14268,7 @@ var SqliteSearchIndex = class {
|
|
|
13643
14268
|
removeSessionStmt;
|
|
13644
14269
|
totalStmt;
|
|
13645
14270
|
constructor(dbPath) {
|
|
13646
|
-
|
|
14271
|
+
fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
|
|
13647
14272
|
this.db = new Database2(dbPath);
|
|
13648
14273
|
this.db.pragma("journal_mode = WAL");
|
|
13649
14274
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -13748,14 +14373,14 @@ function indexSessionDirectory(idx, args) {
|
|
|
13748
14373
|
let docsWritten = 0;
|
|
13749
14374
|
for (const kind of kinds) {
|
|
13750
14375
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
13751
|
-
const filePath =
|
|
13752
|
-
if (!
|
|
13753
|
-
let body =
|
|
14376
|
+
const filePath = path22.join(args.sessionDir, fileName);
|
|
14377
|
+
if (!fs18.existsSync(filePath)) continue;
|
|
14378
|
+
let body = fs18.readFileSync(filePath, "utf8");
|
|
13754
14379
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
13755
14380
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
13756
14381
|
}
|
|
13757
|
-
const stat =
|
|
13758
|
-
const relPath =
|
|
14382
|
+
const stat = fs18.statSync(filePath);
|
|
14383
|
+
const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
13759
14384
|
idx.upsertSessionDoc({
|
|
13760
14385
|
sessionId: args.sessionId,
|
|
13761
14386
|
archived: args.archived,
|
|
@@ -13770,17 +14395,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
13770
14395
|
}
|
|
13771
14396
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
13772
14397
|
const start = Date.now();
|
|
13773
|
-
const archiveBase =
|
|
14398
|
+
const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
|
|
13774
14399
|
const idx = openSearchIndex(projectPath);
|
|
13775
14400
|
try {
|
|
13776
14401
|
idx.resetArchived();
|
|
13777
14402
|
let sessionsIndexed = 0;
|
|
13778
14403
|
let docsWritten = 0;
|
|
13779
|
-
if (
|
|
13780
|
-
const entries =
|
|
14404
|
+
if (fs18.existsSync(archiveBase)) {
|
|
14405
|
+
const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
|
|
13781
14406
|
for (const entry of entries) {
|
|
13782
14407
|
if (!entry.isDirectory()) continue;
|
|
13783
|
-
const sessionDir =
|
|
14408
|
+
const sessionDir = path22.join(archiveBase, entry.name);
|
|
13784
14409
|
const result = indexSessionDirectory(idx, {
|
|
13785
14410
|
sessionId: entry.name,
|
|
13786
14411
|
sessionDir,
|
|
@@ -13800,8 +14425,8 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
13800
14425
|
}
|
|
13801
14426
|
|
|
13802
14427
|
// src/sessions/summarize.ts
|
|
13803
|
-
import * as
|
|
13804
|
-
import * as
|
|
14428
|
+
import * as fs19 from "fs";
|
|
14429
|
+
import * as path23 from "path";
|
|
13805
14430
|
import {
|
|
13806
14431
|
SessionSummarySchema
|
|
13807
14432
|
} from "@harness-engineering/types";
|
|
@@ -13831,10 +14456,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
13831
14456
|
function readInputCorpus(archiveDir) {
|
|
13832
14457
|
const parts = [];
|
|
13833
14458
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
13834
|
-
const p =
|
|
13835
|
-
if (!
|
|
14459
|
+
const p = path23.join(archiveDir, filename);
|
|
14460
|
+
if (!fs19.existsSync(p)) continue;
|
|
13836
14461
|
try {
|
|
13837
|
-
const content =
|
|
14462
|
+
const content = fs19.readFileSync(p, "utf8");
|
|
13838
14463
|
if (content.trim().length === 0) continue;
|
|
13839
14464
|
parts.push(`## FILE: ${kind}
|
|
13840
14465
|
|
|
@@ -13885,7 +14510,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
13885
14510
|
return lines.join("\n");
|
|
13886
14511
|
}
|
|
13887
14512
|
function writeStubMarkdown(archiveDir, reason) {
|
|
13888
|
-
const filePath =
|
|
14513
|
+
const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
|
|
13889
14514
|
const body = `---
|
|
13890
14515
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13891
14516
|
schemaVersion: 1
|
|
@@ -13896,12 +14521,12 @@ status: failed
|
|
|
13896
14521
|
|
|
13897
14522
|
- reason: ${reason}
|
|
13898
14523
|
`;
|
|
13899
|
-
|
|
14524
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
13900
14525
|
return filePath;
|
|
13901
14526
|
}
|
|
13902
14527
|
async function summarizeArchivedSession(ctx) {
|
|
13903
14528
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
13904
|
-
if (!
|
|
14529
|
+
if (!fs19.existsSync(ctx.archiveDir)) {
|
|
13905
14530
|
return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
13906
14531
|
}
|
|
13907
14532
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
@@ -13962,9 +14587,9 @@ async function summarizeArchivedSession(ctx) {
|
|
|
13962
14587
|
outputTokens: response.tokenUsage.outputTokens,
|
|
13963
14588
|
schemaVersion: 1
|
|
13964
14589
|
};
|
|
13965
|
-
const filePath =
|
|
14590
|
+
const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
13966
14591
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
13967
|
-
|
|
14592
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
13968
14593
|
return Ok24({ summary: parsed.data, meta, filePath });
|
|
13969
14594
|
}
|
|
13970
14595
|
function isSummaryEnabled(config) {
|
|
@@ -14077,7 +14702,10 @@ export {
|
|
|
14077
14702
|
computeRateLimitDelay,
|
|
14078
14703
|
createBackend,
|
|
14079
14704
|
createEmptyState,
|
|
14705
|
+
crossFieldRoutingIssues,
|
|
14080
14706
|
detectScopeTier,
|
|
14707
|
+
discoverSkillCatalog,
|
|
14708
|
+
discoverSkillCatalogNames,
|
|
14081
14709
|
emitProposalApproved,
|
|
14082
14710
|
emitProposalCreated,
|
|
14083
14711
|
emitProposalRejected,
|
|
@@ -14103,6 +14731,7 @@ export {
|
|
|
14103
14731
|
resolveEscalationConfig,
|
|
14104
14732
|
resolveOrchestratorId,
|
|
14105
14733
|
routeIssue,
|
|
14734
|
+
routingWarnings,
|
|
14106
14735
|
runGate,
|
|
14107
14736
|
savePublishedIndex,
|
|
14108
14737
|
searchIndexPath,
|