@harness-engineering/orchestrator 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +575 -49
- package/dist/index.d.ts +575 -49
- package/dist/index.js +1014 -362
- package/dist/index.mjs +1014 -371
- package/package.json +7 -7
package/dist/index.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) {
|
|
@@ -3431,6 +3544,14 @@ var DEFAULT_PROBE_INTERVAL_MS = 3e4;
|
|
|
3431
3544
|
var MIN_PROBE_INTERVAL_MS = 1e3;
|
|
3432
3545
|
var DEFAULT_API_KEY = "lm-studio";
|
|
3433
3546
|
var DEFAULT_FETCH_TIMEOUT_MS = 5e3;
|
|
3547
|
+
function normalizeLocalModel(input) {
|
|
3548
|
+
if (input === void 0) return [];
|
|
3549
|
+
if (typeof input === "string") return [input];
|
|
3550
|
+
if (input.length === 0) {
|
|
3551
|
+
throw new Error("localModel array must be non-empty when provided");
|
|
3552
|
+
}
|
|
3553
|
+
return [...input];
|
|
3554
|
+
}
|
|
3434
3555
|
var noopLogger = {
|
|
3435
3556
|
info: () => void 0,
|
|
3436
3557
|
warn: () => void 0
|
|
@@ -3635,11 +3756,11 @@ function detectLegacyFields(agent) {
|
|
|
3635
3756
|
}
|
|
3636
3757
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3637
3758
|
const warnings = [];
|
|
3638
|
-
for (const
|
|
3639
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3640
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3759
|
+
for (const path24 of presentLegacy) {
|
|
3760
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
|
|
3761
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
|
|
3641
3762
|
warnings.push(
|
|
3642
|
-
`Ignoring legacy field '${
|
|
3763
|
+
`Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3643
3764
|
);
|
|
3644
3765
|
}
|
|
3645
3766
|
return warnings;
|
|
@@ -3667,7 +3788,7 @@ function migrateAgentConfig(agent) {
|
|
|
3667
3788
|
}
|
|
3668
3789
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3669
3790
|
const warnings = presentLegacy.map(
|
|
3670
|
-
(
|
|
3791
|
+
(path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3671
3792
|
);
|
|
3672
3793
|
return {
|
|
3673
3794
|
config: { ...agent, backends, routing },
|
|
@@ -3730,61 +3851,160 @@ function synthesizeLocal(agent) {
|
|
|
3730
3851
|
}
|
|
3731
3852
|
|
|
3732
3853
|
// src/agent/backend-router.ts
|
|
3854
|
+
function toArray(value) {
|
|
3855
|
+
return Array.isArray(value) ? value : [value];
|
|
3856
|
+
}
|
|
3733
3857
|
var BackendRouter = class {
|
|
3734
3858
|
backends;
|
|
3735
3859
|
routing;
|
|
3860
|
+
decisionBus;
|
|
3736
3861
|
constructor(opts) {
|
|
3737
3862
|
this.backends = opts.backends;
|
|
3738
3863
|
this.routing = opts.routing;
|
|
3864
|
+
this.decisionBus = opts.decisionBus;
|
|
3739
3865
|
this.validateReferences();
|
|
3740
3866
|
}
|
|
3741
3867
|
/**
|
|
3742
|
-
*
|
|
3868
|
+
* Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
|
|
3743
3869
|
*
|
|
3744
|
-
*
|
|
3745
|
-
*
|
|
3746
|
-
*
|
|
3747
|
-
|
|
3870
|
+
* @param useCase the routing query
|
|
3871
|
+
* @param opts.invocationOverride if set and the named backend exists,
|
|
3872
|
+
* beats all other sources (D7 — the `--backend <name>` escape hatch)
|
|
3873
|
+
*/
|
|
3874
|
+
resolve(useCase, opts) {
|
|
3875
|
+
const startedAt = performance.now();
|
|
3876
|
+
const path24 = [];
|
|
3877
|
+
const tryChain = (source, value) => {
|
|
3878
|
+
if (value === void 0) return void 0;
|
|
3879
|
+
for (const name of toArray(value)) {
|
|
3880
|
+
const step = { source, candidate: name, outcome: "considered" };
|
|
3881
|
+
path24.push(step);
|
|
3882
|
+
if (this.backends[name]) {
|
|
3883
|
+
step.outcome = "chosen";
|
|
3884
|
+
return name;
|
|
3885
|
+
}
|
|
3886
|
+
step.outcome = "unknown-backend";
|
|
3887
|
+
}
|
|
3888
|
+
return void 0;
|
|
3889
|
+
};
|
|
3890
|
+
const decide = (backendName) => {
|
|
3891
|
+
const def = this.backends[backendName];
|
|
3892
|
+
if (!def) {
|
|
3893
|
+
throw new Error(
|
|
3894
|
+
`BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
|
|
3895
|
+
);
|
|
3896
|
+
}
|
|
3897
|
+
return {
|
|
3898
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3899
|
+
useCase,
|
|
3900
|
+
resolutionPath: path24,
|
|
3901
|
+
backendName,
|
|
3902
|
+
backendType: def.type,
|
|
3903
|
+
durationMs: performance.now() - startedAt
|
|
3904
|
+
};
|
|
3905
|
+
};
|
|
3906
|
+
const emitAndReturn = (decision) => {
|
|
3907
|
+
this.decisionBus?.emit(decision);
|
|
3908
|
+
return decision;
|
|
3909
|
+
};
|
|
3910
|
+
const fromInvocation = tryChain(
|
|
3911
|
+
"invocation",
|
|
3912
|
+
opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
|
|
3913
|
+
);
|
|
3914
|
+
if (fromInvocation) return emitAndReturn(decide(fromInvocation));
|
|
3915
|
+
if (useCase.kind === "skill") {
|
|
3916
|
+
const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
|
|
3917
|
+
if (fromSkill) return emitAndReturn(decide(fromSkill));
|
|
3918
|
+
}
|
|
3919
|
+
const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
|
|
3920
|
+
if (mode !== void 0) {
|
|
3921
|
+
const fromMode = tryChain("mode", this.routing.modes?.[mode]);
|
|
3922
|
+
if (fromMode) return emitAndReturn(decide(fromMode));
|
|
3923
|
+
}
|
|
3924
|
+
const fromExisting = this.resolveExistingUseCase(useCase);
|
|
3925
|
+
if (fromExisting !== void 0) {
|
|
3926
|
+
const chained = tryChain("tier", fromExisting);
|
|
3927
|
+
if (chained) return emitAndReturn(decide(chained));
|
|
3928
|
+
}
|
|
3929
|
+
const fromDefault = tryChain("default", this.routing.default);
|
|
3930
|
+
if (fromDefault) return emitAndReturn(decide(fromDefault));
|
|
3931
|
+
const knownList = Object.keys(this.backends).join(", ") || "(none)";
|
|
3932
|
+
throw new Error(
|
|
3933
|
+
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
|
|
3934
|
+
);
|
|
3935
|
+
}
|
|
3936
|
+
/**
|
|
3937
|
+
* Returns the {@link BackendDef} reference for the resolved name.
|
|
3938
|
+
* Identity-equal to the entry in `backends` (no copy) so callers
|
|
3939
|
+
* relying on reference equality (SC21) continue to work.
|
|
3748
3940
|
*/
|
|
3749
|
-
|
|
3941
|
+
resolveDefinition(useCase, opts) {
|
|
3942
|
+
const decision = this.resolve(useCase, opts);
|
|
3943
|
+
const def = this.backends[decision.backendName];
|
|
3944
|
+
if (!def) {
|
|
3945
|
+
throw new Error(
|
|
3946
|
+
`BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3947
|
+
);
|
|
3948
|
+
}
|
|
3949
|
+
return def;
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
|
|
3953
|
+
* for callers that need both. Replaces the previous pattern of
|
|
3954
|
+
* `resolveDefinition(useCase) + resolve(useCase)` which produced two
|
|
3955
|
+
* RoutingDecision emissions per dispatch — doubling routing-decision
|
|
3956
|
+
* log volume now that Phase 4 emits.
|
|
3957
|
+
*
|
|
3958
|
+
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
3959
|
+
* reference equality (SC21) continue to work.
|
|
3960
|
+
*/
|
|
3961
|
+
resolveDecisionAndDef(useCase, opts) {
|
|
3962
|
+
const decision = this.resolve(useCase, opts);
|
|
3963
|
+
const def = this.backends[decision.backendName];
|
|
3964
|
+
if (!def) {
|
|
3965
|
+
throw new Error(
|
|
3966
|
+
`BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3967
|
+
);
|
|
3968
|
+
}
|
|
3969
|
+
return { decision, def };
|
|
3970
|
+
}
|
|
3971
|
+
/**
|
|
3972
|
+
* The pre-Spec-B resolution helper: returns the configured
|
|
3973
|
+
* {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
|
|
3974
|
+
* use cases (or `undefined` for skill/mode use cases, which are owned
|
|
3975
|
+
* by the per-skill / per-mode steps in {@link resolve}). Returning
|
|
3976
|
+
* `undefined` lets the caller fall through to `routing.default`.
|
|
3977
|
+
*/
|
|
3978
|
+
resolveExistingUseCase(useCase) {
|
|
3750
3979
|
switch (useCase.kind) {
|
|
3751
3980
|
case "tier": {
|
|
3752
|
-
const
|
|
3753
|
-
return
|
|
3981
|
+
const tierMap = this.routing;
|
|
3982
|
+
return tierMap[useCase.tier];
|
|
3754
3983
|
}
|
|
3755
3984
|
case "intelligence": {
|
|
3756
3985
|
const intel = this.routing.intelligence;
|
|
3757
|
-
return intel?.[useCase.layer]
|
|
3986
|
+
return intel?.[useCase.layer];
|
|
3758
3987
|
}
|
|
3759
3988
|
case "isolation": {
|
|
3760
3989
|
const iso = this.routing.isolation;
|
|
3761
|
-
return iso?.[useCase.tier]
|
|
3990
|
+
return iso?.[useCase.tier];
|
|
3762
3991
|
}
|
|
3763
3992
|
case "maintenance":
|
|
3764
3993
|
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
|
-
);
|
|
3994
|
+
return void 0;
|
|
3995
|
+
case "skill":
|
|
3996
|
+
case "mode":
|
|
3997
|
+
return void 0;
|
|
3780
3998
|
}
|
|
3781
|
-
return def;
|
|
3782
3999
|
}
|
|
3783
4000
|
validateReferences() {
|
|
3784
4001
|
const known = new Set(Object.keys(this.backends));
|
|
3785
4002
|
const missing = [];
|
|
3786
|
-
const check = (
|
|
3787
|
-
if (
|
|
4003
|
+
const check = (label, value) => {
|
|
4004
|
+
if (value === void 0) return;
|
|
4005
|
+
for (const name of toArray(value)) {
|
|
4006
|
+
if (!known.has(name)) missing.push({ path: label, name });
|
|
4007
|
+
}
|
|
3788
4008
|
};
|
|
3789
4009
|
check("default", this.routing.default);
|
|
3790
4010
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3796,8 +4016,14 @@ var BackendRouter = class {
|
|
|
3796
4016
|
check("isolation.none", this.routing.isolation?.none);
|
|
3797
4017
|
check("isolation.container", this.routing.isolation?.container);
|
|
3798
4018
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
4019
|
+
for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
|
|
4020
|
+
check(`skills.${skill}`, value);
|
|
4021
|
+
}
|
|
4022
|
+
for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
|
|
4023
|
+
check(`modes.${mode}`, value);
|
|
4024
|
+
}
|
|
3799
4025
|
if (missing.length > 0) {
|
|
3800
|
-
const detail = missing.map(({ path:
|
|
4026
|
+
const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
|
|
3801
4027
|
const known_ = [...known].join(", ") || "(none)";
|
|
3802
4028
|
throw new Error(
|
|
3803
4029
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3814,11 +4040,11 @@ import {
|
|
|
3814
4040
|
Ok as Ok10,
|
|
3815
4041
|
Err as Err7
|
|
3816
4042
|
} from "@harness-engineering/types";
|
|
3817
|
-
function resolveExitCode(code, command,
|
|
4043
|
+
function resolveExitCode(code, command, resolve8) {
|
|
3818
4044
|
if (code === 0) {
|
|
3819
|
-
|
|
4045
|
+
resolve8(Ok10(void 0));
|
|
3820
4046
|
} else {
|
|
3821
|
-
|
|
4047
|
+
resolve8(
|
|
3822
4048
|
Err7({
|
|
3823
4049
|
category: "agent_not_found",
|
|
3824
4050
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3826,8 +4052,8 @@ function resolveExitCode(code, command, resolve7) {
|
|
|
3826
4052
|
);
|
|
3827
4053
|
}
|
|
3828
4054
|
}
|
|
3829
|
-
function resolveSpawnError(command,
|
|
3830
|
-
|
|
4055
|
+
function resolveSpawnError(command, resolve8) {
|
|
4056
|
+
resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3831
4057
|
}
|
|
3832
4058
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3833
4059
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4140,10 +4366,10 @@ var ClaudeBackend = class {
|
|
|
4140
4366
|
errRl.close();
|
|
4141
4367
|
}
|
|
4142
4368
|
if (exitCode === null) {
|
|
4143
|
-
await new Promise((
|
|
4369
|
+
await new Promise((resolve8) => {
|
|
4144
4370
|
child.on("exit", (code) => {
|
|
4145
4371
|
exitCode = code;
|
|
4146
|
-
|
|
4372
|
+
resolve8(null);
|
|
4147
4373
|
});
|
|
4148
4374
|
});
|
|
4149
4375
|
}
|
|
@@ -4165,10 +4391,10 @@ var ClaudeBackend = class {
|
|
|
4165
4391
|
return Ok10(void 0);
|
|
4166
4392
|
}
|
|
4167
4393
|
async healthCheck() {
|
|
4168
|
-
return new Promise((
|
|
4394
|
+
return new Promise((resolve8) => {
|
|
4169
4395
|
const child = spawn2(this.command, ["--version"]);
|
|
4170
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4171
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4396
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
|
|
4397
|
+
child.on("error", () => resolveSpawnError(this.command, resolve8));
|
|
4172
4398
|
});
|
|
4173
4399
|
}
|
|
4174
4400
|
};
|
|
@@ -5100,14 +5326,14 @@ var SshBackend = class {
|
|
|
5100
5326
|
async healthCheck() {
|
|
5101
5327
|
const args = [...this.buildSshArgs()];
|
|
5102
5328
|
args[args.length - 1] = "true";
|
|
5103
|
-
return new Promise((
|
|
5329
|
+
return new Promise((resolve8) => {
|
|
5104
5330
|
let child;
|
|
5105
5331
|
try {
|
|
5106
5332
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5107
5333
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5108
5334
|
});
|
|
5109
5335
|
} catch (err) {
|
|
5110
|
-
|
|
5336
|
+
resolve8(
|
|
5111
5337
|
Err13({
|
|
5112
5338
|
category: "agent_not_found",
|
|
5113
5339
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5128,9 +5354,9 @@ var SshBackend = class {
|
|
|
5128
5354
|
child.on("close", (code) => {
|
|
5129
5355
|
clearTimeout(timer);
|
|
5130
5356
|
if (code === 0) {
|
|
5131
|
-
|
|
5357
|
+
resolve8(Ok16(void 0));
|
|
5132
5358
|
} else {
|
|
5133
|
-
|
|
5359
|
+
resolve8(
|
|
5134
5360
|
Err13({
|
|
5135
5361
|
category: "agent_not_found",
|
|
5136
5362
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5140,7 +5366,7 @@ var SshBackend = class {
|
|
|
5140
5366
|
});
|
|
5141
5367
|
child.on("error", (err) => {
|
|
5142
5368
|
clearTimeout(timer);
|
|
5143
|
-
|
|
5369
|
+
resolve8(Err13({ category: "agent_not_found", message: err.message }));
|
|
5144
5370
|
});
|
|
5145
5371
|
});
|
|
5146
5372
|
}
|
|
@@ -5188,13 +5414,13 @@ async function* readLines(stream) {
|
|
|
5188
5414
|
if (buffer.length > 0) yield buffer;
|
|
5189
5415
|
}
|
|
5190
5416
|
function waitForExit(child) {
|
|
5191
|
-
return new Promise((
|
|
5417
|
+
return new Promise((resolve8) => {
|
|
5192
5418
|
if (child.exitCode !== null) {
|
|
5193
|
-
|
|
5419
|
+
resolve8(child.exitCode);
|
|
5194
5420
|
return;
|
|
5195
5421
|
}
|
|
5196
|
-
child.once("close", (code) =>
|
|
5197
|
-
child.once("error", () =>
|
|
5422
|
+
child.once("close", (code) => resolve8(code));
|
|
5423
|
+
child.once("error", () => resolve8(null));
|
|
5198
5424
|
});
|
|
5199
5425
|
}
|
|
5200
5426
|
|
|
@@ -5384,14 +5610,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5384
5610
|
return out;
|
|
5385
5611
|
}
|
|
5386
5612
|
runOneShot(binary, args) {
|
|
5387
|
-
return new Promise((
|
|
5613
|
+
return new Promise((resolve8) => {
|
|
5388
5614
|
let child;
|
|
5389
5615
|
try {
|
|
5390
5616
|
child = this.spawnImpl(binary, args, {
|
|
5391
5617
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5392
5618
|
});
|
|
5393
5619
|
} catch (err) {
|
|
5394
|
-
|
|
5620
|
+
resolve8(
|
|
5395
5621
|
Err14({
|
|
5396
5622
|
category: "agent_not_found",
|
|
5397
5623
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5416,9 +5642,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5416
5642
|
child.on("close", (code) => {
|
|
5417
5643
|
clearTimeout(timer);
|
|
5418
5644
|
if (code === 0) {
|
|
5419
|
-
|
|
5645
|
+
resolve8(Ok17(stdout));
|
|
5420
5646
|
} else {
|
|
5421
|
-
|
|
5647
|
+
resolve8(
|
|
5422
5648
|
Err14({
|
|
5423
5649
|
category: "response_error",
|
|
5424
5650
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5428,7 +5654,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5428
5654
|
});
|
|
5429
5655
|
child.on("error", (err) => {
|
|
5430
5656
|
clearTimeout(timer);
|
|
5431
|
-
|
|
5657
|
+
resolve8(Err14({ category: "agent_not_found", message: err.message }));
|
|
5432
5658
|
});
|
|
5433
5659
|
});
|
|
5434
5660
|
}
|
|
@@ -5488,13 +5714,13 @@ async function* readLines2(stream) {
|
|
|
5488
5714
|
if (buffer.length > 0) yield buffer;
|
|
5489
5715
|
}
|
|
5490
5716
|
function waitForExit2(child) {
|
|
5491
|
-
return new Promise((
|
|
5717
|
+
return new Promise((resolve8) => {
|
|
5492
5718
|
if (child.exitCode !== null) {
|
|
5493
|
-
|
|
5719
|
+
resolve8(child.exitCode);
|
|
5494
5720
|
return;
|
|
5495
5721
|
}
|
|
5496
|
-
child.once("close", (code) =>
|
|
5497
|
-
child.once("error", () =>
|
|
5722
|
+
child.once("close", (code) => resolve8(code));
|
|
5723
|
+
child.once("error", () => resolve8(null));
|
|
5498
5724
|
});
|
|
5499
5725
|
}
|
|
5500
5726
|
|
|
@@ -5694,13 +5920,13 @@ var ContainerBackend = class {
|
|
|
5694
5920
|
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
5695
5921
|
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5696
5922
|
function dockerExec(args) {
|
|
5697
|
-
return new Promise((
|
|
5923
|
+
return new Promise((resolve8, reject) => {
|
|
5698
5924
|
execFile3("docker", args, (error, stdout) => {
|
|
5699
5925
|
if (error) {
|
|
5700
5926
|
reject(error);
|
|
5701
5927
|
return;
|
|
5702
5928
|
}
|
|
5703
|
-
|
|
5929
|
+
resolve8(stdout.trim());
|
|
5704
5930
|
});
|
|
5705
5931
|
});
|
|
5706
5932
|
}
|
|
@@ -5759,11 +5985,11 @@ var DockerRuntime = class {
|
|
|
5759
5985
|
} finally {
|
|
5760
5986
|
rl.close();
|
|
5761
5987
|
}
|
|
5762
|
-
const exitCode = await new Promise((
|
|
5988
|
+
const exitCode = await new Promise((resolve8) => {
|
|
5763
5989
|
if (child.exitCode !== null) {
|
|
5764
|
-
|
|
5990
|
+
resolve8(child.exitCode);
|
|
5765
5991
|
} else {
|
|
5766
|
-
child.on("exit", (code) =>
|
|
5992
|
+
child.on("exit", (code) => resolve8(code ?? 1));
|
|
5767
5993
|
}
|
|
5768
5994
|
});
|
|
5769
5995
|
return exitCode;
|
|
@@ -5822,13 +6048,13 @@ var EnvSecretBackend = class {
|
|
|
5822
6048
|
import { execFile as execFile4 } from "child_process";
|
|
5823
6049
|
import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
|
|
5824
6050
|
function opExec(args) {
|
|
5825
|
-
return new Promise((
|
|
6051
|
+
return new Promise((resolve8, reject) => {
|
|
5826
6052
|
execFile4("op", args, (error, stdout) => {
|
|
5827
6053
|
if (error) {
|
|
5828
6054
|
reject(error);
|
|
5829
6055
|
return;
|
|
5830
6056
|
}
|
|
5831
|
-
|
|
6057
|
+
resolve8(stdout.trim());
|
|
5832
6058
|
});
|
|
5833
6059
|
});
|
|
5834
6060
|
}
|
|
@@ -5871,13 +6097,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5871
6097
|
import { execFile as execFile5 } from "child_process";
|
|
5872
6098
|
import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
|
|
5873
6099
|
function vaultExec(args, env) {
|
|
5874
|
-
return new Promise((
|
|
6100
|
+
return new Promise((resolve8, reject) => {
|
|
5875
6101
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5876
6102
|
if (error) {
|
|
5877
6103
|
reject(error);
|
|
5878
6104
|
return;
|
|
5879
6105
|
}
|
|
5880
|
-
|
|
6106
|
+
resolve8(stdout.trim());
|
|
5881
6107
|
});
|
|
5882
6108
|
});
|
|
5883
6109
|
}
|
|
@@ -5952,7 +6178,11 @@ var OrchestratorBackendFactory = class {
|
|
|
5952
6178
|
opts;
|
|
5953
6179
|
constructor(opts) {
|
|
5954
6180
|
this.opts = opts;
|
|
5955
|
-
this.router = new BackendRouter({
|
|
6181
|
+
this.router = new BackendRouter({
|
|
6182
|
+
backends: opts.backends,
|
|
6183
|
+
routing: opts.routing,
|
|
6184
|
+
...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
|
|
6185
|
+
});
|
|
5956
6186
|
}
|
|
5957
6187
|
/**
|
|
5958
6188
|
* Resolve `useCase` to a backend name, materialize a fresh
|
|
@@ -5971,12 +6201,21 @@ var OrchestratorBackendFactory = class {
|
|
|
5971
6201
|
* is `undefined` for pure-modern configs. Threading the routed name
|
|
5972
6202
|
* through dispatch eliminates that gap.
|
|
5973
6203
|
*/
|
|
5974
|
-
resolveName(useCase) {
|
|
5975
|
-
return this.router.resolve(useCase);
|
|
6204
|
+
resolveName(useCase, opts) {
|
|
6205
|
+
return this.router.resolve(useCase, opts).backendName;
|
|
5976
6206
|
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
6207
|
+
/**
|
|
6208
|
+
* Spec B Phase 1: expose the underlying router for callers that need
|
|
6209
|
+
* it directly (e.g., {@link buildIntelligencePipeline} for the
|
|
6210
|
+
* I1 SEL/PESL comparison fix). Read-only access; consumers must not
|
|
6211
|
+
* mutate router state.
|
|
6212
|
+
*/
|
|
6213
|
+
getRouter() {
|
|
6214
|
+
return this.router;
|
|
6215
|
+
}
|
|
6216
|
+
forUseCase(useCase, opts) {
|
|
6217
|
+
const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
|
|
6218
|
+
const name = decision.backendName;
|
|
5980
6219
|
let backend;
|
|
5981
6220
|
const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
|
|
5982
6221
|
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
@@ -6149,15 +6388,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
6149
6388
|
|
|
6150
6389
|
// src/agent/intelligence-factory.ts
|
|
6151
6390
|
function buildIntelligencePipeline(deps) {
|
|
6152
|
-
const { config } = deps;
|
|
6391
|
+
const { config, router } = deps;
|
|
6153
6392
|
const intel = config.intelligence;
|
|
6154
6393
|
if (!intel?.enabled) return null;
|
|
6155
6394
|
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
6156
6395
|
if (!selProvider) return null;
|
|
6157
|
-
const
|
|
6158
|
-
const
|
|
6159
|
-
const
|
|
6160
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6396
|
+
const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
|
|
6397
|
+
const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
|
|
6398
|
+
const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6161
6399
|
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
6162
6400
|
const graphStore = new GraphStore();
|
|
6163
6401
|
const pipeline = new IntelligencePipeline(selProvider, graphStore, {
|
|
@@ -6174,7 +6412,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6174
6412
|
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
6175
6413
|
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
6176
6414
|
}
|
|
6177
|
-
const routed = resolveRoutedBackend(layer,
|
|
6415
|
+
const routed = resolveRoutedBackend(layer, deps);
|
|
6178
6416
|
if (!routed) return null;
|
|
6179
6417
|
const { name, def } = routed;
|
|
6180
6418
|
const resolver = localResolvers.get(name);
|
|
@@ -6199,20 +6437,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6199
6437
|
logger
|
|
6200
6438
|
});
|
|
6201
6439
|
}
|
|
6202
|
-
function resolveRoutedBackend(layer,
|
|
6203
|
-
const
|
|
6440
|
+
function resolveRoutedBackend(layer, deps) {
|
|
6441
|
+
const { config, router, logger } = deps;
|
|
6204
6442
|
const backends = config.agent.backends;
|
|
6205
|
-
if (!
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6443
|
+
if (!backends || !router) return null;
|
|
6444
|
+
try {
|
|
6445
|
+
const decision = router.resolve({ kind: "intelligence", layer });
|
|
6446
|
+
const def = backends[decision.backendName];
|
|
6447
|
+
if (!def) {
|
|
6448
|
+
logger.warn(
|
|
6449
|
+
`Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
|
|
6450
|
+
);
|
|
6451
|
+
return null;
|
|
6452
|
+
}
|
|
6453
|
+
return { name: decision.backendName, def };
|
|
6454
|
+
} catch (err) {
|
|
6210
6455
|
logger.warn(
|
|
6211
|
-
`Intelligence pipeline:
|
|
6456
|
+
`Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
|
|
6212
6457
|
);
|
|
6213
6458
|
return null;
|
|
6214
6459
|
}
|
|
6215
|
-
return { name, def };
|
|
6216
6460
|
}
|
|
6217
6461
|
function buildExplicitProvider(provider, selModel, config) {
|
|
6218
6462
|
if (provider.kind === "anthropic") {
|
|
@@ -6247,9 +6491,104 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6247
6491
|
});
|
|
6248
6492
|
}
|
|
6249
6493
|
|
|
6494
|
+
// src/routing/decision-bus.ts
|
|
6495
|
+
var RoutingDecisionBus = class {
|
|
6496
|
+
ringBuffer = [];
|
|
6497
|
+
listeners = /* @__PURE__ */ new Set();
|
|
6498
|
+
capacity;
|
|
6499
|
+
logger;
|
|
6500
|
+
constructor(opts) {
|
|
6501
|
+
this.capacity = opts?.capacity ?? 500;
|
|
6502
|
+
this.logger = opts?.logger;
|
|
6503
|
+
}
|
|
6504
|
+
emit(decision) {
|
|
6505
|
+
this.ringBuffer.push(decision);
|
|
6506
|
+
if (this.ringBuffer.length > this.capacity) {
|
|
6507
|
+
this.ringBuffer.shift();
|
|
6508
|
+
}
|
|
6509
|
+
if (this.logger) {
|
|
6510
|
+
this.logger.info("routing-decision", {
|
|
6511
|
+
useCase: decision.useCase,
|
|
6512
|
+
backendName: decision.backendName,
|
|
6513
|
+
resolutionPathLength: decision.resolutionPath.length,
|
|
6514
|
+
durationMs: decision.durationMs
|
|
6515
|
+
});
|
|
6516
|
+
}
|
|
6517
|
+
for (const listener of this.listeners) {
|
|
6518
|
+
try {
|
|
6519
|
+
listener(decision);
|
|
6520
|
+
} catch (err) {
|
|
6521
|
+
if (this.logger) {
|
|
6522
|
+
this.logger.warn("RoutingDecisionBus subscriber threw", {
|
|
6523
|
+
error: String(err)
|
|
6524
|
+
});
|
|
6525
|
+
}
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
recent(filter) {
|
|
6530
|
+
let out = this.ringBuffer.slice();
|
|
6531
|
+
if (filter?.skillName !== void 0) {
|
|
6532
|
+
out = out.filter(
|
|
6533
|
+
(d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
|
|
6534
|
+
);
|
|
6535
|
+
}
|
|
6536
|
+
if (filter?.mode !== void 0) {
|
|
6537
|
+
const m = filter.mode;
|
|
6538
|
+
out = out.filter(
|
|
6539
|
+
(d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
|
|
6540
|
+
);
|
|
6541
|
+
}
|
|
6542
|
+
if (filter?.backendName !== void 0) {
|
|
6543
|
+
out = out.filter((d) => d.backendName === filter.backendName);
|
|
6544
|
+
}
|
|
6545
|
+
if (filter?.limit !== void 0) {
|
|
6546
|
+
out = out.slice(-filter.limit).reverse();
|
|
6547
|
+
} else {
|
|
6548
|
+
out = out.reverse();
|
|
6549
|
+
}
|
|
6550
|
+
return out;
|
|
6551
|
+
}
|
|
6552
|
+
subscribe(listener) {
|
|
6553
|
+
this.listeners.add(listener);
|
|
6554
|
+
return () => {
|
|
6555
|
+
this.listeners.delete(listener);
|
|
6556
|
+
};
|
|
6557
|
+
}
|
|
6558
|
+
/**
|
|
6559
|
+
* Spec B Phase 5 (review-S2 fix): release all subscriber references so
|
|
6560
|
+
* teardown can complete without anchoring closures. Called from
|
|
6561
|
+
* `Orchestrator.stop()` before nulling the bus reference. The bus
|
|
6562
|
+
* remains usable after clear — `subscribe()` works as normal.
|
|
6563
|
+
*/
|
|
6564
|
+
clearListeners() {
|
|
6565
|
+
this.listeners.clear();
|
|
6566
|
+
}
|
|
6567
|
+
};
|
|
6568
|
+
|
|
6569
|
+
// src/agent/triage-skill-mapping.ts
|
|
6570
|
+
function resolveSkillForTriage(triageSkill, catalog) {
|
|
6571
|
+
const expected = `harness-${triageSkill}`;
|
|
6572
|
+
const match = catalog.find((e) => e.name === expected);
|
|
6573
|
+
if (!match) return void 0;
|
|
6574
|
+
return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
|
|
6575
|
+
}
|
|
6576
|
+
|
|
6577
|
+
// src/agent/use-case-builder.ts
|
|
6578
|
+
function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
6579
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
6580
|
+
const decision = triageIssue(issue, {});
|
|
6581
|
+
const resolved = resolveSkillForTriage(decision.skill, catalog);
|
|
6582
|
+
if (resolved) {
|
|
6583
|
+
return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
|
|
6584
|
+
}
|
|
6585
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
6586
|
+
return { kind: "tier", tier };
|
|
6587
|
+
}
|
|
6588
|
+
|
|
6250
6589
|
// src/server/http.ts
|
|
6251
6590
|
import * as http from "http";
|
|
6252
|
-
import * as
|
|
6591
|
+
import * as path17 from "path";
|
|
6253
6592
|
import { assertPortUsable } from "@harness-engineering/core";
|
|
6254
6593
|
|
|
6255
6594
|
// src/server/websocket.ts
|
|
@@ -6312,7 +6651,7 @@ import { z as z3 } from "zod";
|
|
|
6312
6651
|
// src/server/utils.ts
|
|
6313
6652
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6314
6653
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6315
|
-
return new Promise((
|
|
6654
|
+
return new Promise((resolve8, reject) => {
|
|
6316
6655
|
let body = "";
|
|
6317
6656
|
let bytes = 0;
|
|
6318
6657
|
req.on("data", (chunk) => {
|
|
@@ -6324,7 +6663,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6324
6663
|
}
|
|
6325
6664
|
body += String(chunk);
|
|
6326
6665
|
});
|
|
6327
|
-
req.on("end", () =>
|
|
6666
|
+
req.on("end", () => resolve8(body));
|
|
6328
6667
|
req.on("error", reject);
|
|
6329
6668
|
});
|
|
6330
6669
|
}
|
|
@@ -6445,8 +6784,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
|
6445
6784
|
|
|
6446
6785
|
// src/server/routes/plans.ts
|
|
6447
6786
|
import { z as z5 } from "zod";
|
|
6448
|
-
import * as
|
|
6449
|
-
import * as
|
|
6787
|
+
import * as fs10 from "fs/promises";
|
|
6788
|
+
import * as path11 from "path";
|
|
6450
6789
|
var PlanWriteSchema = z5.object({
|
|
6451
6790
|
filename: z5.string().min(1),
|
|
6452
6791
|
content: z5.string().min(1)
|
|
@@ -6466,7 +6805,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6466
6805
|
return;
|
|
6467
6806
|
}
|
|
6468
6807
|
const parsed = result.data;
|
|
6469
|
-
const basename3 =
|
|
6808
|
+
const basename3 = path11.basename(parsed.filename);
|
|
6470
6809
|
if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
|
|
6471
6810
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6472
6811
|
res.end(
|
|
@@ -6474,9 +6813,9 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6474
6813
|
);
|
|
6475
6814
|
return;
|
|
6476
6815
|
}
|
|
6477
|
-
await
|
|
6478
|
-
const filePath =
|
|
6479
|
-
await
|
|
6816
|
+
await fs10.mkdir(plansDir, { recursive: true });
|
|
6817
|
+
const filePath = path11.join(plansDir, basename3);
|
|
6818
|
+
await fs10.writeFile(filePath, parsed.content, "utf-8");
|
|
6480
6819
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
6481
6820
|
res.end(JSON.stringify({ ok: true, filename: basename3 }));
|
|
6482
6821
|
} catch {
|
|
@@ -6851,8 +7190,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6851
7190
|
}
|
|
6852
7191
|
|
|
6853
7192
|
// src/server/routes/roadmap-actions.ts
|
|
6854
|
-
import * as
|
|
6855
|
-
import * as
|
|
7193
|
+
import * as fs11 from "fs/promises";
|
|
7194
|
+
import * as path12 from "path";
|
|
6856
7195
|
import {
|
|
6857
7196
|
parseRoadmap as parseRoadmap2,
|
|
6858
7197
|
serializeRoadmap as serializeRoadmap2,
|
|
@@ -6888,7 +7227,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6888
7227
|
sendJSON2(res, 503, { error: "Roadmap path not configured" });
|
|
6889
7228
|
return;
|
|
6890
7229
|
}
|
|
6891
|
-
const projectRoot =
|
|
7230
|
+
const projectRoot = path12.dirname(path12.dirname(roadmapPath));
|
|
6892
7231
|
const mode = loadProjectRoadmapMode(projectRoot);
|
|
6893
7232
|
if (mode === "file-less") {
|
|
6894
7233
|
const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
|
|
@@ -6941,7 +7280,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6941
7280
|
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
6942
7281
|
return;
|
|
6943
7282
|
}
|
|
6944
|
-
const content = await
|
|
7283
|
+
const content = await fs11.readFile(roadmapPath, "utf-8");
|
|
6945
7284
|
const roadmapResult = parseRoadmap2(content);
|
|
6946
7285
|
if (!roadmapResult.ok) {
|
|
6947
7286
|
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
@@ -6972,8 +7311,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6972
7311
|
roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
|
|
6973
7312
|
const tmpPath = roadmapPath + ".tmp";
|
|
6974
7313
|
const serialized = serializeRoadmap2(roadmap);
|
|
6975
|
-
await
|
|
6976
|
-
await
|
|
7314
|
+
await fs11.writeFile(tmpPath, serialized, "utf-8");
|
|
7315
|
+
await fs11.rename(tmpPath, roadmapPath);
|
|
6977
7316
|
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
6978
7317
|
} catch (err) {
|
|
6979
7318
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
@@ -7473,7 +7812,7 @@ import {
|
|
|
7473
7812
|
} from "@harness-engineering/types";
|
|
7474
7813
|
|
|
7475
7814
|
// src/proposals/gate.ts
|
|
7476
|
-
import { parse as
|
|
7815
|
+
import { parse as parseYaml2 } from "yaml";
|
|
7477
7816
|
import {
|
|
7478
7817
|
getProposal,
|
|
7479
7818
|
updateProposal,
|
|
@@ -7490,7 +7829,7 @@ function checkSkillYaml(yaml) {
|
|
|
7490
7829
|
const findings = [];
|
|
7491
7830
|
let doc;
|
|
7492
7831
|
try {
|
|
7493
|
-
doc =
|
|
7832
|
+
doc = parseYaml2(yaml);
|
|
7494
7833
|
} catch (err) {
|
|
7495
7834
|
findings.push({
|
|
7496
7835
|
severity: "error",
|
|
@@ -7613,9 +7952,9 @@ async function runGate(projectPath, proposalId) {
|
|
|
7613
7952
|
}
|
|
7614
7953
|
|
|
7615
7954
|
// src/proposals/promote.ts
|
|
7616
|
-
import * as
|
|
7617
|
-
import * as
|
|
7618
|
-
import { parse as
|
|
7955
|
+
import * as fs12 from "fs";
|
|
7956
|
+
import * as path13 from "path";
|
|
7957
|
+
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
7619
7958
|
import {
|
|
7620
7959
|
getProposal as getProposal2,
|
|
7621
7960
|
updateProposal as updateProposal2,
|
|
@@ -7635,11 +7974,11 @@ var PromotionError = class extends Error {
|
|
|
7635
7974
|
};
|
|
7636
7975
|
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
7637
7976
|
function skillDir(projectPath, name) {
|
|
7638
|
-
return
|
|
7977
|
+
return path13.join(projectPath, "agents", "skills", "claude-code", name);
|
|
7639
7978
|
}
|
|
7640
7979
|
function readIfExists(p) {
|
|
7641
7980
|
try {
|
|
7642
|
-
return
|
|
7981
|
+
return fs12.readFileSync(p, "utf-8");
|
|
7643
7982
|
} catch {
|
|
7644
7983
|
return null;
|
|
7645
7984
|
}
|
|
@@ -7647,7 +7986,7 @@ function readIfExists(p) {
|
|
|
7647
7986
|
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
7648
7987
|
let doc;
|
|
7649
7988
|
try {
|
|
7650
|
-
doc =
|
|
7989
|
+
doc = parseYaml3(yamlText);
|
|
7651
7990
|
} catch (err) {
|
|
7652
7991
|
throw new PromotionError(
|
|
7653
7992
|
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -7685,15 +8024,15 @@ function assertGateReady(proposal) {
|
|
|
7685
8024
|
}
|
|
7686
8025
|
async function promoteNewSkill(projectPath, proposal) {
|
|
7687
8026
|
const target = skillDir(projectPath, proposal.content.name);
|
|
7688
|
-
if (
|
|
8027
|
+
if (fs12.existsSync(target)) {
|
|
7689
8028
|
throw new PromotionError(
|
|
7690
8029
|
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
7691
8030
|
);
|
|
7692
8031
|
}
|
|
7693
|
-
|
|
8032
|
+
fs12.mkdirSync(target, { recursive: true });
|
|
7694
8033
|
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
7695
|
-
|
|
7696
|
-
|
|
8034
|
+
fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
|
|
8035
|
+
fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
7697
8036
|
return { skillPath: target };
|
|
7698
8037
|
}
|
|
7699
8038
|
async function promoteRefinement(projectPath, proposal) {
|
|
@@ -7701,12 +8040,12 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
7701
8040
|
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
7702
8041
|
}
|
|
7703
8042
|
const target = skillDir(projectPath, proposal.targetSkill);
|
|
7704
|
-
if (!
|
|
8043
|
+
if (!fs12.existsSync(target)) {
|
|
7705
8044
|
throw new PromotionError(
|
|
7706
8045
|
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
7707
8046
|
);
|
|
7708
8047
|
}
|
|
7709
|
-
const yamlPath =
|
|
8048
|
+
const yamlPath = path13.join(target, "skill.yaml");
|
|
7710
8049
|
const before = readIfExists(yamlPath) ?? "";
|
|
7711
8050
|
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
7712
8051
|
if (after === before) {
|
|
@@ -7714,7 +8053,7 @@ async function promoteRefinement(projectPath, proposal) {
|
|
|
7714
8053
|
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
7715
8054
|
);
|
|
7716
8055
|
}
|
|
7717
|
-
|
|
8056
|
+
fs12.writeFileSync(yamlPath, after);
|
|
7718
8057
|
return { skillPath: target };
|
|
7719
8058
|
}
|
|
7720
8059
|
async function promote(projectPath, proposalId, decidedBy) {
|
|
@@ -8001,35 +8340,185 @@ function handleV1ProposalsRoute(req, res, deps) {
|
|
|
8001
8340
|
return false;
|
|
8002
8341
|
}
|
|
8003
8342
|
|
|
8004
|
-
// src/server/routes/
|
|
8005
|
-
import * as fs12 from "fs/promises";
|
|
8006
|
-
import * as path12 from "path";
|
|
8343
|
+
// src/server/routes/v1/routing.ts
|
|
8007
8344
|
import { z as z14 } from "zod";
|
|
8008
|
-
var
|
|
8009
|
-
|
|
8345
|
+
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
8346
|
+
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
8347
|
+
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
8348
|
+
function sendJSON9(res, status, body) {
|
|
8349
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8350
|
+
res.end(JSON.stringify(body));
|
|
8351
|
+
}
|
|
8352
|
+
function unavailable(res) {
|
|
8353
|
+
sendJSON9(res, 503, { error: "BackendRouter not available" });
|
|
8354
|
+
return true;
|
|
8355
|
+
}
|
|
8356
|
+
function resolveChain(value, backends) {
|
|
8357
|
+
return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
|
|
8358
|
+
}
|
|
8359
|
+
function buildResolvedChains(routing, backends) {
|
|
8360
|
+
const out = {};
|
|
8361
|
+
out["default"] = resolveChain(routing.default, backends);
|
|
8362
|
+
for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
|
|
8363
|
+
const v = routing[tier];
|
|
8364
|
+
if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
|
|
8365
|
+
}
|
|
8366
|
+
if (routing.intelligence) {
|
|
8367
|
+
for (const [layer, v] of Object.entries(routing.intelligence)) {
|
|
8368
|
+
if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
|
|
8369
|
+
}
|
|
8370
|
+
}
|
|
8371
|
+
if (routing.isolation) {
|
|
8372
|
+
for (const [tier, v] of Object.entries(routing.isolation)) {
|
|
8373
|
+
if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
|
|
8374
|
+
}
|
|
8375
|
+
}
|
|
8376
|
+
if (routing.skills) {
|
|
8377
|
+
for (const [name, v] of Object.entries(routing.skills)) {
|
|
8378
|
+
if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
|
|
8379
|
+
}
|
|
8380
|
+
}
|
|
8381
|
+
if (routing.modes) {
|
|
8382
|
+
for (const [mode, v] of Object.entries(routing.modes)) {
|
|
8383
|
+
if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
|
|
8384
|
+
}
|
|
8385
|
+
}
|
|
8386
|
+
return out;
|
|
8387
|
+
}
|
|
8388
|
+
function handleConfig(res, deps) {
|
|
8389
|
+
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
8390
|
+
sendJSON9(res, 200, {
|
|
8391
|
+
routing: deps.routing,
|
|
8392
|
+
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
8393
|
+
backends: Object.keys(deps.backends)
|
|
8394
|
+
});
|
|
8395
|
+
return true;
|
|
8396
|
+
}
|
|
8397
|
+
function parseDecisionsQuery(url) {
|
|
8398
|
+
const qIdx = url.indexOf("?");
|
|
8399
|
+
if (qIdx === -1) return {};
|
|
8400
|
+
const p = new URLSearchParams(url.slice(qIdx + 1));
|
|
8401
|
+
const filter = {};
|
|
8402
|
+
const skill = p.get("skill");
|
|
8403
|
+
const mode = p.get("mode");
|
|
8404
|
+
const backend = p.get("backend");
|
|
8405
|
+
const limit = p.get("limit");
|
|
8406
|
+
if (skill) filter.skillName = skill;
|
|
8407
|
+
if (mode) filter.mode = mode;
|
|
8408
|
+
if (backend) filter.backendName = backend;
|
|
8409
|
+
if (limit) {
|
|
8410
|
+
const n = Number(limit);
|
|
8411
|
+
if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
|
|
8412
|
+
}
|
|
8413
|
+
return filter;
|
|
8414
|
+
}
|
|
8415
|
+
function handleDecisions(req, res, deps) {
|
|
8416
|
+
if (!deps.bus) return unavailable(res);
|
|
8417
|
+
const filter = parseDecisionsQuery(req.url ?? "");
|
|
8418
|
+
sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
|
|
8419
|
+
return true;
|
|
8420
|
+
}
|
|
8421
|
+
var UseCaseSchema = z14.discriminatedUnion("kind", [
|
|
8422
|
+
z14.object({
|
|
8423
|
+
kind: z14.literal("tier"),
|
|
8424
|
+
tier: z14.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
|
|
8425
|
+
}),
|
|
8426
|
+
z14.object({ kind: z14.literal("intelligence"), layer: z14.enum(["sel", "pesl"]) }),
|
|
8427
|
+
z14.object({ kind: z14.literal("isolation"), tier: z14.string() }),
|
|
8428
|
+
z14.object({ kind: z14.literal("maintenance") }),
|
|
8429
|
+
z14.object({ kind: z14.literal("chat") }),
|
|
8430
|
+
z14.object({
|
|
8431
|
+
kind: z14.literal("skill"),
|
|
8432
|
+
skillName: z14.string().min(1),
|
|
8433
|
+
cognitiveMode: z14.string().optional()
|
|
8434
|
+
}),
|
|
8435
|
+
z14.object({ kind: z14.literal("mode"), cognitiveMode: z14.string().min(1) })
|
|
8436
|
+
]);
|
|
8437
|
+
var TraceBodySchema = z14.object({
|
|
8438
|
+
useCase: UseCaseSchema,
|
|
8439
|
+
invocationOverride: z14.string().min(1).optional()
|
|
8440
|
+
});
|
|
8441
|
+
async function handleTrace(req, res, deps) {
|
|
8442
|
+
if (!deps.routing || !deps.backends) {
|
|
8443
|
+
unavailable(res);
|
|
8444
|
+
return true;
|
|
8445
|
+
}
|
|
8446
|
+
let raw;
|
|
8447
|
+
try {
|
|
8448
|
+
raw = await readBody(req);
|
|
8449
|
+
} catch {
|
|
8450
|
+
sendJSON9(res, 400, { error: "body read failed" });
|
|
8451
|
+
return true;
|
|
8452
|
+
}
|
|
8453
|
+
let parsed;
|
|
8454
|
+
try {
|
|
8455
|
+
parsed = JSON.parse(raw);
|
|
8456
|
+
} catch {
|
|
8457
|
+
sendJSON9(res, 400, { error: "invalid JSON body" });
|
|
8458
|
+
return true;
|
|
8459
|
+
}
|
|
8460
|
+
const r = TraceBodySchema.safeParse(parsed);
|
|
8461
|
+
if (!r.success) {
|
|
8462
|
+
sendJSON9(res, 400, { error: r.error.message });
|
|
8463
|
+
return true;
|
|
8464
|
+
}
|
|
8465
|
+
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
8466
|
+
try {
|
|
8467
|
+
const dryRunRouter = new BackendRouter({
|
|
8468
|
+
backends: deps.backends,
|
|
8469
|
+
routing: deps.routing
|
|
8470
|
+
});
|
|
8471
|
+
const { decision, def } = dryRunRouter.resolveDecisionAndDef(
|
|
8472
|
+
r.data.useCase,
|
|
8473
|
+
opts
|
|
8474
|
+
);
|
|
8475
|
+
sendJSON9(res, 200, { decision, def: { type: def.type } });
|
|
8476
|
+
} catch (err) {
|
|
8477
|
+
sendJSON9(res, 500, { error: String(err) });
|
|
8478
|
+
}
|
|
8479
|
+
return true;
|
|
8480
|
+
}
|
|
8481
|
+
function handleV1RoutingRoute(req, res, deps) {
|
|
8482
|
+
const url = req.url ?? "";
|
|
8483
|
+
const method = req.method ?? "GET";
|
|
8484
|
+
if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
|
|
8485
|
+
if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
|
|
8486
|
+
if (method === "POST" && TRACE_RE.test(url)) {
|
|
8487
|
+
void handleTrace(req, res, deps);
|
|
8488
|
+
return true;
|
|
8489
|
+
}
|
|
8490
|
+
return false;
|
|
8491
|
+
}
|
|
8492
|
+
|
|
8493
|
+
// src/server/routes/sessions.ts
|
|
8494
|
+
import * as fs13 from "fs/promises";
|
|
8495
|
+
import * as path14 from "path";
|
|
8496
|
+
import { z as z15 } from "zod";
|
|
8497
|
+
var SessionCreateSchema = z15.object({
|
|
8498
|
+
sessionId: z15.string().min(1)
|
|
8010
8499
|
}).passthrough();
|
|
8011
8500
|
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
8501
|
function isSafeId(id) {
|
|
8013
|
-
return UUID_RE2.test(id) ||
|
|
8502
|
+
return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
|
|
8014
8503
|
}
|
|
8015
8504
|
function jsonResponse(res, status, data) {
|
|
8016
8505
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8017
8506
|
res.end(JSON.stringify(data));
|
|
8018
8507
|
}
|
|
8019
8508
|
function extractSessionId(url) {
|
|
8020
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
8509
|
+
const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
|
|
8021
8510
|
const id = segments.pop();
|
|
8022
8511
|
return id && id !== "sessions" ? id : null;
|
|
8023
8512
|
}
|
|
8024
8513
|
async function handleList2(res, sessionsDir) {
|
|
8025
8514
|
try {
|
|
8026
|
-
const entries = await
|
|
8515
|
+
const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
|
|
8027
8516
|
const sessions = [];
|
|
8028
8517
|
for (const entry of entries) {
|
|
8029
8518
|
if (!entry.isDirectory()) continue;
|
|
8030
8519
|
try {
|
|
8031
|
-
const content = await
|
|
8032
|
-
|
|
8520
|
+
const content = await fs13.readFile(
|
|
8521
|
+
path14.join(sessionsDir, entry.name, "session.json"),
|
|
8033
8522
|
"utf-8"
|
|
8034
8523
|
);
|
|
8035
8524
|
sessions.push(JSON.parse(content));
|
|
@@ -8054,7 +8543,7 @@ async function handleGet2(res, id, sessionsDir) {
|
|
|
8054
8543
|
return;
|
|
8055
8544
|
}
|
|
8056
8545
|
try {
|
|
8057
|
-
const content = await
|
|
8546
|
+
const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
|
|
8058
8547
|
jsonResponse(res, 200, JSON.parse(content));
|
|
8059
8548
|
} catch (err) {
|
|
8060
8549
|
if (err.code === "ENOENT") {
|
|
@@ -8077,9 +8566,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
8077
8566
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
8078
8567
|
return;
|
|
8079
8568
|
}
|
|
8080
|
-
const sessionDir =
|
|
8081
|
-
await
|
|
8082
|
-
await
|
|
8569
|
+
const sessionDir = path14.join(sessionsDir, session.sessionId);
|
|
8570
|
+
await fs13.mkdir(sessionDir, { recursive: true });
|
|
8571
|
+
await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
8083
8572
|
jsonResponse(res, 200, { ok: true });
|
|
8084
8573
|
} catch {
|
|
8085
8574
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -8093,10 +8582,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
8093
8582
|
return;
|
|
8094
8583
|
}
|
|
8095
8584
|
const body = await readBody(req);
|
|
8096
|
-
const updates =
|
|
8097
|
-
const sessionFilePath =
|
|
8098
|
-
const current = JSON.parse(await
|
|
8099
|
-
await
|
|
8585
|
+
const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
|
|
8586
|
+
const sessionFilePath = path14.join(sessionsDir, id, "session.json");
|
|
8587
|
+
const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
|
|
8588
|
+
await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
8100
8589
|
jsonResponse(res, 200, { ok: true });
|
|
8101
8590
|
} catch {
|
|
8102
8591
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -8109,7 +8598,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
8109
8598
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
8110
8599
|
return;
|
|
8111
8600
|
}
|
|
8112
|
-
await
|
|
8601
|
+
await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
|
|
8113
8602
|
jsonResponse(res, 200, { ok: true });
|
|
8114
8603
|
} catch {
|
|
8115
8604
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -8213,20 +8702,20 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
8213
8702
|
}
|
|
8214
8703
|
|
|
8215
8704
|
// src/server/routes/auth.ts
|
|
8216
|
-
import { z as
|
|
8705
|
+
import { z as z16 } from "zod";
|
|
8217
8706
|
import {
|
|
8218
8707
|
TokenScopeSchema,
|
|
8219
8708
|
BridgeKindSchema,
|
|
8220
8709
|
AuthTokenPublicSchema
|
|
8221
8710
|
} from "@harness-engineering/types";
|
|
8222
|
-
var CreateBodySchema =
|
|
8223
|
-
name:
|
|
8224
|
-
scopes:
|
|
8711
|
+
var CreateBodySchema = z16.object({
|
|
8712
|
+
name: z16.string().min(1).max(100),
|
|
8713
|
+
scopes: z16.array(TokenScopeSchema).min(1),
|
|
8225
8714
|
bridgeKind: BridgeKindSchema.optional(),
|
|
8226
|
-
tenantId:
|
|
8227
|
-
expiresAt:
|
|
8715
|
+
tenantId: z16.string().optional(),
|
|
8716
|
+
expiresAt: z16.string().datetime().optional()
|
|
8228
8717
|
});
|
|
8229
|
-
function
|
|
8718
|
+
function sendJSON10(res, status, body) {
|
|
8230
8719
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8231
8720
|
res.end(JSON.stringify(body));
|
|
8232
8721
|
}
|
|
@@ -8236,19 +8725,19 @@ async function handlePost(req, res, store) {
|
|
|
8236
8725
|
raw = await readBody(req);
|
|
8237
8726
|
} catch (err) {
|
|
8238
8727
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
8239
|
-
|
|
8728
|
+
sendJSON10(res, 413, { error: msg });
|
|
8240
8729
|
return;
|
|
8241
8730
|
}
|
|
8242
8731
|
let json;
|
|
8243
8732
|
try {
|
|
8244
8733
|
json = JSON.parse(raw);
|
|
8245
8734
|
} catch {
|
|
8246
|
-
|
|
8735
|
+
sendJSON10(res, 400, { error: "Invalid JSON body" });
|
|
8247
8736
|
return;
|
|
8248
8737
|
}
|
|
8249
8738
|
const parsed = CreateBodySchema.safeParse(json);
|
|
8250
8739
|
if (!parsed.success) {
|
|
8251
|
-
|
|
8740
|
+
sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
8252
8741
|
return;
|
|
8253
8742
|
}
|
|
8254
8743
|
try {
|
|
@@ -8261,37 +8750,37 @@ async function handlePost(req, res, store) {
|
|
|
8261
8750
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
8262
8751
|
const result = await store.create(input);
|
|
8263
8752
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
8264
|
-
|
|
8753
|
+
sendJSON10(res, 200, {
|
|
8265
8754
|
...publicRecord,
|
|
8266
8755
|
token: result.token
|
|
8267
8756
|
});
|
|
8268
8757
|
} catch (err) {
|
|
8269
8758
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
8270
8759
|
if (msg.includes("already exists")) {
|
|
8271
|
-
|
|
8760
|
+
sendJSON10(res, 409, { error: msg });
|
|
8272
8761
|
return;
|
|
8273
8762
|
}
|
|
8274
|
-
|
|
8763
|
+
sendJSON10(res, 500, { error: "Internal error creating token" });
|
|
8275
8764
|
}
|
|
8276
8765
|
}
|
|
8277
8766
|
async function handleList3(res, store) {
|
|
8278
8767
|
try {
|
|
8279
8768
|
const list = await store.list();
|
|
8280
|
-
|
|
8769
|
+
sendJSON10(res, 200, list);
|
|
8281
8770
|
} catch {
|
|
8282
|
-
|
|
8771
|
+
sendJSON10(res, 500, { error: "Internal error listing tokens" });
|
|
8283
8772
|
}
|
|
8284
8773
|
}
|
|
8285
8774
|
async function handleDelete2(res, store, id) {
|
|
8286
8775
|
try {
|
|
8287
8776
|
const ok = await store.revoke(id);
|
|
8288
8777
|
if (!ok) {
|
|
8289
|
-
|
|
8778
|
+
sendJSON10(res, 404, { error: "Token not found" });
|
|
8290
8779
|
return;
|
|
8291
8780
|
}
|
|
8292
|
-
|
|
8781
|
+
sendJSON10(res, 200, { deleted: true });
|
|
8293
8782
|
} catch {
|
|
8294
|
-
|
|
8783
|
+
sendJSON10(res, 500, { error: "Internal error revoking token" });
|
|
8295
8784
|
}
|
|
8296
8785
|
}
|
|
8297
8786
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -8316,12 +8805,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
8316
8805
|
return true;
|
|
8317
8806
|
}
|
|
8318
8807
|
}
|
|
8319
|
-
|
|
8808
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
8320
8809
|
return true;
|
|
8321
8810
|
}
|
|
8322
8811
|
|
|
8323
8812
|
// src/server/routes/local-model.ts
|
|
8324
|
-
function
|
|
8813
|
+
function sendJSON11(res, status, body) {
|
|
8325
8814
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8326
8815
|
res.end(JSON.stringify(body));
|
|
8327
8816
|
}
|
|
@@ -8329,36 +8818,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
8329
8818
|
const { method, url } = req;
|
|
8330
8819
|
if (url !== "/api/v1/local-model/status") return false;
|
|
8331
8820
|
if (method !== "GET") {
|
|
8332
|
-
|
|
8821
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
8333
8822
|
return true;
|
|
8334
8823
|
}
|
|
8335
8824
|
if (!getStatus) {
|
|
8336
|
-
|
|
8825
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
8337
8826
|
return true;
|
|
8338
8827
|
}
|
|
8339
8828
|
const status = getStatus();
|
|
8340
8829
|
if (!status) {
|
|
8341
|
-
|
|
8830
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
8342
8831
|
return true;
|
|
8343
8832
|
}
|
|
8344
|
-
|
|
8833
|
+
sendJSON11(res, 200, status);
|
|
8345
8834
|
return true;
|
|
8346
8835
|
}
|
|
8347
8836
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
8348
8837
|
const { method, url } = req;
|
|
8349
8838
|
if (url !== "/api/v1/local-models/status") return false;
|
|
8350
8839
|
if (method !== "GET") {
|
|
8351
|
-
|
|
8840
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
8352
8841
|
return true;
|
|
8353
8842
|
}
|
|
8354
8843
|
const statuses = getStatuses ? getStatuses() : [];
|
|
8355
|
-
|
|
8844
|
+
sendJSON11(res, 200, statuses);
|
|
8356
8845
|
return true;
|
|
8357
8846
|
}
|
|
8358
8847
|
|
|
8359
8848
|
// src/server/static.ts
|
|
8360
|
-
import * as
|
|
8361
|
-
import * as
|
|
8849
|
+
import * as fs14 from "fs";
|
|
8850
|
+
import * as path15 from "path";
|
|
8362
8851
|
var MIME_TYPES = {
|
|
8363
8852
|
".html": "text/html; charset=utf-8",
|
|
8364
8853
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -8378,29 +8867,29 @@ var MIME_TYPES = {
|
|
|
8378
8867
|
function handleStaticFile(req, res, dashboardDir) {
|
|
8379
8868
|
const { method, url } = req;
|
|
8380
8869
|
if (method !== "GET") return false;
|
|
8381
|
-
const apiPrefix =
|
|
8382
|
-
const wsPath =
|
|
8870
|
+
const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
|
|
8871
|
+
const wsPath = path15.posix.join(path15.posix.sep, "ws");
|
|
8383
8872
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
8384
8873
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
8385
|
-
const requestedPath =
|
|
8386
|
-
const resolved =
|
|
8387
|
-
if (!resolved.startsWith(
|
|
8388
|
-
return serveFile(
|
|
8874
|
+
const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8875
|
+
const resolved = path15.resolve(requestedPath);
|
|
8876
|
+
if (!resolved.startsWith(path15.resolve(dashboardDir))) {
|
|
8877
|
+
return serveFile(path15.join(dashboardDir, "index.html"), res);
|
|
8389
8878
|
}
|
|
8390
|
-
if (
|
|
8879
|
+
if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
|
|
8391
8880
|
return serveFile(resolved, res);
|
|
8392
8881
|
}
|
|
8393
|
-
const indexPath =
|
|
8394
|
-
if (
|
|
8882
|
+
const indexPath = path15.join(dashboardDir, "index.html");
|
|
8883
|
+
if (fs14.existsSync(indexPath)) {
|
|
8395
8884
|
return serveFile(indexPath, res);
|
|
8396
8885
|
}
|
|
8397
8886
|
return false;
|
|
8398
8887
|
}
|
|
8399
8888
|
function serveFile(filePath, res) {
|
|
8400
|
-
const ext =
|
|
8889
|
+
const ext = path15.extname(filePath).toLowerCase();
|
|
8401
8890
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
8402
8891
|
try {
|
|
8403
|
-
const content =
|
|
8892
|
+
const content = fs14.readFileSync(filePath);
|
|
8404
8893
|
res.writeHead(200, { "Content-Type": contentType });
|
|
8405
8894
|
res.end(content);
|
|
8406
8895
|
return true;
|
|
@@ -8410,8 +8899,8 @@ function serveFile(filePath, res) {
|
|
|
8410
8899
|
}
|
|
8411
8900
|
|
|
8412
8901
|
// src/server/plan-watcher.ts
|
|
8413
|
-
import * as
|
|
8414
|
-
import * as
|
|
8902
|
+
import * as fs15 from "fs";
|
|
8903
|
+
import * as path16 from "path";
|
|
8415
8904
|
var PlanWatcher = class {
|
|
8416
8905
|
plansDir;
|
|
8417
8906
|
queue;
|
|
@@ -8425,11 +8914,11 @@ var PlanWatcher = class {
|
|
|
8425
8914
|
* Creates the directory if it does not exist.
|
|
8426
8915
|
*/
|
|
8427
8916
|
start() {
|
|
8428
|
-
|
|
8429
|
-
this.watcher =
|
|
8917
|
+
fs15.mkdirSync(this.plansDir, { recursive: true });
|
|
8918
|
+
this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
|
|
8430
8919
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
8431
|
-
const filePath =
|
|
8432
|
-
if (
|
|
8920
|
+
const filePath = path16.join(this.plansDir, filename);
|
|
8921
|
+
if (fs15.existsSync(filePath)) {
|
|
8433
8922
|
void this.handleNewPlan(filename);
|
|
8434
8923
|
}
|
|
8435
8924
|
}
|
|
@@ -8462,7 +8951,7 @@ var PlanWatcher = class {
|
|
|
8462
8951
|
// src/auth/tokens.ts
|
|
8463
8952
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
8464
8953
|
import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
|
|
8465
|
-
import { dirname as
|
|
8954
|
+
import { dirname as dirname5 } from "path";
|
|
8466
8955
|
import bcrypt from "bcryptjs";
|
|
8467
8956
|
import {
|
|
8468
8957
|
AuthTokenSchema,
|
|
@@ -8482,8 +8971,8 @@ function parseToken(raw) {
|
|
|
8482
8971
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
8483
8972
|
}
|
|
8484
8973
|
var TokenStore = class {
|
|
8485
|
-
constructor(
|
|
8486
|
-
this.path =
|
|
8974
|
+
constructor(path24) {
|
|
8975
|
+
this.path = path24;
|
|
8487
8976
|
}
|
|
8488
8977
|
path;
|
|
8489
8978
|
cache = null;
|
|
@@ -8504,7 +8993,7 @@ var TokenStore = class {
|
|
|
8504
8993
|
return this.cache;
|
|
8505
8994
|
}
|
|
8506
8995
|
async persist(records) {
|
|
8507
|
-
await mkdir7(
|
|
8996
|
+
await mkdir7(dirname5(this.path), { recursive: true });
|
|
8508
8997
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
|
|
8509
8998
|
await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
8510
8999
|
await rename2(tmp, this.path);
|
|
@@ -8587,11 +9076,11 @@ var TokenStore = class {
|
|
|
8587
9076
|
|
|
8588
9077
|
// src/auth/audit.ts
|
|
8589
9078
|
import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
8590
|
-
import { dirname as
|
|
9079
|
+
import { dirname as dirname6 } from "path";
|
|
8591
9080
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
8592
9081
|
var AuditLogger = class {
|
|
8593
|
-
constructor(
|
|
8594
|
-
this.path =
|
|
9082
|
+
constructor(path24, opts = {}) {
|
|
9083
|
+
this.path = path24;
|
|
8595
9084
|
this.opts = opts;
|
|
8596
9085
|
}
|
|
8597
9086
|
path;
|
|
@@ -8618,7 +9107,7 @@ var AuditLogger = class {
|
|
|
8618
9107
|
async writeLine(line) {
|
|
8619
9108
|
try {
|
|
8620
9109
|
if (this.opts.createDir !== false && !this.dirEnsured) {
|
|
8621
|
-
await mkdir8(
|
|
9110
|
+
await mkdir8(dirname6(this.path), { recursive: true });
|
|
8622
9111
|
this.dirEnsured = true;
|
|
8623
9112
|
}
|
|
8624
9113
|
await appendFile(this.path, line, "utf8");
|
|
@@ -8718,14 +9207,36 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8718
9207
|
pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
|
|
8719
9208
|
scope: "read-telemetry",
|
|
8720
9209
|
description: "Prompt-cache hit/miss snapshot (rolling window)."
|
|
9210
|
+
},
|
|
9211
|
+
// ── Spec B Phase 5 routing observability ──
|
|
9212
|
+
// D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
|
|
9213
|
+
// precedent (read-only observability). A dedicated `read-routing`
|
|
9214
|
+
// scope was rejected to avoid a TokenScopeSchema + ADR cascade.
|
|
9215
|
+
{
|
|
9216
|
+
method: "GET",
|
|
9217
|
+
pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
|
|
9218
|
+
scope: "read-telemetry",
|
|
9219
|
+
description: "Current routing config + resolved fallback chains + known backends."
|
|
9220
|
+
},
|
|
9221
|
+
{
|
|
9222
|
+
method: "GET",
|
|
9223
|
+
pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
|
|
9224
|
+
scope: "read-telemetry",
|
|
9225
|
+
description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
|
|
9226
|
+
},
|
|
9227
|
+
{
|
|
9228
|
+
method: "POST",
|
|
9229
|
+
pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
|
|
9230
|
+
scope: "read-telemetry",
|
|
9231
|
+
description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
|
|
8721
9232
|
}
|
|
8722
9233
|
];
|
|
8723
9234
|
function isV1Bridge(method, url) {
|
|
8724
9235
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8725
9236
|
}
|
|
8726
|
-
function requiredBridgeScope(method,
|
|
9237
|
+
function requiredBridgeScope(method, path24) {
|
|
8727
9238
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8728
|
-
if (r.method === method && r.pattern.test(
|
|
9239
|
+
if (r.method === method && r.pattern.test(path24)) return r.scope;
|
|
8729
9240
|
}
|
|
8730
9241
|
return null;
|
|
8731
9242
|
}
|
|
@@ -8735,24 +9246,24 @@ function hasScope(held, required) {
|
|
|
8735
9246
|
if (held.includes("admin")) return true;
|
|
8736
9247
|
return held.includes(required);
|
|
8737
9248
|
}
|
|
8738
|
-
function requiredScopeForRoute(method,
|
|
8739
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
9249
|
+
function requiredScopeForRoute(method, path24) {
|
|
9250
|
+
const bridgeScope = requiredBridgeScope(method, path24);
|
|
8740
9251
|
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 (
|
|
9252
|
+
if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
9253
|
+
if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
9254
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
|
|
9255
|
+
if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
|
|
9256
|
+
if (path24.startsWith("/api/interactions")) return "resolve-interaction";
|
|
9257
|
+
if (path24.startsWith("/api/plans")) return "read-status";
|
|
9258
|
+
if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
|
|
9259
|
+
if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
9260
|
+
if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
9261
|
+
if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
|
|
8751
9262
|
return "read-status";
|
|
8752
|
-
if (
|
|
8753
|
-
if (
|
|
8754
|
-
if (
|
|
8755
|
-
if (
|
|
9263
|
+
if (path24.startsWith("/api/maintenance")) return "trigger-job";
|
|
9264
|
+
if (path24.startsWith("/api/streams")) return "read-status";
|
|
9265
|
+
if (path24.startsWith("/api/sessions")) return "read-status";
|
|
9266
|
+
if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
8756
9267
|
return null;
|
|
8757
9268
|
}
|
|
8758
9269
|
|
|
@@ -8816,6 +9327,15 @@ var OrchestratorServer = class {
|
|
|
8816
9327
|
getLocalModelStatuses = null;
|
|
8817
9328
|
webhooks;
|
|
8818
9329
|
cacheMetrics;
|
|
9330
|
+
// Spec B Phase 5 — routing observability accessor closures + the WS
|
|
9331
|
+
// broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
|
|
9332
|
+
// calls it explicitly; clearListeners in Orchestrator.stop() is the
|
|
9333
|
+
// belt-and-suspenders second line).
|
|
9334
|
+
getBackendRouterFn = null;
|
|
9335
|
+
getRoutingDecisionBusFn = null;
|
|
9336
|
+
getRoutingConfigFn = null;
|
|
9337
|
+
getBackendsFn = null;
|
|
9338
|
+
routingDecisionUnsubscribe = null;
|
|
8819
9339
|
recorder = null;
|
|
8820
9340
|
planWatcher = null;
|
|
8821
9341
|
tokenStore;
|
|
@@ -8828,8 +9348,8 @@ var OrchestratorServer = class {
|
|
|
8828
9348
|
this.orchestrator = orchestrator;
|
|
8829
9349
|
this.port = port;
|
|
8830
9350
|
this.initDependencies(deps);
|
|
8831
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8832
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
9351
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
|
|
9352
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
|
|
8833
9353
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8834
9354
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8835
9355
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8842,20 +9362,24 @@ var OrchestratorServer = class {
|
|
|
8842
9362
|
}
|
|
8843
9363
|
initDependencies(deps) {
|
|
8844
9364
|
this.interactionQueue = deps?.interactionQueue;
|
|
8845
|
-
this.plansDir = deps?.plansDir ??
|
|
8846
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
9365
|
+
this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
|
|
9366
|
+
this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
|
|
8847
9367
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8848
9368
|
this.pipeline = deps?.pipeline ?? null;
|
|
8849
9369
|
this.analysisArchive = deps?.analysisArchive;
|
|
8850
9370
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8851
9371
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8852
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
9372
|
+
this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
|
|
8853
9373
|
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8854
9374
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8855
9375
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8856
9376
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
8857
9377
|
this.webhooks = deps?.webhooks;
|
|
8858
9378
|
this.cacheMetrics = deps?.cacheMetrics;
|
|
9379
|
+
this.getBackendRouterFn = deps?.getBackendRouter ?? null;
|
|
9380
|
+
this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
|
|
9381
|
+
this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
|
|
9382
|
+
this.getBackendsFn = deps?.getBackends ?? null;
|
|
8859
9383
|
}
|
|
8860
9384
|
wireEvents() {
|
|
8861
9385
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -8866,6 +9390,12 @@ var OrchestratorServer = class {
|
|
|
8866
9390
|
};
|
|
8867
9391
|
this.orchestrator.on("state_change", this.stateChangeListener);
|
|
8868
9392
|
this.orchestrator.on("agent_event", this.agentEventListener);
|
|
9393
|
+
const bus = this.getRoutingDecisionBusFn?.() ?? null;
|
|
9394
|
+
if (bus) {
|
|
9395
|
+
this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
|
|
9396
|
+
this.broadcaster.broadcast("routing:decision", decision);
|
|
9397
|
+
});
|
|
9398
|
+
}
|
|
8869
9399
|
}
|
|
8870
9400
|
/**
|
|
8871
9401
|
* Broadcast a new interaction to all WebSocket clients.
|
|
@@ -9021,6 +9551,14 @@ var OrchestratorServer = class {
|
|
|
9021
9551
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
9022
9552
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
9023
9553
|
}),
|
|
9554
|
+
// Spec B Phase 5 — routing observability. Returns 503 when the
|
|
9555
|
+
// backendFactory is null (legacy single-backend configs).
|
|
9556
|
+
(req, res) => handleV1RoutingRoute(req, res, {
|
|
9557
|
+
router: this.getBackendRouterFn?.() ?? null,
|
|
9558
|
+
bus: this.getRoutingDecisionBusFn?.() ?? null,
|
|
9559
|
+
routing: this.getRoutingConfigFn?.() ?? null,
|
|
9560
|
+
backends: this.getBackendsFn?.() ?? null
|
|
9561
|
+
}),
|
|
9024
9562
|
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9025
9563
|
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9026
9564
|
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
@@ -9117,17 +9655,21 @@ var OrchestratorServer = class {
|
|
|
9117
9655
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
9118
9656
|
this.planWatcher.start();
|
|
9119
9657
|
}
|
|
9120
|
-
return new Promise((
|
|
9658
|
+
return new Promise((resolve8) => {
|
|
9121
9659
|
const host = getBindHost();
|
|
9122
9660
|
this.httpServer.listen(this.port, host, () => {
|
|
9123
9661
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
9124
|
-
|
|
9662
|
+
resolve8();
|
|
9125
9663
|
});
|
|
9126
9664
|
});
|
|
9127
9665
|
}
|
|
9128
9666
|
stop() {
|
|
9129
9667
|
this.orchestrator.removeListener("state_change", this.stateChangeListener);
|
|
9130
9668
|
this.orchestrator.removeListener("agent_event", this.agentEventListener);
|
|
9669
|
+
if (this.routingDecisionUnsubscribe) {
|
|
9670
|
+
this.routingDecisionUnsubscribe();
|
|
9671
|
+
this.routingDecisionUnsubscribe = null;
|
|
9672
|
+
}
|
|
9131
9673
|
if (this.planWatcher) {
|
|
9132
9674
|
this.planWatcher.stop();
|
|
9133
9675
|
this.planWatcher = null;
|
|
@@ -9140,7 +9682,7 @@ var OrchestratorServer = class {
|
|
|
9140
9682
|
// src/gateway/webhooks/store.ts
|
|
9141
9683
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
9142
9684
|
import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
|
|
9143
|
-
import { dirname as
|
|
9685
|
+
import { dirname as dirname7 } from "path";
|
|
9144
9686
|
import { WebhookSubscriptionSchema } from "@harness-engineering/types";
|
|
9145
9687
|
|
|
9146
9688
|
// src/gateway/webhooks/signer.ts
|
|
@@ -9171,8 +9713,8 @@ function genSecret2() {
|
|
|
9171
9713
|
return randomBytes4(32).toString("base64url");
|
|
9172
9714
|
}
|
|
9173
9715
|
var WebhookStore = class {
|
|
9174
|
-
constructor(
|
|
9175
|
-
this.path =
|
|
9716
|
+
constructor(path24) {
|
|
9717
|
+
this.path = path24;
|
|
9176
9718
|
}
|
|
9177
9719
|
path;
|
|
9178
9720
|
cache = null;
|
|
@@ -9195,7 +9737,7 @@ var WebhookStore = class {
|
|
|
9195
9737
|
async persist(records) {
|
|
9196
9738
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
|
|
9197
9739
|
try {
|
|
9198
|
-
await mkdir9(
|
|
9740
|
+
await mkdir9(dirname7(this.path), { recursive: true });
|
|
9199
9741
|
await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
|
|
9200
9742
|
await rename3(tmp, this.path);
|
|
9201
9743
|
await chmod(this.path, 384);
|
|
@@ -10164,8 +10706,8 @@ var StructuredLogger = class {
|
|
|
10164
10706
|
};
|
|
10165
10707
|
|
|
10166
10708
|
// src/workspace/config-scanner.ts
|
|
10167
|
-
import { existsSync as
|
|
10168
|
-
import { join as
|
|
10709
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
10710
|
+
import { join as join15, relative } from "path";
|
|
10169
10711
|
import {
|
|
10170
10712
|
scanForInjection,
|
|
10171
10713
|
SecurityScanner,
|
|
@@ -10189,10 +10731,10 @@ function adjustFindingSeverity(findings) {
|
|
|
10189
10731
|
});
|
|
10190
10732
|
}
|
|
10191
10733
|
async function scanSingleFile(filePath, targetDir, scanner) {
|
|
10192
|
-
if (!
|
|
10734
|
+
if (!existsSync6(filePath)) return null;
|
|
10193
10735
|
let content;
|
|
10194
10736
|
try {
|
|
10195
|
-
content =
|
|
10737
|
+
content = readFileSync6(filePath, "utf8");
|
|
10196
10738
|
} catch {
|
|
10197
10739
|
return null;
|
|
10198
10740
|
}
|
|
@@ -10211,7 +10753,7 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
10211
10753
|
const scanner = new SecurityScanner(parseSecurityConfig({}));
|
|
10212
10754
|
const results = [];
|
|
10213
10755
|
for (const configFile of CONFIG_FILES) {
|
|
10214
|
-
const result = await scanSingleFile(
|
|
10756
|
+
const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
|
|
10215
10757
|
if (result) results.push(result);
|
|
10216
10758
|
}
|
|
10217
10759
|
return { exitCode: computeScanExitCode(results), results };
|
|
@@ -10725,19 +11267,19 @@ var SingleProcessLeaderElector = class {
|
|
|
10725
11267
|
};
|
|
10726
11268
|
|
|
10727
11269
|
// 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:
|
|
11270
|
+
import * as fs16 from "fs";
|
|
11271
|
+
import * as path18 from "path";
|
|
11272
|
+
import { z as z17 } from "zod";
|
|
11273
|
+
var RunResultSchema = z17.object({
|
|
11274
|
+
taskId: z17.string(),
|
|
11275
|
+
startedAt: z17.string(),
|
|
11276
|
+
completedAt: z17.string(),
|
|
11277
|
+
status: z17.enum(["success", "failure", "skipped", "no-issues"]),
|
|
11278
|
+
findings: z17.number(),
|
|
11279
|
+
fixed: z17.number(),
|
|
11280
|
+
prUrl: z17.string().nullable(),
|
|
11281
|
+
prUpdated: z17.boolean(),
|
|
11282
|
+
error: z17.string().optional()
|
|
10741
11283
|
});
|
|
10742
11284
|
var MAX_HISTORY = 500;
|
|
10743
11285
|
var fallbackLogger = {
|
|
@@ -10761,10 +11303,10 @@ var MaintenanceReporter = class {
|
|
|
10761
11303
|
*/
|
|
10762
11304
|
async load() {
|
|
10763
11305
|
try {
|
|
10764
|
-
await
|
|
10765
|
-
const filePath =
|
|
10766
|
-
const data = await
|
|
10767
|
-
const parsed =
|
|
11306
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11307
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11308
|
+
const data = await fs16.promises.readFile(filePath, "utf-8");
|
|
11309
|
+
const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
10768
11310
|
if (parsed.success) {
|
|
10769
11311
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10770
11312
|
}
|
|
@@ -10797,9 +11339,9 @@ var MaintenanceReporter = class {
|
|
|
10797
11339
|
*/
|
|
10798
11340
|
async persist() {
|
|
10799
11341
|
try {
|
|
10800
|
-
await
|
|
10801
|
-
const filePath =
|
|
10802
|
-
await
|
|
11342
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11343
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11344
|
+
await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
10803
11345
|
} catch (err) {
|
|
10804
11346
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10805
11347
|
}
|
|
@@ -11286,7 +11828,7 @@ function parseStatusLine(output) {
|
|
|
11286
11828
|
// src/maintenance/check-script-runner.ts
|
|
11287
11829
|
import { execFile as execFile6 } from "child_process";
|
|
11288
11830
|
import { promisify as promisify3 } from "util";
|
|
11289
|
-
import * as
|
|
11831
|
+
import * as path19 from "path";
|
|
11290
11832
|
var execFileAsync = promisify3(execFile6);
|
|
11291
11833
|
var CheckScriptRunner = class {
|
|
11292
11834
|
constructor(cwd) {
|
|
@@ -11305,7 +11847,7 @@ var CheckScriptRunner = class {
|
|
|
11305
11847
|
}
|
|
11306
11848
|
};
|
|
11307
11849
|
async function captureScript(spec, projectRoot) {
|
|
11308
|
-
const resolved =
|
|
11850
|
+
const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
|
|
11309
11851
|
const args = spec.args ?? [];
|
|
11310
11852
|
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11311
11853
|
try {
|
|
@@ -11395,8 +11937,8 @@ function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
|
11395
11937
|
}
|
|
11396
11938
|
|
|
11397
11939
|
// src/maintenance/output-store.ts
|
|
11398
|
-
import * as
|
|
11399
|
-
import * as
|
|
11940
|
+
import * as fs17 from "fs";
|
|
11941
|
+
import * as path20 from "path";
|
|
11400
11942
|
var DEFAULT_RETENTION = {
|
|
11401
11943
|
runs: 50,
|
|
11402
11944
|
maxAgeDays: 30
|
|
@@ -11436,13 +11978,13 @@ var TaskOutputStore = class {
|
|
|
11436
11978
|
async write(taskId, entry, retention) {
|
|
11437
11979
|
this.ensureSafeTaskId(taskId);
|
|
11438
11980
|
const dir = this.dirFor(taskId);
|
|
11439
|
-
await
|
|
11981
|
+
await fs17.promises.mkdir(dir, { recursive: true });
|
|
11440
11982
|
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11441
|
-
const filePath =
|
|
11983
|
+
const filePath = path20.join(dir, fileName);
|
|
11442
11984
|
const tmpPath = `${filePath}.tmp`;
|
|
11443
11985
|
const payload = JSON.stringify(entry, null, 2);
|
|
11444
|
-
await
|
|
11445
|
-
await
|
|
11986
|
+
await fs17.promises.writeFile(tmpPath, payload, "utf-8");
|
|
11987
|
+
await fs17.promises.rename(tmpPath, filePath);
|
|
11446
11988
|
try {
|
|
11447
11989
|
await this.applyRetention(taskId, retention);
|
|
11448
11990
|
} catch (err) {
|
|
@@ -11466,7 +12008,7 @@ var TaskOutputStore = class {
|
|
|
11466
12008
|
const slice = fileNames.slice(offset, offset + limit);
|
|
11467
12009
|
const out = [];
|
|
11468
12010
|
for (const name of slice) {
|
|
11469
|
-
const entry = await this.readEntry(
|
|
12011
|
+
const entry = await this.readEntry(path20.join(dir, name));
|
|
11470
12012
|
if (entry) out.push(entry);
|
|
11471
12013
|
}
|
|
11472
12014
|
return out;
|
|
@@ -11482,18 +12024,18 @@ var TaskOutputStore = class {
|
|
|
11482
12024
|
}
|
|
11483
12025
|
const dir = this.dirFor(taskId);
|
|
11484
12026
|
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
11485
|
-
return this.readEntry(
|
|
12027
|
+
return this.readEntry(path20.join(dir, fileName));
|
|
11486
12028
|
}
|
|
11487
12029
|
/**
|
|
11488
12030
|
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
11489
12031
|
* outputs from outside the store API.
|
|
11490
12032
|
*/
|
|
11491
12033
|
dirFor(taskId) {
|
|
11492
|
-
return
|
|
12034
|
+
return path20.join(this.rootDir, taskId, "outputs");
|
|
11493
12035
|
}
|
|
11494
12036
|
async readEntry(filePath) {
|
|
11495
12037
|
try {
|
|
11496
|
-
const buf = await
|
|
12038
|
+
const buf = await fs17.promises.readFile(filePath, "utf-8");
|
|
11497
12039
|
const parsed = JSON.parse(buf);
|
|
11498
12040
|
return parsed;
|
|
11499
12041
|
} catch {
|
|
@@ -11515,7 +12057,7 @@ var TaskOutputStore = class {
|
|
|
11515
12057
|
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
11516
12058
|
for (const name of toRemove) {
|
|
11517
12059
|
try {
|
|
11518
|
-
await
|
|
12060
|
+
await fs17.promises.unlink(path20.join(dir, name));
|
|
11519
12061
|
} catch {
|
|
11520
12062
|
}
|
|
11521
12063
|
}
|
|
@@ -11524,7 +12066,7 @@ var TaskOutputStore = class {
|
|
|
11524
12066
|
async function listJsonFilesDescending(dir) {
|
|
11525
12067
|
let names;
|
|
11526
12068
|
try {
|
|
11527
|
-
names = await
|
|
12069
|
+
names = await fs17.promises.readdir(dir);
|
|
11528
12070
|
} catch {
|
|
11529
12071
|
return [];
|
|
11530
12072
|
}
|
|
@@ -11698,8 +12240,8 @@ function validateCheckShape(prefix, task, errors) {
|
|
|
11698
12240
|
});
|
|
11699
12241
|
}
|
|
11700
12242
|
if (hasScript) {
|
|
11701
|
-
const
|
|
11702
|
-
if (!
|
|
12243
|
+
const path24 = task.checkScript?.path;
|
|
12244
|
+
if (!path24 || path24.trim().length === 0) {
|
|
11703
12245
|
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
11704
12246
|
}
|
|
11705
12247
|
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
@@ -11825,9 +12367,9 @@ function handleEdge(top, next, color, stack, errors, reported) {
|
|
|
11825
12367
|
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
11826
12368
|
}
|
|
11827
12369
|
}
|
|
11828
|
-
function reportCycle(
|
|
11829
|
-
const cycleStart =
|
|
11830
|
-
const cyclePath = cycleStart >= 0 ? [...
|
|
12370
|
+
function reportCycle(path24, next, errors, reported) {
|
|
12371
|
+
const cycleStart = path24.indexOf(next);
|
|
12372
|
+
const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
|
|
11831
12373
|
const key = cyclePath.join("\u2192");
|
|
11832
12374
|
if (reported.has(key)) return;
|
|
11833
12375
|
reported.add(key);
|
|
@@ -11838,11 +12380,6 @@ function reportCycle(path22, next, errors, reported) {
|
|
|
11838
12380
|
}
|
|
11839
12381
|
|
|
11840
12382
|
// 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
12383
|
var Orchestrator = class extends EventEmitter {
|
|
11847
12384
|
state;
|
|
11848
12385
|
config;
|
|
@@ -11867,6 +12404,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11867
12404
|
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
11868
12405
|
*/
|
|
11869
12406
|
backendFactory;
|
|
12407
|
+
/**
|
|
12408
|
+
* Spec B Phase 4 (D8): per-orchestrator in-process bus for
|
|
12409
|
+
* `RoutingDecision` events. Constructed alongside backendFactory when
|
|
12410
|
+
* agent.backends synthesis succeeds; null when legacy single-backend
|
|
12411
|
+
* config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
|
|
12412
|
+
* subscribe via `getRoutingDecisionBus()`.
|
|
12413
|
+
*/
|
|
12414
|
+
routingDecisionBus;
|
|
11870
12415
|
/**
|
|
11871
12416
|
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
11872
12417
|
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
@@ -11889,6 +12434,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11889
12434
|
* so this map is the single source of truth post-migration.
|
|
11890
12435
|
*/
|
|
11891
12436
|
localResolvers = /* @__PURE__ */ new Map();
|
|
12437
|
+
/**
|
|
12438
|
+
* Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
|
|
12439
|
+
* construction from `projectRoot/agents/skills/`. Consulted by
|
|
12440
|
+
* `buildRoutingUseCase` at dispatch start to construct
|
|
12441
|
+
* `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
|
|
12442
|
+
* Empty when the orchestrator runs outside a harness project root
|
|
12443
|
+
* (then dispatch falls through to per-tier, preserving F11/N2).
|
|
12444
|
+
*/
|
|
12445
|
+
skillCatalog;
|
|
11892
12446
|
/**
|
|
11893
12447
|
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
11894
12448
|
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
@@ -11937,7 +12491,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11937
12491
|
completionHandler;
|
|
11938
12492
|
/** Project root directory, derived from workspace root. */
|
|
11939
12493
|
get projectRoot() {
|
|
11940
|
-
return
|
|
12494
|
+
return path21.resolve(this.config.workspace.root, "..", "..");
|
|
11941
12495
|
}
|
|
11942
12496
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
11943
12497
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -11981,6 +12535,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11981
12535
|
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
11982
12536
|
);
|
|
11983
12537
|
}
|
|
12538
|
+
const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
|
|
12539
|
+
this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
|
|
12540
|
+
if (this.skillCatalog.length === 0) {
|
|
12541
|
+
this.logger.warn(
|
|
12542
|
+
`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")}.`
|
|
12543
|
+
);
|
|
12544
|
+
}
|
|
11984
12545
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
11985
12546
|
this.workspace = new WorkspaceManager(config.workspace, {
|
|
11986
12547
|
emitEvent: (event) => {
|
|
@@ -11992,10 +12553,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11992
12553
|
this.renderer = new PromptRenderer();
|
|
11993
12554
|
this.overrideBackend = overrides?.backend ?? null;
|
|
11994
12555
|
this.interactionQueue = new InteractionQueue(
|
|
11995
|
-
|
|
12556
|
+
path21.join(config.workspace.root, "..", "interactions"),
|
|
11996
12557
|
this
|
|
11997
12558
|
);
|
|
11998
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12559
|
+
this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
|
|
11999
12560
|
const backendsMap = this.config.agent.backends ?? {};
|
|
12000
12561
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
12001
12562
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -12016,6 +12577,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12016
12577
|
const routing = this.config.agent.routing ?? {
|
|
12017
12578
|
default: firstBackendName ?? "primary"
|
|
12018
12579
|
};
|
|
12580
|
+
this.routingDecisionBus = new RoutingDecisionBus({
|
|
12581
|
+
capacity: 500,
|
|
12582
|
+
logger: this.logger
|
|
12583
|
+
});
|
|
12019
12584
|
this.backendFactory = new OrchestratorBackendFactory({
|
|
12020
12585
|
backends: this.config.agent.backends,
|
|
12021
12586
|
routing,
|
|
@@ -12023,6 +12588,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12023
12588
|
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
12024
12589
|
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
12025
12590
|
cacheMetrics: this.cacheMetrics,
|
|
12591
|
+
decisionBus: this.routingDecisionBus,
|
|
12026
12592
|
getResolverModelFor: (name) => {
|
|
12027
12593
|
const resolver = this.localResolvers.get(name);
|
|
12028
12594
|
return resolver ? () => resolver.resolveModel() : void 0;
|
|
@@ -12030,6 +12596,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12030
12596
|
});
|
|
12031
12597
|
} else {
|
|
12032
12598
|
this.backendFactory = null;
|
|
12599
|
+
this.routingDecisionBus = null;
|
|
12033
12600
|
}
|
|
12034
12601
|
this.pipeline = null;
|
|
12035
12602
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
@@ -12039,7 +12606,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12039
12606
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
12040
12607
|
});
|
|
12041
12608
|
this.recorder = new StreamRecorder(
|
|
12042
|
-
|
|
12609
|
+
path21.resolve(config.workspace.root, "..", "streams"),
|
|
12043
12610
|
this.logger
|
|
12044
12611
|
);
|
|
12045
12612
|
const self = this;
|
|
@@ -12070,10 +12637,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12070
12637
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
12071
12638
|
if (config.server?.port) {
|
|
12072
12639
|
const webhookStore = new WebhookStore(
|
|
12073
|
-
|
|
12640
|
+
path21.join(this.projectRoot, ".harness", "webhooks.json")
|
|
12074
12641
|
);
|
|
12075
12642
|
this.webhookQueue = new WebhookQueue(
|
|
12076
|
-
|
|
12643
|
+
path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
12077
12644
|
);
|
|
12078
12645
|
const webhookDelivery = new WebhookDelivery({
|
|
12079
12646
|
queue: this.webhookQueue,
|
|
@@ -12111,7 +12678,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12111
12678
|
queue: this.webhookQueue
|
|
12112
12679
|
},
|
|
12113
12680
|
cacheMetrics: this.cacheMetrics,
|
|
12114
|
-
|
|
12681
|
+
// Spec B Phase 5: routing observability accessors. Closures so the
|
|
12682
|
+
// server re-reads on every request — stop() / start() do not
|
|
12683
|
+
// require server reconstruction. Returns null if no backendFactory
|
|
12684
|
+
// (legacy single-backend configs), and the route handler renders
|
|
12685
|
+
// 503 in that case.
|
|
12686
|
+
getBackendRouter: () => this.getBackendRouter(),
|
|
12687
|
+
getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
|
|
12688
|
+
getRoutingConfig: () => this.getRoutingConfig(),
|
|
12689
|
+
getBackends: () => this.getBackends(),
|
|
12690
|
+
plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
12115
12691
|
pipeline: this.pipeline,
|
|
12116
12692
|
analysisArchive: this.analysisArchive,
|
|
12117
12693
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -12221,7 +12797,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12221
12797
|
}
|
|
12222
12798
|
};
|
|
12223
12799
|
const outputStore = new TaskOutputStore({
|
|
12224
|
-
rootDir:
|
|
12800
|
+
rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12225
12801
|
logger: this.logger
|
|
12226
12802
|
});
|
|
12227
12803
|
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
@@ -12262,7 +12838,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
12262
12838
|
${messages}`);
|
|
12263
12839
|
}
|
|
12264
12840
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
12265
|
-
persistDir:
|
|
12841
|
+
persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12266
12842
|
logger: this.logger
|
|
12267
12843
|
});
|
|
12268
12844
|
await this.maintenanceReporter.load();
|
|
@@ -12313,10 +12889,17 @@ ${messages}`);
|
|
|
12313
12889
|
}
|
|
12314
12890
|
}
|
|
12315
12891
|
createIntelligencePipeline() {
|
|
12892
|
+
if (!this.backendFactory) {
|
|
12893
|
+
this.logger.warn(
|
|
12894
|
+
"intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
|
|
12895
|
+
);
|
|
12896
|
+
return null;
|
|
12897
|
+
}
|
|
12316
12898
|
const bundle = buildIntelligencePipeline({
|
|
12317
12899
|
config: this.config,
|
|
12318
12900
|
localResolvers: this.localResolvers,
|
|
12319
|
-
logger: this.logger
|
|
12901
|
+
logger: this.logger,
|
|
12902
|
+
router: this.backendFactory.getRouter()
|
|
12320
12903
|
});
|
|
12321
12904
|
if (!bundle) return null;
|
|
12322
12905
|
this.graphStore = bundle.graphStore;
|
|
@@ -12367,11 +12950,13 @@ ${messages}`);
|
|
|
12367
12950
|
simulationResults,
|
|
12368
12951
|
personaRecommendations
|
|
12369
12952
|
} = pipelineResult ?? {};
|
|
12953
|
+
const selfAssignee = await this.orchestratorIdPromise;
|
|
12370
12954
|
const tickEvent = {
|
|
12371
12955
|
type: "tick",
|
|
12372
12956
|
candidates,
|
|
12373
12957
|
runningStates: runningStatesResult.value,
|
|
12374
12958
|
nowMs,
|
|
12959
|
+
selfAssignee,
|
|
12375
12960
|
...concernSignals !== void 0 && { concernSignals },
|
|
12376
12961
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
12377
12962
|
...complexityScores !== void 0 && { complexityScores },
|
|
@@ -12795,14 +13380,24 @@ ${messages}`);
|
|
|
12795
13380
|
issue,
|
|
12796
13381
|
attempt: attempt || 1
|
|
12797
13382
|
});
|
|
12798
|
-
const useCase =
|
|
13383
|
+
const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
|
|
13384
|
+
const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
|
|
13385
|
+
const routerOpts = invocationOverride ? { invocationOverride } : void 0;
|
|
13386
|
+
if (invocationOverride) {
|
|
13387
|
+
this.logger.info(
|
|
13388
|
+
`Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
|
|
13389
|
+
{ issueId: issue.id }
|
|
13390
|
+
);
|
|
13391
|
+
}
|
|
12799
13392
|
let routedBackendName;
|
|
12800
13393
|
if (this.overrideBackend !== null) {
|
|
12801
13394
|
routedBackendName = this.overrideBackend.name;
|
|
12802
13395
|
} else if (this.backendFactory !== null) {
|
|
12803
|
-
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
13396
|
+
routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
|
|
12804
13397
|
} else {
|
|
12805
|
-
|
|
13398
|
+
const routingDefault = this.config.agent.routing?.default;
|
|
13399
|
+
const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
|
|
13400
|
+
routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
|
|
12806
13401
|
}
|
|
12807
13402
|
const session = {
|
|
12808
13403
|
sessionId: `pending-${Date.now()}`,
|
|
@@ -12841,7 +13436,7 @@ ${messages}`);
|
|
|
12841
13436
|
if (this.overrideBackend !== null) {
|
|
12842
13437
|
agentBackend = this.overrideBackend;
|
|
12843
13438
|
} else if (this.backendFactory !== null) {
|
|
12844
|
-
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
13439
|
+
agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
|
|
12845
13440
|
} else {
|
|
12846
13441
|
throw new Error(
|
|
12847
13442
|
`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 +13726,8 @@ ${messages}`);
|
|
|
13131
13726
|
unsub();
|
|
13132
13727
|
}
|
|
13133
13728
|
this.localModelStatusUnsubscribes = [];
|
|
13729
|
+
this.routingDecisionBus?.clearListeners();
|
|
13730
|
+
this.routingDecisionBus = null;
|
|
13134
13731
|
for (const resolver of this.localResolvers.values()) {
|
|
13135
13732
|
resolver.stop();
|
|
13136
13733
|
}
|
|
@@ -13204,6 +13801,42 @@ ${messages}`);
|
|
|
13204
13801
|
tickActivity: this.tickActivity
|
|
13205
13802
|
};
|
|
13206
13803
|
}
|
|
13804
|
+
/**
|
|
13805
|
+
* Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
|
|
13806
|
+
* Phase 7 (dashboard WS broadcast). Returns null when the legacy
|
|
13807
|
+
* single-backend config bypassed agent.backends synthesis.
|
|
13808
|
+
*/
|
|
13809
|
+
getRoutingDecisionBus() {
|
|
13810
|
+
return this.routingDecisionBus;
|
|
13811
|
+
}
|
|
13812
|
+
/**
|
|
13813
|
+
* Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
|
|
13814
|
+
* dispatch path uses the factory-owned router directly; observability
|
|
13815
|
+
* routes (config / decisions) reach it through this accessor. Returns
|
|
13816
|
+
* null when the legacy single-backend config bypassed agent.backends
|
|
13817
|
+
* synthesis (no backendFactory built).
|
|
13818
|
+
*/
|
|
13819
|
+
getBackendRouter() {
|
|
13820
|
+
return this.backendFactory?.getRouter() ?? null;
|
|
13821
|
+
}
|
|
13822
|
+
/**
|
|
13823
|
+
* Spec B Phase 5: snapshot of the active RoutingConfig for the config
|
|
13824
|
+
* route and the trace route's bus-less router construction. Returns
|
|
13825
|
+
* null when the operator's harness.config.json carries no
|
|
13826
|
+
* `agent.routing` block.
|
|
13827
|
+
*/
|
|
13828
|
+
getRoutingConfig() {
|
|
13829
|
+
return this.config.agent.routing ?? null;
|
|
13830
|
+
}
|
|
13831
|
+
/**
|
|
13832
|
+
* Spec B Phase 5: snapshot of `agent.backends` for the config route
|
|
13833
|
+
* (existence annotations) and the trace route (bus-less router
|
|
13834
|
+
* construction). Returns null when no synthesized backends map exists
|
|
13835
|
+
* (legacy single-backend configs).
|
|
13836
|
+
*/
|
|
13837
|
+
getBackends() {
|
|
13838
|
+
return this.config.agent.backends ?? null;
|
|
13839
|
+
}
|
|
13207
13840
|
/** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
|
|
13208
13841
|
getMaintenanceStatus() {
|
|
13209
13842
|
return this.maintenanceScheduler?.getStatus() ?? null;
|
|
@@ -13582,8 +14215,8 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
13582
14215
|
}
|
|
13583
14216
|
|
|
13584
14217
|
// src/sessions/search-index.ts
|
|
13585
|
-
import * as
|
|
13586
|
-
import * as
|
|
14218
|
+
import * as fs18 from "fs";
|
|
14219
|
+
import * as path22 from "path";
|
|
13587
14220
|
import Database2 from "better-sqlite3";
|
|
13588
14221
|
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
13589
14222
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
@@ -13628,7 +14261,7 @@ function normalizeFts5Query(query) {
|
|
|
13628
14261
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
13629
14262
|
}
|
|
13630
14263
|
function searchIndexPath(projectPath) {
|
|
13631
|
-
return
|
|
14264
|
+
return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
13632
14265
|
}
|
|
13633
14266
|
var FILE_KIND_TO_FILENAME = {
|
|
13634
14267
|
summary: "summary.md",
|
|
@@ -13643,7 +14276,7 @@ var SqliteSearchIndex = class {
|
|
|
13643
14276
|
removeSessionStmt;
|
|
13644
14277
|
totalStmt;
|
|
13645
14278
|
constructor(dbPath) {
|
|
13646
|
-
|
|
14279
|
+
fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
|
|
13647
14280
|
this.db = new Database2(dbPath);
|
|
13648
14281
|
this.db.pragma("journal_mode = WAL");
|
|
13649
14282
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -13748,14 +14381,14 @@ function indexSessionDirectory(idx, args) {
|
|
|
13748
14381
|
let docsWritten = 0;
|
|
13749
14382
|
for (const kind of kinds) {
|
|
13750
14383
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
13751
|
-
const filePath =
|
|
13752
|
-
if (!
|
|
13753
|
-
let body =
|
|
14384
|
+
const filePath = path22.join(args.sessionDir, fileName);
|
|
14385
|
+
if (!fs18.existsSync(filePath)) continue;
|
|
14386
|
+
let body = fs18.readFileSync(filePath, "utf8");
|
|
13754
14387
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
13755
14388
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
13756
14389
|
}
|
|
13757
|
-
const stat =
|
|
13758
|
-
const relPath =
|
|
14390
|
+
const stat = fs18.statSync(filePath);
|
|
14391
|
+
const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
13759
14392
|
idx.upsertSessionDoc({
|
|
13760
14393
|
sessionId: args.sessionId,
|
|
13761
14394
|
archived: args.archived,
|
|
@@ -13770,17 +14403,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
13770
14403
|
}
|
|
13771
14404
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
13772
14405
|
const start = Date.now();
|
|
13773
|
-
const archiveBase =
|
|
14406
|
+
const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
|
|
13774
14407
|
const idx = openSearchIndex(projectPath);
|
|
13775
14408
|
try {
|
|
13776
14409
|
idx.resetArchived();
|
|
13777
14410
|
let sessionsIndexed = 0;
|
|
13778
14411
|
let docsWritten = 0;
|
|
13779
|
-
if (
|
|
13780
|
-
const entries =
|
|
14412
|
+
if (fs18.existsSync(archiveBase)) {
|
|
14413
|
+
const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
|
|
13781
14414
|
for (const entry of entries) {
|
|
13782
14415
|
if (!entry.isDirectory()) continue;
|
|
13783
|
-
const sessionDir =
|
|
14416
|
+
const sessionDir = path22.join(archiveBase, entry.name);
|
|
13784
14417
|
const result = indexSessionDirectory(idx, {
|
|
13785
14418
|
sessionId: entry.name,
|
|
13786
14419
|
sessionDir,
|
|
@@ -13800,8 +14433,8 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
13800
14433
|
}
|
|
13801
14434
|
|
|
13802
14435
|
// src/sessions/summarize.ts
|
|
13803
|
-
import * as
|
|
13804
|
-
import * as
|
|
14436
|
+
import * as fs19 from "fs";
|
|
14437
|
+
import * as path23 from "path";
|
|
13805
14438
|
import {
|
|
13806
14439
|
SessionSummarySchema
|
|
13807
14440
|
} from "@harness-engineering/types";
|
|
@@ -13831,10 +14464,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
13831
14464
|
function readInputCorpus(archiveDir) {
|
|
13832
14465
|
const parts = [];
|
|
13833
14466
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
13834
|
-
const p =
|
|
13835
|
-
if (!
|
|
14467
|
+
const p = path23.join(archiveDir, filename);
|
|
14468
|
+
if (!fs19.existsSync(p)) continue;
|
|
13836
14469
|
try {
|
|
13837
|
-
const content =
|
|
14470
|
+
const content = fs19.readFileSync(p, "utf8");
|
|
13838
14471
|
if (content.trim().length === 0) continue;
|
|
13839
14472
|
parts.push(`## FILE: ${kind}
|
|
13840
14473
|
|
|
@@ -13885,7 +14518,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
13885
14518
|
return lines.join("\n");
|
|
13886
14519
|
}
|
|
13887
14520
|
function writeStubMarkdown(archiveDir, reason) {
|
|
13888
|
-
const filePath =
|
|
14521
|
+
const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
|
|
13889
14522
|
const body = `---
|
|
13890
14523
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13891
14524
|
schemaVersion: 1
|
|
@@ -13896,12 +14529,12 @@ status: failed
|
|
|
13896
14529
|
|
|
13897
14530
|
- reason: ${reason}
|
|
13898
14531
|
`;
|
|
13899
|
-
|
|
14532
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
13900
14533
|
return filePath;
|
|
13901
14534
|
}
|
|
13902
14535
|
async function summarizeArchivedSession(ctx) {
|
|
13903
14536
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
13904
|
-
if (!
|
|
14537
|
+
if (!fs19.existsSync(ctx.archiveDir)) {
|
|
13905
14538
|
return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
13906
14539
|
}
|
|
13907
14540
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
@@ -13962,9 +14595,9 @@ async function summarizeArchivedSession(ctx) {
|
|
|
13962
14595
|
outputTokens: response.tokenUsage.outputTokens,
|
|
13963
14596
|
schemaVersion: 1
|
|
13964
14597
|
};
|
|
13965
|
-
const filePath =
|
|
14598
|
+
const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
13966
14599
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
13967
|
-
|
|
14600
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
13968
14601
|
return Ok24({ summary: parsed.data, meta, filePath });
|
|
13969
14602
|
}
|
|
13970
14603
|
function isSummaryEnabled(config) {
|
|
@@ -14042,12 +14675,14 @@ function buildArchiveHooks(opts) {
|
|
|
14042
14675
|
export {
|
|
14043
14676
|
AnalysisArchive,
|
|
14044
14677
|
BUILT_IN_TASKS,
|
|
14678
|
+
BackendDefSchema,
|
|
14045
14679
|
BackendRouter,
|
|
14046
14680
|
ClaimManager,
|
|
14047
14681
|
GateNotReadyError,
|
|
14048
14682
|
GateRunError,
|
|
14049
14683
|
InteractionQueue,
|
|
14050
14684
|
LinearGraphQLStub,
|
|
14685
|
+
LocalModelResolver,
|
|
14051
14686
|
MAX_ATTEMPTS,
|
|
14052
14687
|
MockBackend,
|
|
14053
14688
|
ORCHESTRATOR_IDENTITY_FILE,
|
|
@@ -14058,6 +14693,8 @@ export {
|
|
|
14058
14693
|
PromptRenderer,
|
|
14059
14694
|
RETRY_DELAYS_MS,
|
|
14060
14695
|
RoadmapTrackerAdapter,
|
|
14696
|
+
RoutingConfigSchema,
|
|
14697
|
+
RoutingValueSchema,
|
|
14061
14698
|
SinkConfigError,
|
|
14062
14699
|
SinkRegistry,
|
|
14063
14700
|
SlackSink,
|
|
@@ -14077,7 +14714,11 @@ export {
|
|
|
14077
14714
|
computeRateLimitDelay,
|
|
14078
14715
|
createBackend,
|
|
14079
14716
|
createEmptyState,
|
|
14717
|
+
crossFieldRoutingIssues,
|
|
14718
|
+
defaultFetchModels,
|
|
14080
14719
|
detectScopeTier,
|
|
14720
|
+
discoverSkillCatalog,
|
|
14721
|
+
discoverSkillCatalogNames,
|
|
14081
14722
|
emitProposalApproved,
|
|
14082
14723
|
emitProposalCreated,
|
|
14083
14724
|
emitProposalRejected,
|
|
@@ -14093,6 +14734,7 @@ export {
|
|
|
14093
14734
|
loadPublishedIndex,
|
|
14094
14735
|
migrateAgentConfig,
|
|
14095
14736
|
normalizeFts5Query,
|
|
14737
|
+
normalizeLocalModel,
|
|
14096
14738
|
openSearchIndex,
|
|
14097
14739
|
promote,
|
|
14098
14740
|
reconcile,
|
|
@@ -14103,6 +14745,7 @@ export {
|
|
|
14103
14745
|
resolveEscalationConfig,
|
|
14104
14746
|
resolveOrchestratorId,
|
|
14105
14747
|
routeIssue,
|
|
14748
|
+
routingWarnings,
|
|
14106
14749
|
runGate,
|
|
14107
14750
|
savePublishedIndex,
|
|
14108
14751
|
searchIndexPath,
|