@harness-engineering/orchestrator 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +623 -51
- package/dist/index.d.ts +623 -51
- package/dist/index.js +2587 -529
- package/dist/index.mjs +2560 -501
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -27,7 +27,7 @@ function sortCandidates(issues) {
|
|
|
27
27
|
return comparePriority(a, b) ?? compareCreatedAt(a, b) ?? a.identifier.localeCompare(b.identifier);
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
function isEligible(issue, state, activeStates, terminalStates) {
|
|
30
|
+
function isEligible(issue, state, activeStates, terminalStates, selfAssignee) {
|
|
31
31
|
if (!issue.id || !issue.identifier || !issue.title || !issue.state) {
|
|
32
32
|
return false;
|
|
33
33
|
}
|
|
@@ -49,6 +49,9 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
49
49
|
if (state.completed.has(issue.id)) {
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
|
+
if (selfAssignee !== void 0 && issue.assignee != null && issue.assignee !== selfAssignee) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
52
55
|
if (normalizedState === "todo" && issue.blockedBy.length > 0) {
|
|
53
56
|
const hasNonTerminalBlocker = issue.blockedBy.some((blocker) => {
|
|
54
57
|
if (blocker.state === null) return true;
|
|
@@ -60,9 +63,11 @@ function isEligible(issue, state, activeStates, terminalStates) {
|
|
|
60
63
|
}
|
|
61
64
|
return true;
|
|
62
65
|
}
|
|
63
|
-
function selectCandidates(issues, state, activeStates, terminalStates) {
|
|
66
|
+
function selectCandidates(issues, state, activeStates, terminalStates, selfAssignee) {
|
|
64
67
|
const sorted = sortCandidates(issues);
|
|
65
|
-
return sorted.filter(
|
|
68
|
+
return sorted.filter(
|
|
69
|
+
(issue) => isEligible(issue, state, activeStates, terminalStates, selfAssignee)
|
|
70
|
+
);
|
|
66
71
|
}
|
|
67
72
|
|
|
68
73
|
// src/core/concurrency.ts
|
|
@@ -658,7 +663,8 @@ function handleTick(state, event, config) {
|
|
|
658
663
|
candidates,
|
|
659
664
|
next,
|
|
660
665
|
config.tracker.activeStates,
|
|
661
|
-
config.tracker.terminalStates
|
|
666
|
+
config.tracker.terminalStates,
|
|
667
|
+
event.selfAssignee
|
|
662
668
|
);
|
|
663
669
|
const escalationConfig = resolveEscalationConfig(config);
|
|
664
670
|
for (const issue of eligible) {
|
|
@@ -1144,7 +1150,7 @@ var ClaimManager = class {
|
|
|
1144
1150
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1145
1151
|
if (!claimResult.ok) return claimResult;
|
|
1146
1152
|
if (this.verifyDelayMs > 0) {
|
|
1147
|
-
await new Promise((
|
|
1153
|
+
await new Promise((resolve8) => setTimeout(resolve8, this.verifyDelayMs));
|
|
1148
1154
|
}
|
|
1149
1155
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1150
1156
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1796,7 +1802,8 @@ function formatFilesList(files) {
|
|
|
1796
1802
|
}
|
|
1797
1803
|
|
|
1798
1804
|
// src/workflow/loader.ts
|
|
1799
|
-
import * as
|
|
1805
|
+
import * as fs7 from "fs/promises";
|
|
1806
|
+
import * as path7 from "path";
|
|
1800
1807
|
import { parse } from "yaml";
|
|
1801
1808
|
import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
|
|
1802
1809
|
|
|
@@ -1804,7 +1811,8 @@ import { Ok as Ok3, Err as Err2 } from "@harness-engineering/types";
|
|
|
1804
1811
|
import { z as z2 } from "zod";
|
|
1805
1812
|
import {
|
|
1806
1813
|
Ok as Ok2,
|
|
1807
|
-
Err
|
|
1814
|
+
Err,
|
|
1815
|
+
STANDARD_COGNITIVE_MODES
|
|
1808
1816
|
} from "@harness-engineering/types";
|
|
1809
1817
|
|
|
1810
1818
|
// src/workflow/schema.ts
|
|
@@ -1852,16 +1860,29 @@ var BackendDefSchema = z.discriminatedUnion("type", [
|
|
|
1852
1860
|
probeIntervalMs: z.number().int().min(1e3).optional()
|
|
1853
1861
|
}).strict()
|
|
1854
1862
|
]);
|
|
1863
|
+
var RoutingValueSchema = z.union([
|
|
1864
|
+
z.string().min(1),
|
|
1865
|
+
z.array(z.string().min(1)).nonempty("fallback chain must contain at least one backend name").readonly()
|
|
1866
|
+
]);
|
|
1855
1867
|
var RoutingConfigSchema = z.object({
|
|
1856
|
-
default:
|
|
1857
|
-
"quick-fix":
|
|
1858
|
-
"guided-change":
|
|
1859
|
-
"full-exploration":
|
|
1860
|
-
diagnostic:
|
|
1868
|
+
default: RoutingValueSchema,
|
|
1869
|
+
"quick-fix": RoutingValueSchema.optional(),
|
|
1870
|
+
"guided-change": RoutingValueSchema.optional(),
|
|
1871
|
+
"full-exploration": RoutingValueSchema.optional(),
|
|
1872
|
+
diagnostic: RoutingValueSchema.optional(),
|
|
1861
1873
|
intelligence: z.object({
|
|
1862
|
-
sel:
|
|
1863
|
-
pesl:
|
|
1864
|
-
}).strict().optional()
|
|
1874
|
+
sel: RoutingValueSchema.optional(),
|
|
1875
|
+
pesl: RoutingValueSchema.optional()
|
|
1876
|
+
}).strict().optional(),
|
|
1877
|
+
// --- Spec B Phase 2: isolation block widened to RoutingValueSchema ---
|
|
1878
|
+
isolation: z.object({
|
|
1879
|
+
none: RoutingValueSchema.optional(),
|
|
1880
|
+
container: RoutingValueSchema.optional(),
|
|
1881
|
+
"remote-sandbox": RoutingValueSchema.optional()
|
|
1882
|
+
}).strict().optional(),
|
|
1883
|
+
// --- Spec B Phase 0: new optional maps (resolver wired in Phase 1) ---
|
|
1884
|
+
skills: z.record(z.string().min(1), RoutingValueSchema).optional(),
|
|
1885
|
+
modes: z.record(z.string().min(1), RoutingValueSchema).optional()
|
|
1865
1886
|
}).strict();
|
|
1866
1887
|
|
|
1867
1888
|
// src/workflow/config.ts
|
|
@@ -1870,13 +1891,17 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
|
1870
1891
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1871
1892
|
const issues = [];
|
|
1872
1893
|
const names = new Set(Object.keys(backends));
|
|
1873
|
-
const checkRef = (
|
|
1874
|
-
if (
|
|
1894
|
+
const checkRef = (path24, value) => {
|
|
1895
|
+
if (value === void 0) return;
|
|
1896
|
+
const entries = Array.isArray(value) ? value : [value];
|
|
1897
|
+
entries.forEach((name, idx) => {
|
|
1898
|
+
if (names.has(name)) return;
|
|
1899
|
+
const pathWithIdx = Array.isArray(value) ? [...path24, String(idx)] : path24;
|
|
1875
1900
|
issues.push({
|
|
1876
|
-
path:
|
|
1877
|
-
message: `routing.${
|
|
1901
|
+
path: pathWithIdx,
|
|
1902
|
+
message: `routing.${pathWithIdx.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1878
1903
|
});
|
|
1879
|
-
}
|
|
1904
|
+
});
|
|
1880
1905
|
};
|
|
1881
1906
|
checkRef(["default"], routing.default);
|
|
1882
1907
|
checkRef(["quick-fix"], routing["quick-fix"]);
|
|
@@ -1885,9 +1910,44 @@ function crossFieldRoutingIssues(backends, routing) {
|
|
|
1885
1910
|
checkRef(["diagnostic"], routing.diagnostic);
|
|
1886
1911
|
checkRef(["intelligence", "sel"], routing.intelligence?.sel);
|
|
1887
1912
|
checkRef(["intelligence", "pesl"], routing.intelligence?.pesl);
|
|
1913
|
+
checkRef(["isolation", "none"], routing.isolation?.none);
|
|
1914
|
+
checkRef(["isolation", "container"], routing.isolation?.container);
|
|
1915
|
+
checkRef(["isolation", "remote-sandbox"], routing.isolation?.["remote-sandbox"]);
|
|
1916
|
+
if (routing.skills) {
|
|
1917
|
+
for (const [skill, value] of Object.entries(routing.skills)) {
|
|
1918
|
+
checkRef(["skills", skill], value);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
if (routing.modes) {
|
|
1922
|
+
for (const [mode, value] of Object.entries(routing.modes)) {
|
|
1923
|
+
checkRef(["modes", mode], value);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1888
1926
|
return issues;
|
|
1889
1927
|
}
|
|
1890
|
-
function
|
|
1928
|
+
function routingWarnings(routing, knownSkillNames) {
|
|
1929
|
+
const warnings = [];
|
|
1930
|
+
if (knownSkillNames.length > 0 && routing.skills) {
|
|
1931
|
+
const known = new Set(knownSkillNames);
|
|
1932
|
+
for (const name of Object.keys(routing.skills)) {
|
|
1933
|
+
if (known.has(name)) continue;
|
|
1934
|
+
warnings.push(
|
|
1935
|
+
`routing.skills.${name} references a skill that is not present in the local skill catalog. If this is intentional (e.g., a skill installed by a downstream consumer), this warning can be ignored.`
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (routing.modes) {
|
|
1940
|
+
const standardModes = new Set(STANDARD_COGNITIVE_MODES);
|
|
1941
|
+
for (const mode of Object.keys(routing.modes)) {
|
|
1942
|
+
if (standardModes.has(mode)) continue;
|
|
1943
|
+
warnings.push(
|
|
1944
|
+
`routing.modes.${mode} is not in STANDARD_COGNITIVE_MODES (${[...STANDARD_COGNITIVE_MODES].join(", ")}). Custom cognitive modes are allowed but uncommon; verify this is not a typo.`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
return warnings;
|
|
1949
|
+
}
|
|
1950
|
+
function validateWorkflowConfig(config, options = {}) {
|
|
1891
1951
|
if (!config || typeof config !== "object")
|
|
1892
1952
|
return Err(new Error("Config is missing or not an object"));
|
|
1893
1953
|
const c = config;
|
|
@@ -1903,6 +1963,7 @@ function validateWorkflowConfig(config) {
|
|
|
1903
1963
|
if (!hasLegacyBackend && !hasModernBackends) {
|
|
1904
1964
|
return Err(new Error("Config must define agent.backend or agent.backends."));
|
|
1905
1965
|
}
|
|
1966
|
+
const warnings = [];
|
|
1906
1967
|
if (hasModernBackends) {
|
|
1907
1968
|
const backendsParsed = BackendsMapSchema.safeParse(agent.backends);
|
|
1908
1969
|
if (!backendsParsed.success) {
|
|
@@ -1913,9 +1974,10 @@ function validateWorkflowConfig(config) {
|
|
|
1913
1974
|
return Err(new Error(`agent.routing: ${routingParsed.error.message}`));
|
|
1914
1975
|
}
|
|
1915
1976
|
if (routingParsed.data) {
|
|
1977
|
+
const routingData = routingParsed.data;
|
|
1916
1978
|
const cross = crossFieldRoutingIssues(
|
|
1917
1979
|
backendsParsed.data,
|
|
1918
|
-
|
|
1980
|
+
routingData
|
|
1919
1981
|
);
|
|
1920
1982
|
if (cross.length > 0) {
|
|
1921
1983
|
return Err(
|
|
@@ -1924,9 +1986,10 @@ function validateWorkflowConfig(config) {
|
|
|
1924
1986
|
)
|
|
1925
1987
|
);
|
|
1926
1988
|
}
|
|
1989
|
+
warnings.push(...routingWarnings(routingData, options.knownSkillNames ?? []));
|
|
1927
1990
|
}
|
|
1928
1991
|
}
|
|
1929
|
-
return Ok2(config);
|
|
1992
|
+
return Ok2({ config, warnings });
|
|
1930
1993
|
}
|
|
1931
1994
|
function getDefaultConfig() {
|
|
1932
1995
|
return {
|
|
@@ -1979,11 +2042,55 @@ function getDefaultConfig() {
|
|
|
1979
2042
|
};
|
|
1980
2043
|
}
|
|
1981
2044
|
|
|
2045
|
+
// src/workflow/skill-catalog.ts
|
|
2046
|
+
import * as fs6 from "fs";
|
|
2047
|
+
import * as path6 from "path";
|
|
2048
|
+
import { parse as parseYaml } from "yaml";
|
|
2049
|
+
function discoverSkillCatalog(projectRoot) {
|
|
2050
|
+
const skillsRoot = path6.join(projectRoot, "agents", "skills");
|
|
2051
|
+
if (!fs6.existsSync(skillsRoot)) return [];
|
|
2052
|
+
const byName = /* @__PURE__ */ new Map();
|
|
2053
|
+
let hosts;
|
|
2054
|
+
try {
|
|
2055
|
+
hosts = fs6.readdirSync(skillsRoot, { withFileTypes: true });
|
|
2056
|
+
} catch {
|
|
2057
|
+
return [];
|
|
2058
|
+
}
|
|
2059
|
+
for (const host of hosts) {
|
|
2060
|
+
if (!host.isDirectory()) continue;
|
|
2061
|
+
const hostDir = path6.join(skillsRoot, host.name);
|
|
2062
|
+
let skills;
|
|
2063
|
+
try {
|
|
2064
|
+
skills = fs6.readdirSync(hostDir, { withFileTypes: true });
|
|
2065
|
+
} catch {
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
for (const skill of skills) {
|
|
2069
|
+
if (!skill.isDirectory()) continue;
|
|
2070
|
+
const skillYamlPath = path6.join(hostDir, skill.name, "skill.yaml");
|
|
2071
|
+
if (!fs6.existsSync(skillYamlPath)) continue;
|
|
2072
|
+
try {
|
|
2073
|
+
const content = fs6.readFileSync(skillYamlPath, "utf-8");
|
|
2074
|
+
const parsed = parseYaml(content);
|
|
2075
|
+
if (parsed && typeof parsed.name === "string" && parsed.name.length > 0 && !byName.has(parsed.name)) {
|
|
2076
|
+
const entry = typeof parsed.cognitive_mode === "string" && parsed.cognitive_mode.length > 0 ? { name: parsed.name, cognitiveMode: parsed.cognitive_mode } : { name: parsed.name };
|
|
2077
|
+
byName.set(parsed.name, entry);
|
|
2078
|
+
}
|
|
2079
|
+
} catch {
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
2084
|
+
}
|
|
2085
|
+
function discoverSkillCatalogNames(projectRoot) {
|
|
2086
|
+
return discoverSkillCatalog(projectRoot).map((e) => e.name);
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1982
2089
|
// src/workflow/loader.ts
|
|
1983
2090
|
var WorkflowLoader = class {
|
|
1984
2091
|
async loadWorkflow(filePath) {
|
|
1985
2092
|
try {
|
|
1986
|
-
const content = await
|
|
2093
|
+
const content = await fs7.readFile(filePath, "utf-8");
|
|
1987
2094
|
const parts = content.split("---");
|
|
1988
2095
|
if (parts.length < 3) {
|
|
1989
2096
|
return Err2(
|
|
@@ -1995,13 +2102,16 @@ var WorkflowLoader = class {
|
|
|
1995
2102
|
const yamlContent = parts[1].trim();
|
|
1996
2103
|
const promptTemplate = parts.slice(2).join("---").trim();
|
|
1997
2104
|
const configData = parse(yamlContent);
|
|
1998
|
-
const
|
|
2105
|
+
const projectRoot = path7.dirname(path7.resolve(filePath));
|
|
2106
|
+
const knownSkillNames = discoverSkillCatalogNames(projectRoot);
|
|
2107
|
+
const configResult = validateWorkflowConfig(configData, { knownSkillNames });
|
|
1999
2108
|
if (!configResult.ok) {
|
|
2000
2109
|
return Err2(configResult.error);
|
|
2001
2110
|
}
|
|
2002
2111
|
return Ok3({
|
|
2003
|
-
config: configResult.value,
|
|
2004
|
-
promptTemplate
|
|
2112
|
+
config: configResult.value.config,
|
|
2113
|
+
promptTemplate,
|
|
2114
|
+
warnings: configResult.value.warnings
|
|
2005
2115
|
});
|
|
2006
2116
|
} catch (error) {
|
|
2007
2117
|
return Err2(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2010,7 +2120,7 @@ var WorkflowLoader = class {
|
|
|
2010
2120
|
};
|
|
2011
2121
|
|
|
2012
2122
|
// src/tracker/adapters/roadmap.ts
|
|
2013
|
-
import * as
|
|
2123
|
+
import * as fs8 from "fs/promises";
|
|
2014
2124
|
import { createHash as createHash2 } from "crypto";
|
|
2015
2125
|
import {
|
|
2016
2126
|
parseRoadmap,
|
|
@@ -2047,7 +2157,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2047
2157
|
async fetchIssuesByStates(stateNames) {
|
|
2048
2158
|
try {
|
|
2049
2159
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2050
|
-
const content = await
|
|
2160
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2051
2161
|
const roadmapResult = parseRoadmap(content);
|
|
2052
2162
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2053
2163
|
const issues = [];
|
|
@@ -2079,7 +2189,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2079
2189
|
if (!terminal) {
|
|
2080
2190
|
return Err3(new Error("Tracker config has no terminalStates; cannot mark complete"));
|
|
2081
2191
|
}
|
|
2082
|
-
const content = await
|
|
2192
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2083
2193
|
const roadmapResult = parseRoadmap(content);
|
|
2084
2194
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2085
2195
|
const roadmap = roadmapResult.value;
|
|
@@ -2088,7 +2198,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2088
2198
|
const normalizedTerminal = this.config.terminalStates.map((s) => s.toLowerCase());
|
|
2089
2199
|
if (normalizedTerminal.includes(target.status.toLowerCase())) return Ok4(void 0);
|
|
2090
2200
|
target.status = terminal;
|
|
2091
|
-
await
|
|
2201
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2092
2202
|
return Ok4(void 0);
|
|
2093
2203
|
} catch (error) {
|
|
2094
2204
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2102,19 +2212,22 @@ var RoadmapTrackerAdapter = class {
|
|
|
2102
2212
|
async claimIssue(issueId, orchestratorId) {
|
|
2103
2213
|
try {
|
|
2104
2214
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2105
|
-
const content = await
|
|
2215
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2106
2216
|
const roadmapResult = parseRoadmap(content);
|
|
2107
2217
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2108
2218
|
const roadmap = roadmapResult.value;
|
|
2109
2219
|
const target = this.findFeatureById(roadmap.milestones, issueId);
|
|
2110
2220
|
if (!target) return Ok4(void 0);
|
|
2221
|
+
if (target.assignee != null && target.assignee !== orchestratorId) {
|
|
2222
|
+
return Ok4(void 0);
|
|
2223
|
+
}
|
|
2111
2224
|
if (target.status === "in-progress" && target.assignee === orchestratorId) {
|
|
2112
2225
|
return Ok4(void 0);
|
|
2113
2226
|
}
|
|
2114
2227
|
target.status = "in-progress";
|
|
2115
2228
|
target.assignee = orchestratorId;
|
|
2116
2229
|
target.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2117
|
-
await
|
|
2230
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2118
2231
|
return Ok4(void 0);
|
|
2119
2232
|
} catch (error) {
|
|
2120
2233
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2131,7 +2244,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2131
2244
|
if (!activeState) {
|
|
2132
2245
|
return Err3(new Error("Tracker config has no activeStates; cannot release"));
|
|
2133
2246
|
}
|
|
2134
|
-
const content = await
|
|
2247
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2135
2248
|
const roadmapResult = parseRoadmap(content);
|
|
2136
2249
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2137
2250
|
const roadmap = roadmapResult.value;
|
|
@@ -2143,7 +2256,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2143
2256
|
target.status = activeState;
|
|
2144
2257
|
target.assignee = null;
|
|
2145
2258
|
target.updatedAt = null;
|
|
2146
|
-
await
|
|
2259
|
+
await fs8.writeFile(this.config.filePath, serializeRoadmap(roadmap), "utf-8");
|
|
2147
2260
|
return Ok4(void 0);
|
|
2148
2261
|
} catch (error) {
|
|
2149
2262
|
return Err3(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -2165,7 +2278,7 @@ var RoadmapTrackerAdapter = class {
|
|
|
2165
2278
|
async fetchIssueStatesByIds(issueIds) {
|
|
2166
2279
|
try {
|
|
2167
2280
|
if (!this.config.filePath) return Err3(new Error("Missing filePath"));
|
|
2168
|
-
const content = await
|
|
2281
|
+
const content = await fs8.readFile(this.config.filePath, "utf-8");
|
|
2169
2282
|
const roadmapResult = parseRoadmap(content);
|
|
2170
2283
|
if (!roadmapResult.ok) return roadmapResult;
|
|
2171
2284
|
const issueMap = /* @__PURE__ */ new Map();
|
|
@@ -2230,8 +2343,8 @@ var LinearGraphQLStub = class {
|
|
|
2230
2343
|
};
|
|
2231
2344
|
|
|
2232
2345
|
// src/workspace/manager.ts
|
|
2233
|
-
import * as
|
|
2234
|
-
import * as
|
|
2346
|
+
import * as fs9 from "fs/promises";
|
|
2347
|
+
import * as path8 from "path";
|
|
2235
2348
|
import { execFile as execFile2 } from "child_process";
|
|
2236
2349
|
import { promisify as promisify2 } from "util";
|
|
2237
2350
|
import { Ok as Ok6, Err as Err4 } from "@harness-engineering/types";
|
|
@@ -2262,15 +2375,15 @@ var WorkspaceManager = class {
|
|
|
2262
2375
|
*/
|
|
2263
2376
|
resolvePath(identifier) {
|
|
2264
2377
|
const sanitized = this.sanitizeIdentifier(identifier);
|
|
2265
|
-
return
|
|
2378
|
+
return path8.join(this.config.root, sanitized);
|
|
2266
2379
|
}
|
|
2267
2380
|
/**
|
|
2268
2381
|
* Discovers the git repository root from the workspace root directory.
|
|
2269
2382
|
*/
|
|
2270
2383
|
async getRepoRoot() {
|
|
2271
2384
|
if (this.repoRoot) return this.repoRoot;
|
|
2272
|
-
const root =
|
|
2273
|
-
await
|
|
2385
|
+
const root = path8.resolve(this.config.root);
|
|
2386
|
+
await fs9.mkdir(root, { recursive: true });
|
|
2274
2387
|
const stdout = await this.git(["rev-parse", "--show-toplevel"], root);
|
|
2275
2388
|
this.repoRoot = stdout.trim();
|
|
2276
2389
|
return this.repoRoot;
|
|
@@ -2281,23 +2394,23 @@ var WorkspaceManager = class {
|
|
|
2281
2394
|
*/
|
|
2282
2395
|
async ensureWorkspace(identifier) {
|
|
2283
2396
|
try {
|
|
2284
|
-
const workspacePath =
|
|
2397
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2285
2398
|
try {
|
|
2286
|
-
await
|
|
2399
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2287
2400
|
const repoRoot2 = await this.getRepoRoot();
|
|
2288
2401
|
try {
|
|
2289
2402
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2290
2403
|
} catch {
|
|
2291
|
-
await
|
|
2404
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2292
2405
|
}
|
|
2293
2406
|
} catch {
|
|
2294
2407
|
try {
|
|
2295
|
-
await
|
|
2408
|
+
await fs9.access(workspacePath);
|
|
2296
2409
|
const repoRoot2 = await this.getRepoRoot();
|
|
2297
2410
|
try {
|
|
2298
2411
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot2);
|
|
2299
2412
|
} catch {
|
|
2300
|
-
await
|
|
2413
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2301
2414
|
}
|
|
2302
2415
|
} catch {
|
|
2303
2416
|
}
|
|
@@ -2393,7 +2506,7 @@ var WorkspaceManager = class {
|
|
|
2393
2506
|
async exists(identifier) {
|
|
2394
2507
|
try {
|
|
2395
2508
|
const workspacePath = this.resolvePath(identifier);
|
|
2396
|
-
await
|
|
2509
|
+
await fs9.access(workspacePath);
|
|
2397
2510
|
return true;
|
|
2398
2511
|
} catch {
|
|
2399
2512
|
return false;
|
|
@@ -2406,9 +2519,9 @@ var WorkspaceManager = class {
|
|
|
2406
2519
|
*/
|
|
2407
2520
|
async findPushedBranch(identifier) {
|
|
2408
2521
|
try {
|
|
2409
|
-
const workspacePath =
|
|
2522
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2410
2523
|
try {
|
|
2411
|
-
await
|
|
2524
|
+
await fs9.access(path8.join(workspacePath, ".git"));
|
|
2412
2525
|
} catch {
|
|
2413
2526
|
return null;
|
|
2414
2527
|
}
|
|
@@ -2514,12 +2627,12 @@ var WorkspaceManager = class {
|
|
|
2514
2627
|
*/
|
|
2515
2628
|
async removeWorkspace(identifier) {
|
|
2516
2629
|
try {
|
|
2517
|
-
const workspacePath =
|
|
2630
|
+
const workspacePath = path8.resolve(this.resolvePath(identifier));
|
|
2518
2631
|
try {
|
|
2519
2632
|
const repoRoot = await this.getRepoRoot();
|
|
2520
2633
|
await this.git(["worktree", "remove", "--force", workspacePath], repoRoot);
|
|
2521
2634
|
} catch {
|
|
2522
|
-
await
|
|
2635
|
+
await fs9.rm(workspacePath, { recursive: true, force: true });
|
|
2523
2636
|
}
|
|
2524
2637
|
return Ok6(void 0);
|
|
2525
2638
|
} catch (error) {
|
|
@@ -2544,7 +2657,7 @@ var WorkspaceHooks = class {
|
|
|
2544
2657
|
if (!command) {
|
|
2545
2658
|
return Ok7(void 0);
|
|
2546
2659
|
}
|
|
2547
|
-
return new Promise((
|
|
2660
|
+
return new Promise((resolve8) => {
|
|
2548
2661
|
const filteredEnv = {};
|
|
2549
2662
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2550
2663
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2557,19 +2670,19 @@ var WorkspaceHooks = class {
|
|
|
2557
2670
|
});
|
|
2558
2671
|
const timeout = setTimeout(() => {
|
|
2559
2672
|
child.kill();
|
|
2560
|
-
|
|
2673
|
+
resolve8(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2561
2674
|
}, this.config.timeoutMs);
|
|
2562
2675
|
child.on("exit", (code) => {
|
|
2563
2676
|
clearTimeout(timeout);
|
|
2564
2677
|
if (code === 0) {
|
|
2565
|
-
|
|
2678
|
+
resolve8(Ok7(void 0));
|
|
2566
2679
|
} else {
|
|
2567
|
-
|
|
2680
|
+
resolve8(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2568
2681
|
}
|
|
2569
2682
|
});
|
|
2570
2683
|
child.on("error", (error) => {
|
|
2571
2684
|
clearTimeout(timeout);
|
|
2572
|
-
|
|
2685
|
+
resolve8(Err5(error));
|
|
2573
2686
|
});
|
|
2574
2687
|
});
|
|
2575
2688
|
}
|
|
@@ -2609,7 +2722,7 @@ var MockBackend = class {
|
|
|
2609
2722
|
content: "Thinking...",
|
|
2610
2723
|
sessionId: session.sessionId
|
|
2611
2724
|
};
|
|
2612
|
-
await new Promise((
|
|
2725
|
+
await new Promise((resolve8) => setTimeout(resolve8, 100));
|
|
2613
2726
|
yield {
|
|
2614
2727
|
type: "thought",
|
|
2615
2728
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2661,12 +2774,12 @@ var PromptRenderer = class {
|
|
|
2661
2774
|
|
|
2662
2775
|
// src/orchestrator.ts
|
|
2663
2776
|
import { EventEmitter } from "events";
|
|
2664
|
-
import * as
|
|
2777
|
+
import * as path21 from "path";
|
|
2665
2778
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2666
2779
|
import { writeTaint } from "@harness-engineering/core";
|
|
2667
2780
|
|
|
2668
2781
|
// src/intelligence/pipeline-runner.ts
|
|
2669
|
-
import * as
|
|
2782
|
+
import * as path9 from "path";
|
|
2670
2783
|
import { weightedRecommendPersona, refreshProfiles } from "@harness-engineering/intelligence";
|
|
2671
2784
|
import {
|
|
2672
2785
|
GitHubIssuesSyncAdapter,
|
|
@@ -2788,7 +2901,7 @@ var IntelligencePipelineRunner = class {
|
|
|
2788
2901
|
}
|
|
2789
2902
|
async loadGraphStore() {
|
|
2790
2903
|
try {
|
|
2791
|
-
const graphDir =
|
|
2904
|
+
const graphDir = path9.join(this.ctx.config.workspace.root, "..", "graph");
|
|
2792
2905
|
const loaded = await this.ctx.graphStore.load(graphDir);
|
|
2793
2906
|
if (loaded) {
|
|
2794
2907
|
this.ctx.logger.info("Graph store loaded from disk");
|
|
@@ -3066,7 +3179,7 @@ var IntelligencePipelineRunner = class {
|
|
|
3066
3179
|
};
|
|
3067
3180
|
|
|
3068
3181
|
// src/completion/handler.ts
|
|
3069
|
-
import * as
|
|
3182
|
+
import * as path10 from "path";
|
|
3070
3183
|
import { GitHubIssuesSyncAdapter as GitHubIssuesSyncAdapter2, loadTrackerSyncConfig as loadTrackerSyncConfig2 } from "@harness-engineering/core";
|
|
3071
3184
|
var CompletionHandler = class {
|
|
3072
3185
|
ctx;
|
|
@@ -3149,7 +3262,7 @@ var CompletionHandler = class {
|
|
|
3149
3262
|
result: outcome.result
|
|
3150
3263
|
});
|
|
3151
3264
|
if (this.ctx.graphStore) {
|
|
3152
|
-
const graphDir =
|
|
3265
|
+
const graphDir = path10.join(this.ctx.config.workspace.root, "..", "graph");
|
|
3153
3266
|
await this.ctx.graphStore.save(graphDir);
|
|
3154
3267
|
}
|
|
3155
3268
|
} catch (err) {
|
|
@@ -3635,11 +3748,11 @@ function detectLegacyFields(agent) {
|
|
|
3635
3748
|
}
|
|
3636
3749
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3637
3750
|
const warnings = [];
|
|
3638
|
-
for (const
|
|
3639
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3640
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3751
|
+
for (const path24 of presentLegacy) {
|
|
3752
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path24)) continue;
|
|
3753
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path24)) continue;
|
|
3641
3754
|
warnings.push(
|
|
3642
|
-
`Ignoring legacy field '${
|
|
3755
|
+
`Ignoring legacy field '${path24}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3643
3756
|
);
|
|
3644
3757
|
}
|
|
3645
3758
|
return warnings;
|
|
@@ -3667,7 +3780,7 @@ function migrateAgentConfig(agent) {
|
|
|
3667
3780
|
}
|
|
3668
3781
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3669
3782
|
const warnings = presentLegacy.map(
|
|
3670
|
-
(
|
|
3783
|
+
(path24) => `Deprecated config field '${path24}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3671
3784
|
);
|
|
3672
3785
|
return {
|
|
3673
3786
|
config: { ...agent, backends, routing },
|
|
@@ -3730,61 +3843,160 @@ function synthesizeLocal(agent) {
|
|
|
3730
3843
|
}
|
|
3731
3844
|
|
|
3732
3845
|
// src/agent/backend-router.ts
|
|
3846
|
+
function toArray(value) {
|
|
3847
|
+
return Array.isArray(value) ? value : [value];
|
|
3848
|
+
}
|
|
3733
3849
|
var BackendRouter = class {
|
|
3734
3850
|
backends;
|
|
3735
3851
|
routing;
|
|
3852
|
+
decisionBus;
|
|
3736
3853
|
constructor(opts) {
|
|
3737
3854
|
this.backends = opts.backends;
|
|
3738
3855
|
this.routing = opts.routing;
|
|
3856
|
+
this.decisionBus = opts.decisionBus;
|
|
3739
3857
|
this.validateReferences();
|
|
3740
3858
|
}
|
|
3741
3859
|
/**
|
|
3742
|
-
*
|
|
3860
|
+
* Resolve a {@link RoutingUseCase} to a {@link RoutingDecision}.
|
|
3861
|
+
*
|
|
3862
|
+
* @param useCase the routing query
|
|
3863
|
+
* @param opts.invocationOverride if set and the named backend exists,
|
|
3864
|
+
* beats all other sources (D7 — the `--backend <name>` escape hatch)
|
|
3865
|
+
*/
|
|
3866
|
+
resolve(useCase, opts) {
|
|
3867
|
+
const startedAt = performance.now();
|
|
3868
|
+
const path24 = [];
|
|
3869
|
+
const tryChain = (source, value) => {
|
|
3870
|
+
if (value === void 0) return void 0;
|
|
3871
|
+
for (const name of toArray(value)) {
|
|
3872
|
+
const step = { source, candidate: name, outcome: "considered" };
|
|
3873
|
+
path24.push(step);
|
|
3874
|
+
if (this.backends[name]) {
|
|
3875
|
+
step.outcome = "chosen";
|
|
3876
|
+
return name;
|
|
3877
|
+
}
|
|
3878
|
+
step.outcome = "unknown-backend";
|
|
3879
|
+
}
|
|
3880
|
+
return void 0;
|
|
3881
|
+
};
|
|
3882
|
+
const decide = (backendName) => {
|
|
3883
|
+
const def = this.backends[backendName];
|
|
3884
|
+
if (!def) {
|
|
3885
|
+
throw new Error(
|
|
3886
|
+
`BackendRouter.resolve: internal invariant violated \u2014 backend '${backendName}' missing.`
|
|
3887
|
+
);
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3891
|
+
useCase,
|
|
3892
|
+
resolutionPath: path24,
|
|
3893
|
+
backendName,
|
|
3894
|
+
backendType: def.type,
|
|
3895
|
+
durationMs: performance.now() - startedAt
|
|
3896
|
+
};
|
|
3897
|
+
};
|
|
3898
|
+
const emitAndReturn = (decision) => {
|
|
3899
|
+
this.decisionBus?.emit(decision);
|
|
3900
|
+
return decision;
|
|
3901
|
+
};
|
|
3902
|
+
const fromInvocation = tryChain(
|
|
3903
|
+
"invocation",
|
|
3904
|
+
opts?.invocationOverride !== void 0 ? opts.invocationOverride : void 0
|
|
3905
|
+
);
|
|
3906
|
+
if (fromInvocation) return emitAndReturn(decide(fromInvocation));
|
|
3907
|
+
if (useCase.kind === "skill") {
|
|
3908
|
+
const fromSkill = tryChain("skill", this.routing.skills?.[useCase.skillName]);
|
|
3909
|
+
if (fromSkill) return emitAndReturn(decide(fromSkill));
|
|
3910
|
+
}
|
|
3911
|
+
const mode = useCase.kind === "skill" ? useCase.cognitiveMode : useCase.kind === "mode" ? useCase.cognitiveMode : void 0;
|
|
3912
|
+
if (mode !== void 0) {
|
|
3913
|
+
const fromMode = tryChain("mode", this.routing.modes?.[mode]);
|
|
3914
|
+
if (fromMode) return emitAndReturn(decide(fromMode));
|
|
3915
|
+
}
|
|
3916
|
+
const fromExisting = this.resolveExistingUseCase(useCase);
|
|
3917
|
+
if (fromExisting !== void 0) {
|
|
3918
|
+
const chained = tryChain("tier", fromExisting);
|
|
3919
|
+
if (chained) return emitAndReturn(decide(chained));
|
|
3920
|
+
}
|
|
3921
|
+
const fromDefault = tryChain("default", this.routing.default);
|
|
3922
|
+
if (fromDefault) return emitAndReturn(decide(fromDefault));
|
|
3923
|
+
const knownList = Object.keys(this.backends).join(", ") || "(none)";
|
|
3924
|
+
throw new Error(
|
|
3925
|
+
`BackendRouter.resolve: routing.default produced no available backend for useCase=${JSON.stringify(useCase)}. Resolution path: ${JSON.stringify(path24)}. Known backends: [${knownList}].`
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Returns the {@link BackendDef} reference for the resolved name.
|
|
3930
|
+
* Identity-equal to the entry in `backends` (no copy) so callers
|
|
3931
|
+
* relying on reference equality (SC21) continue to work.
|
|
3932
|
+
*/
|
|
3933
|
+
resolveDefinition(useCase, opts) {
|
|
3934
|
+
const decision = this.resolve(useCase, opts);
|
|
3935
|
+
const def = this.backends[decision.backendName];
|
|
3936
|
+
if (!def) {
|
|
3937
|
+
throw new Error(
|
|
3938
|
+
`BackendRouter.resolveDefinition: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3939
|
+
);
|
|
3940
|
+
}
|
|
3941
|
+
return def;
|
|
3942
|
+
}
|
|
3943
|
+
/**
|
|
3944
|
+
* Spec B Phase 4 (closes P1-IMP-2): a single resolve() + def lookup
|
|
3945
|
+
* for callers that need both. Replaces the previous pattern of
|
|
3946
|
+
* `resolveDefinition(useCase) + resolve(useCase)` which produced two
|
|
3947
|
+
* RoutingDecision emissions per dispatch — doubling routing-decision
|
|
3948
|
+
* log volume now that Phase 4 emits.
|
|
3743
3949
|
*
|
|
3744
|
-
* - `
|
|
3745
|
-
*
|
|
3746
|
-
|
|
3747
|
-
|
|
3950
|
+
* Identity-equal `BackendDef` (no copy) so callers relying on
|
|
3951
|
+
* reference equality (SC21) continue to work.
|
|
3952
|
+
*/
|
|
3953
|
+
resolveDecisionAndDef(useCase, opts) {
|
|
3954
|
+
const decision = this.resolve(useCase, opts);
|
|
3955
|
+
const def = this.backends[decision.backendName];
|
|
3956
|
+
if (!def) {
|
|
3957
|
+
throw new Error(
|
|
3958
|
+
`BackendRouter.resolveDecisionAndDef: routing target '${decision.backendName}' is not in backends (useCase=${JSON.stringify(useCase)}).`
|
|
3959
|
+
);
|
|
3960
|
+
}
|
|
3961
|
+
return { decision, def };
|
|
3962
|
+
}
|
|
3963
|
+
/**
|
|
3964
|
+
* The pre-Spec-B resolution helper: returns the configured
|
|
3965
|
+
* {@link RoutingValue} for tier/intelligence/isolation/maintenance/chat
|
|
3966
|
+
* use cases (or `undefined` for skill/mode use cases, which are owned
|
|
3967
|
+
* by the per-skill / per-mode steps in {@link resolve}). Returning
|
|
3968
|
+
* `undefined` lets the caller fall through to `routing.default`.
|
|
3748
3969
|
*/
|
|
3749
|
-
|
|
3970
|
+
resolveExistingUseCase(useCase) {
|
|
3750
3971
|
switch (useCase.kind) {
|
|
3751
3972
|
case "tier": {
|
|
3752
|
-
const
|
|
3753
|
-
return
|
|
3973
|
+
const tierMap = this.routing;
|
|
3974
|
+
return tierMap[useCase.tier];
|
|
3754
3975
|
}
|
|
3755
3976
|
case "intelligence": {
|
|
3756
3977
|
const intel = this.routing.intelligence;
|
|
3757
|
-
return intel?.[useCase.layer]
|
|
3978
|
+
return intel?.[useCase.layer];
|
|
3758
3979
|
}
|
|
3759
3980
|
case "isolation": {
|
|
3760
3981
|
const iso = this.routing.isolation;
|
|
3761
|
-
return iso?.[useCase.tier]
|
|
3982
|
+
return iso?.[useCase.tier];
|
|
3762
3983
|
}
|
|
3763
3984
|
case "maintenance":
|
|
3764
3985
|
case "chat":
|
|
3765
|
-
return
|
|
3986
|
+
return void 0;
|
|
3987
|
+
case "skill":
|
|
3988
|
+
case "mode":
|
|
3989
|
+
return void 0;
|
|
3766
3990
|
}
|
|
3767
3991
|
}
|
|
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
|
-
);
|
|
3780
|
-
}
|
|
3781
|
-
return def;
|
|
3782
|
-
}
|
|
3783
3992
|
validateReferences() {
|
|
3784
3993
|
const known = new Set(Object.keys(this.backends));
|
|
3785
3994
|
const missing = [];
|
|
3786
|
-
const check = (
|
|
3787
|
-
if (
|
|
3995
|
+
const check = (label, value) => {
|
|
3996
|
+
if (value === void 0) return;
|
|
3997
|
+
for (const name of toArray(value)) {
|
|
3998
|
+
if (!known.has(name)) missing.push({ path: label, name });
|
|
3999
|
+
}
|
|
3788
4000
|
};
|
|
3789
4001
|
check("default", this.routing.default);
|
|
3790
4002
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3796,8 +4008,14 @@ var BackendRouter = class {
|
|
|
3796
4008
|
check("isolation.none", this.routing.isolation?.none);
|
|
3797
4009
|
check("isolation.container", this.routing.isolation?.container);
|
|
3798
4010
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
4011
|
+
for (const [skill, value] of Object.entries(this.routing.skills ?? {})) {
|
|
4012
|
+
check(`skills.${skill}`, value);
|
|
4013
|
+
}
|
|
4014
|
+
for (const [mode, value] of Object.entries(this.routing.modes ?? {})) {
|
|
4015
|
+
check(`modes.${mode}`, value);
|
|
4016
|
+
}
|
|
3799
4017
|
if (missing.length > 0) {
|
|
3800
|
-
const detail = missing.map(({ path:
|
|
4018
|
+
const detail = missing.map(({ path: path24, name }) => `routing.${path24} -> '${name}'`).join("; ");
|
|
3801
4019
|
const known_ = [...known].join(", ") || "(none)";
|
|
3802
4020
|
throw new Error(
|
|
3803
4021
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3814,11 +4032,11 @@ import {
|
|
|
3814
4032
|
Ok as Ok10,
|
|
3815
4033
|
Err as Err7
|
|
3816
4034
|
} from "@harness-engineering/types";
|
|
3817
|
-
function resolveExitCode(code, command,
|
|
4035
|
+
function resolveExitCode(code, command, resolve8) {
|
|
3818
4036
|
if (code === 0) {
|
|
3819
|
-
|
|
4037
|
+
resolve8(Ok10(void 0));
|
|
3820
4038
|
} else {
|
|
3821
|
-
|
|
4039
|
+
resolve8(
|
|
3822
4040
|
Err7({
|
|
3823
4041
|
category: "agent_not_found",
|
|
3824
4042
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3826,8 +4044,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3826
4044
|
);
|
|
3827
4045
|
}
|
|
3828
4046
|
}
|
|
3829
|
-
function resolveSpawnError(command,
|
|
3830
|
-
|
|
4047
|
+
function resolveSpawnError(command, resolve8) {
|
|
4048
|
+
resolve8(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3831
4049
|
}
|
|
3832
4050
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3833
4051
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4140,10 +4358,10 @@ var ClaudeBackend = class {
|
|
|
4140
4358
|
errRl.close();
|
|
4141
4359
|
}
|
|
4142
4360
|
if (exitCode === null) {
|
|
4143
|
-
await new Promise((
|
|
4361
|
+
await new Promise((resolve8) => {
|
|
4144
4362
|
child.on("exit", (code) => {
|
|
4145
4363
|
exitCode = code;
|
|
4146
|
-
|
|
4364
|
+
resolve8(null);
|
|
4147
4365
|
});
|
|
4148
4366
|
});
|
|
4149
4367
|
}
|
|
@@ -4165,10 +4383,10 @@ var ClaudeBackend = class {
|
|
|
4165
4383
|
return Ok10(void 0);
|
|
4166
4384
|
}
|
|
4167
4385
|
async healthCheck() {
|
|
4168
|
-
return new Promise((
|
|
4386
|
+
return new Promise((resolve8) => {
|
|
4169
4387
|
const child = spawn2(this.command, ["--version"]);
|
|
4170
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4171
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4388
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve8));
|
|
4389
|
+
child.on("error", () => resolveSpawnError(this.command, resolve8));
|
|
4172
4390
|
});
|
|
4173
4391
|
}
|
|
4174
4392
|
};
|
|
@@ -4791,7 +5009,7 @@ var PiBackend = class {
|
|
|
4791
5009
|
} else {
|
|
4792
5010
|
resolvedModelName = this.config.model;
|
|
4793
5011
|
}
|
|
4794
|
-
const piSdk = await import("@
|
|
5012
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4795
5013
|
const model = buildLocalModel({
|
|
4796
5014
|
model: resolvedModelName,
|
|
4797
5015
|
endpoint: this.config.endpoint,
|
|
@@ -4946,7 +5164,7 @@ var PiBackend = class {
|
|
|
4946
5164
|
}
|
|
4947
5165
|
async healthCheck() {
|
|
4948
5166
|
try {
|
|
4949
|
-
await import("@
|
|
5167
|
+
await import("@earendil-works/pi-coding-agent");
|
|
4950
5168
|
return Ok15(void 0);
|
|
4951
5169
|
} catch (err) {
|
|
4952
5170
|
return Err12({
|
|
@@ -5100,14 +5318,14 @@ var SshBackend = class {
|
|
|
5100
5318
|
async healthCheck() {
|
|
5101
5319
|
const args = [...this.buildSshArgs()];
|
|
5102
5320
|
args[args.length - 1] = "true";
|
|
5103
|
-
return new Promise((
|
|
5321
|
+
return new Promise((resolve8) => {
|
|
5104
5322
|
let child;
|
|
5105
5323
|
try {
|
|
5106
5324
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5107
5325
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5108
5326
|
});
|
|
5109
5327
|
} catch (err) {
|
|
5110
|
-
|
|
5328
|
+
resolve8(
|
|
5111
5329
|
Err13({
|
|
5112
5330
|
category: "agent_not_found",
|
|
5113
5331
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5128,9 +5346,9 @@ var SshBackend = class {
|
|
|
5128
5346
|
child.on("close", (code) => {
|
|
5129
5347
|
clearTimeout(timer);
|
|
5130
5348
|
if (code === 0) {
|
|
5131
|
-
|
|
5349
|
+
resolve8(Ok16(void 0));
|
|
5132
5350
|
} else {
|
|
5133
|
-
|
|
5351
|
+
resolve8(
|
|
5134
5352
|
Err13({
|
|
5135
5353
|
category: "agent_not_found",
|
|
5136
5354
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5140,7 +5358,7 @@ var SshBackend = class {
|
|
|
5140
5358
|
});
|
|
5141
5359
|
child.on("error", (err) => {
|
|
5142
5360
|
clearTimeout(timer);
|
|
5143
|
-
|
|
5361
|
+
resolve8(Err13({ category: "agent_not_found", message: err.message }));
|
|
5144
5362
|
});
|
|
5145
5363
|
});
|
|
5146
5364
|
}
|
|
@@ -5188,13 +5406,13 @@ async function* readLines(stream) {
|
|
|
5188
5406
|
if (buffer.length > 0) yield buffer;
|
|
5189
5407
|
}
|
|
5190
5408
|
function waitForExit(child) {
|
|
5191
|
-
return new Promise((
|
|
5409
|
+
return new Promise((resolve8) => {
|
|
5192
5410
|
if (child.exitCode !== null) {
|
|
5193
|
-
|
|
5411
|
+
resolve8(child.exitCode);
|
|
5194
5412
|
return;
|
|
5195
5413
|
}
|
|
5196
|
-
child.once("close", (code) =>
|
|
5197
|
-
child.once("error", () =>
|
|
5414
|
+
child.once("close", (code) => resolve8(code));
|
|
5415
|
+
child.once("error", () => resolve8(null));
|
|
5198
5416
|
});
|
|
5199
5417
|
}
|
|
5200
5418
|
|
|
@@ -5384,14 +5602,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5384
5602
|
return out;
|
|
5385
5603
|
}
|
|
5386
5604
|
runOneShot(binary, args) {
|
|
5387
|
-
return new Promise((
|
|
5605
|
+
return new Promise((resolve8) => {
|
|
5388
5606
|
let child;
|
|
5389
5607
|
try {
|
|
5390
5608
|
child = this.spawnImpl(binary, args, {
|
|
5391
5609
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5392
5610
|
});
|
|
5393
5611
|
} catch (err) {
|
|
5394
|
-
|
|
5612
|
+
resolve8(
|
|
5395
5613
|
Err14({
|
|
5396
5614
|
category: "agent_not_found",
|
|
5397
5615
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5416,9 +5634,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5416
5634
|
child.on("close", (code) => {
|
|
5417
5635
|
clearTimeout(timer);
|
|
5418
5636
|
if (code === 0) {
|
|
5419
|
-
|
|
5637
|
+
resolve8(Ok17(stdout));
|
|
5420
5638
|
} else {
|
|
5421
|
-
|
|
5639
|
+
resolve8(
|
|
5422
5640
|
Err14({
|
|
5423
5641
|
category: "response_error",
|
|
5424
5642
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5428,7 +5646,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5428
5646
|
});
|
|
5429
5647
|
child.on("error", (err) => {
|
|
5430
5648
|
clearTimeout(timer);
|
|
5431
|
-
|
|
5649
|
+
resolve8(Err14({ category: "agent_not_found", message: err.message }));
|
|
5432
5650
|
});
|
|
5433
5651
|
});
|
|
5434
5652
|
}
|
|
@@ -5488,13 +5706,13 @@ async function* readLines2(stream) {
|
|
|
5488
5706
|
if (buffer.length > 0) yield buffer;
|
|
5489
5707
|
}
|
|
5490
5708
|
function waitForExit2(child) {
|
|
5491
|
-
return new Promise((
|
|
5709
|
+
return new Promise((resolve8) => {
|
|
5492
5710
|
if (child.exitCode !== null) {
|
|
5493
|
-
|
|
5711
|
+
resolve8(child.exitCode);
|
|
5494
5712
|
return;
|
|
5495
5713
|
}
|
|
5496
|
-
child.once("close", (code) =>
|
|
5497
|
-
child.once("error", () =>
|
|
5714
|
+
child.once("close", (code) => resolve8(code));
|
|
5715
|
+
child.once("error", () => resolve8(null));
|
|
5498
5716
|
});
|
|
5499
5717
|
}
|
|
5500
5718
|
|
|
@@ -5694,13 +5912,13 @@ var ContainerBackend = class {
|
|
|
5694
5912
|
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
5695
5913
|
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5696
5914
|
function dockerExec(args) {
|
|
5697
|
-
return new Promise((
|
|
5915
|
+
return new Promise((resolve8, reject) => {
|
|
5698
5916
|
execFile3("docker", args, (error, stdout) => {
|
|
5699
5917
|
if (error) {
|
|
5700
5918
|
reject(error);
|
|
5701
5919
|
return;
|
|
5702
5920
|
}
|
|
5703
|
-
|
|
5921
|
+
resolve8(stdout.trim());
|
|
5704
5922
|
});
|
|
5705
5923
|
});
|
|
5706
5924
|
}
|
|
@@ -5759,11 +5977,11 @@ var DockerRuntime = class {
|
|
|
5759
5977
|
} finally {
|
|
5760
5978
|
rl.close();
|
|
5761
5979
|
}
|
|
5762
|
-
const exitCode = await new Promise((
|
|
5980
|
+
const exitCode = await new Promise((resolve8) => {
|
|
5763
5981
|
if (child.exitCode !== null) {
|
|
5764
|
-
|
|
5982
|
+
resolve8(child.exitCode);
|
|
5765
5983
|
} else {
|
|
5766
|
-
child.on("exit", (code) =>
|
|
5984
|
+
child.on("exit", (code) => resolve8(code ?? 1));
|
|
5767
5985
|
}
|
|
5768
5986
|
});
|
|
5769
5987
|
return exitCode;
|
|
@@ -5822,13 +6040,13 @@ var EnvSecretBackend = class {
|
|
|
5822
6040
|
import { execFile as execFile4 } from "child_process";
|
|
5823
6041
|
import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
|
|
5824
6042
|
function opExec(args) {
|
|
5825
|
-
return new Promise((
|
|
6043
|
+
return new Promise((resolve8, reject) => {
|
|
5826
6044
|
execFile4("op", args, (error, stdout) => {
|
|
5827
6045
|
if (error) {
|
|
5828
6046
|
reject(error);
|
|
5829
6047
|
return;
|
|
5830
6048
|
}
|
|
5831
|
-
|
|
6049
|
+
resolve8(stdout.trim());
|
|
5832
6050
|
});
|
|
5833
6051
|
});
|
|
5834
6052
|
}
|
|
@@ -5871,13 +6089,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5871
6089
|
import { execFile as execFile5 } from "child_process";
|
|
5872
6090
|
import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
|
|
5873
6091
|
function vaultExec(args, env) {
|
|
5874
|
-
return new Promise((
|
|
6092
|
+
return new Promise((resolve8, reject) => {
|
|
5875
6093
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5876
6094
|
if (error) {
|
|
5877
6095
|
reject(error);
|
|
5878
6096
|
return;
|
|
5879
6097
|
}
|
|
5880
|
-
|
|
6098
|
+
resolve8(stdout.trim());
|
|
5881
6099
|
});
|
|
5882
6100
|
});
|
|
5883
6101
|
}
|
|
@@ -5952,7 +6170,11 @@ var OrchestratorBackendFactory = class {
|
|
|
5952
6170
|
opts;
|
|
5953
6171
|
constructor(opts) {
|
|
5954
6172
|
this.opts = opts;
|
|
5955
|
-
this.router = new BackendRouter({
|
|
6173
|
+
this.router = new BackendRouter({
|
|
6174
|
+
backends: opts.backends,
|
|
6175
|
+
routing: opts.routing,
|
|
6176
|
+
...opts.decisionBus !== void 0 ? { decisionBus: opts.decisionBus } : {}
|
|
6177
|
+
});
|
|
5956
6178
|
}
|
|
5957
6179
|
/**
|
|
5958
6180
|
* Resolve `useCase` to a backend name, materialize a fresh
|
|
@@ -5971,12 +6193,21 @@ var OrchestratorBackendFactory = class {
|
|
|
5971
6193
|
* is `undefined` for pure-modern configs. Threading the routed name
|
|
5972
6194
|
* through dispatch eliminates that gap.
|
|
5973
6195
|
*/
|
|
5974
|
-
resolveName(useCase) {
|
|
5975
|
-
return this.router.resolve(useCase);
|
|
6196
|
+
resolveName(useCase, opts) {
|
|
6197
|
+
return this.router.resolve(useCase, opts).backendName;
|
|
5976
6198
|
}
|
|
5977
|
-
|
|
5978
|
-
|
|
5979
|
-
|
|
6199
|
+
/**
|
|
6200
|
+
* Spec B Phase 1: expose the underlying router for callers that need
|
|
6201
|
+
* it directly (e.g., {@link buildIntelligencePipeline} for the
|
|
6202
|
+
* I1 SEL/PESL comparison fix). Read-only access; consumers must not
|
|
6203
|
+
* mutate router state.
|
|
6204
|
+
*/
|
|
6205
|
+
getRouter() {
|
|
6206
|
+
return this.router;
|
|
6207
|
+
}
|
|
6208
|
+
forUseCase(useCase, opts) {
|
|
6209
|
+
const { def, decision } = this.router.resolveDecisionAndDef(useCase, opts);
|
|
6210
|
+
const name = decision.backendName;
|
|
5980
6211
|
let backend;
|
|
5981
6212
|
const createOpts = this.opts.cacheMetrics ? { cacheMetrics: this.opts.cacheMetrics } : {};
|
|
5982
6213
|
if ((def.type === "local" || def.type === "pi") && this.opts.getResolverModelFor) {
|
|
@@ -6149,15 +6380,14 @@ function buildClaudeCliProvider(def, args, layerModel) {
|
|
|
6149
6380
|
|
|
6150
6381
|
// src/agent/intelligence-factory.ts
|
|
6151
6382
|
function buildIntelligencePipeline(deps) {
|
|
6152
|
-
const { config } = deps;
|
|
6383
|
+
const { config, router } = deps;
|
|
6153
6384
|
const intel = config.intelligence;
|
|
6154
6385
|
if (!intel?.enabled) return null;
|
|
6155
6386
|
const selProvider = buildAnalysisProviderForLayer("sel", deps);
|
|
6156
6387
|
if (!selProvider) return null;
|
|
6157
|
-
const
|
|
6158
|
-
const
|
|
6159
|
-
const
|
|
6160
|
-
const peslProvider = peslName !== void 0 && peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6388
|
+
const peslName = router.resolve({ kind: "intelligence", layer: "pesl" }).backendName;
|
|
6389
|
+
const selName = router.resolve({ kind: "intelligence", layer: "sel" }).backendName;
|
|
6390
|
+
const peslProvider = peslName !== selName ? buildAnalysisProviderForLayer("pesl", deps) : null;
|
|
6161
6391
|
const peslModel = intel.models?.pesl ?? config.agent.model;
|
|
6162
6392
|
const graphStore = new GraphStore();
|
|
6163
6393
|
const pipeline = new IntelligencePipeline(selProvider, graphStore, {
|
|
@@ -6174,7 +6404,7 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6174
6404
|
const layerModel = layer === "sel" ? intel.models?.sel : intel.models?.pesl;
|
|
6175
6405
|
return buildExplicitProvider(intel.provider, layerModel ?? config.agent.model, config);
|
|
6176
6406
|
}
|
|
6177
|
-
const routed = resolveRoutedBackend(layer,
|
|
6407
|
+
const routed = resolveRoutedBackend(layer, deps);
|
|
6178
6408
|
if (!routed) return null;
|
|
6179
6409
|
const { name, def } = routed;
|
|
6180
6410
|
const resolver = localResolvers.get(name);
|
|
@@ -6199,20 +6429,26 @@ function buildAnalysisProviderForLayer(layer, deps) {
|
|
|
6199
6429
|
logger
|
|
6200
6430
|
});
|
|
6201
6431
|
}
|
|
6202
|
-
function resolveRoutedBackend(layer,
|
|
6203
|
-
const
|
|
6432
|
+
function resolveRoutedBackend(layer, deps) {
|
|
6433
|
+
const { config, router, logger } = deps;
|
|
6204
6434
|
const backends = config.agent.backends;
|
|
6205
|
-
if (!
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6435
|
+
if (!backends || !router) return null;
|
|
6436
|
+
try {
|
|
6437
|
+
const decision = router.resolve({ kind: "intelligence", layer });
|
|
6438
|
+
const def = backends[decision.backendName];
|
|
6439
|
+
if (!def) {
|
|
6440
|
+
logger.warn(
|
|
6441
|
+
`Intelligence pipeline: routed backend '${decision.backendName}' for layer '${layer}' is not in agent.backends.`
|
|
6442
|
+
);
|
|
6443
|
+
return null;
|
|
6444
|
+
}
|
|
6445
|
+
return { name: decision.backendName, def };
|
|
6446
|
+
} catch (err) {
|
|
6210
6447
|
logger.warn(
|
|
6211
|
-
`Intelligence pipeline:
|
|
6448
|
+
`Intelligence pipeline: router could not resolve intelligence.${layer}; intelligence disabled. error=${String(err)}`
|
|
6212
6449
|
);
|
|
6213
6450
|
return null;
|
|
6214
6451
|
}
|
|
6215
|
-
return { name, def };
|
|
6216
6452
|
}
|
|
6217
6453
|
function buildExplicitProvider(provider, selModel, config) {
|
|
6218
6454
|
if (provider.kind === "anthropic") {
|
|
@@ -6247,9 +6483,104 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6247
6483
|
});
|
|
6248
6484
|
}
|
|
6249
6485
|
|
|
6486
|
+
// src/routing/decision-bus.ts
|
|
6487
|
+
var RoutingDecisionBus = class {
|
|
6488
|
+
ringBuffer = [];
|
|
6489
|
+
listeners = /* @__PURE__ */ new Set();
|
|
6490
|
+
capacity;
|
|
6491
|
+
logger;
|
|
6492
|
+
constructor(opts) {
|
|
6493
|
+
this.capacity = opts?.capacity ?? 500;
|
|
6494
|
+
this.logger = opts?.logger;
|
|
6495
|
+
}
|
|
6496
|
+
emit(decision) {
|
|
6497
|
+
this.ringBuffer.push(decision);
|
|
6498
|
+
if (this.ringBuffer.length > this.capacity) {
|
|
6499
|
+
this.ringBuffer.shift();
|
|
6500
|
+
}
|
|
6501
|
+
if (this.logger) {
|
|
6502
|
+
this.logger.info("routing-decision", {
|
|
6503
|
+
useCase: decision.useCase,
|
|
6504
|
+
backendName: decision.backendName,
|
|
6505
|
+
resolutionPathLength: decision.resolutionPath.length,
|
|
6506
|
+
durationMs: decision.durationMs
|
|
6507
|
+
});
|
|
6508
|
+
}
|
|
6509
|
+
for (const listener of this.listeners) {
|
|
6510
|
+
try {
|
|
6511
|
+
listener(decision);
|
|
6512
|
+
} catch (err) {
|
|
6513
|
+
if (this.logger) {
|
|
6514
|
+
this.logger.warn("RoutingDecisionBus subscriber threw", {
|
|
6515
|
+
error: String(err)
|
|
6516
|
+
});
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
}
|
|
6520
|
+
}
|
|
6521
|
+
recent(filter) {
|
|
6522
|
+
let out = this.ringBuffer.slice();
|
|
6523
|
+
if (filter?.skillName !== void 0) {
|
|
6524
|
+
out = out.filter(
|
|
6525
|
+
(d) => d.useCase.kind === "skill" && d.useCase.skillName === filter.skillName
|
|
6526
|
+
);
|
|
6527
|
+
}
|
|
6528
|
+
if (filter?.mode !== void 0) {
|
|
6529
|
+
const m = filter.mode;
|
|
6530
|
+
out = out.filter(
|
|
6531
|
+
(d) => d.useCase.kind === "mode" && d.useCase.cognitiveMode === m || d.useCase.kind === "skill" && d.useCase.cognitiveMode === m
|
|
6532
|
+
);
|
|
6533
|
+
}
|
|
6534
|
+
if (filter?.backendName !== void 0) {
|
|
6535
|
+
out = out.filter((d) => d.backendName === filter.backendName);
|
|
6536
|
+
}
|
|
6537
|
+
if (filter?.limit !== void 0) {
|
|
6538
|
+
out = out.slice(-filter.limit).reverse();
|
|
6539
|
+
} else {
|
|
6540
|
+
out = out.reverse();
|
|
6541
|
+
}
|
|
6542
|
+
return out;
|
|
6543
|
+
}
|
|
6544
|
+
subscribe(listener) {
|
|
6545
|
+
this.listeners.add(listener);
|
|
6546
|
+
return () => {
|
|
6547
|
+
this.listeners.delete(listener);
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
/**
|
|
6551
|
+
* Spec B Phase 5 (review-S2 fix): release all subscriber references so
|
|
6552
|
+
* teardown can complete without anchoring closures. Called from
|
|
6553
|
+
* `Orchestrator.stop()` before nulling the bus reference. The bus
|
|
6554
|
+
* remains usable after clear — `subscribe()` works as normal.
|
|
6555
|
+
*/
|
|
6556
|
+
clearListeners() {
|
|
6557
|
+
this.listeners.clear();
|
|
6558
|
+
}
|
|
6559
|
+
};
|
|
6560
|
+
|
|
6561
|
+
// src/agent/triage-skill-mapping.ts
|
|
6562
|
+
function resolveSkillForTriage(triageSkill, catalog) {
|
|
6563
|
+
const expected = `harness-${triageSkill}`;
|
|
6564
|
+
const match = catalog.find((e) => e.name === expected);
|
|
6565
|
+
if (!match) return void 0;
|
|
6566
|
+
return match.cognitiveMode !== void 0 ? { name: match.name, cognitiveMode: match.cognitiveMode } : { name: match.name };
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6569
|
+
// src/agent/use-case-builder.ts
|
|
6570
|
+
function buildRoutingUseCase(issue, backendParam, catalog) {
|
|
6571
|
+
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
6572
|
+
const decision = triageIssue(issue, {});
|
|
6573
|
+
const resolved = resolveSkillForTriage(decision.skill, catalog);
|
|
6574
|
+
if (resolved) {
|
|
6575
|
+
return resolved.cognitiveMode !== void 0 ? { kind: "skill", skillName: resolved.name, cognitiveMode: resolved.cognitiveMode } : { kind: "skill", skillName: resolved.name };
|
|
6576
|
+
}
|
|
6577
|
+
const tier = detectScopeTier(issue, artifactPresenceFromIssue(issue));
|
|
6578
|
+
return { kind: "tier", tier };
|
|
6579
|
+
}
|
|
6580
|
+
|
|
6250
6581
|
// src/server/http.ts
|
|
6251
6582
|
import * as http from "http";
|
|
6252
|
-
import * as
|
|
6583
|
+
import * as path17 from "path";
|
|
6253
6584
|
import { assertPortUsable } from "@harness-engineering/core";
|
|
6254
6585
|
|
|
6255
6586
|
// src/server/websocket.ts
|
|
@@ -6312,7 +6643,7 @@ import { z as z3 } from "zod";
|
|
|
6312
6643
|
// src/server/utils.ts
|
|
6313
6644
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6314
6645
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6315
|
-
return new Promise((
|
|
6646
|
+
return new Promise((resolve8, reject) => {
|
|
6316
6647
|
let body = "";
|
|
6317
6648
|
let bytes = 0;
|
|
6318
6649
|
req.on("data", (chunk) => {
|
|
@@ -6324,7 +6655,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6324
6655
|
}
|
|
6325
6656
|
body += String(chunk);
|
|
6326
6657
|
});
|
|
6327
|
-
req.on("end", () =>
|
|
6658
|
+
req.on("end", () => resolve8(body));
|
|
6328
6659
|
req.on("error", reject);
|
|
6329
6660
|
});
|
|
6330
6661
|
}
|
|
@@ -6445,8 +6776,8 @@ function handleV1InteractionsResolveRoute(req, res, queue) {
|
|
|
6445
6776
|
|
|
6446
6777
|
// src/server/routes/plans.ts
|
|
6447
6778
|
import { z as z5 } from "zod";
|
|
6448
|
-
import * as
|
|
6449
|
-
import * as
|
|
6779
|
+
import * as fs10 from "fs/promises";
|
|
6780
|
+
import * as path11 from "path";
|
|
6450
6781
|
var PlanWriteSchema = z5.object({
|
|
6451
6782
|
filename: z5.string().min(1),
|
|
6452
6783
|
content: z5.string().min(1)
|
|
@@ -6466,7 +6797,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6466
6797
|
return;
|
|
6467
6798
|
}
|
|
6468
6799
|
const parsed = result.data;
|
|
6469
|
-
const basename3 =
|
|
6800
|
+
const basename3 = path11.basename(parsed.filename);
|
|
6470
6801
|
if (basename3 !== parsed.filename || !basename3.endsWith(".md")) {
|
|
6471
6802
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
6472
6803
|
res.end(
|
|
@@ -6474,9 +6805,9 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
6474
6805
|
);
|
|
6475
6806
|
return;
|
|
6476
6807
|
}
|
|
6477
|
-
await
|
|
6478
|
-
const filePath =
|
|
6479
|
-
await
|
|
6808
|
+
await fs10.mkdir(plansDir, { recursive: true });
|
|
6809
|
+
const filePath = path11.join(plansDir, basename3);
|
|
6810
|
+
await fs10.writeFile(filePath, parsed.content, "utf-8");
|
|
6480
6811
|
res.writeHead(201, { "Content-Type": "application/json" });
|
|
6481
6812
|
res.end(JSON.stringify({ ok: true, filename: basename3 }));
|
|
6482
6813
|
} catch {
|
|
@@ -6851,8 +7182,8 @@ function handleAnalyzeRoute(req, res, pipeline) {
|
|
|
6851
7182
|
}
|
|
6852
7183
|
|
|
6853
7184
|
// src/server/routes/roadmap-actions.ts
|
|
6854
|
-
import * as
|
|
6855
|
-
import * as
|
|
7185
|
+
import * as fs11 from "fs/promises";
|
|
7186
|
+
import * as path12 from "path";
|
|
6856
7187
|
import {
|
|
6857
7188
|
parseRoadmap as parseRoadmap2,
|
|
6858
7189
|
serializeRoadmap as serializeRoadmap2,
|
|
@@ -6888,7 +7219,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6888
7219
|
sendJSON2(res, 503, { error: "Roadmap path not configured" });
|
|
6889
7220
|
return;
|
|
6890
7221
|
}
|
|
6891
|
-
const projectRoot =
|
|
7222
|
+
const projectRoot = path12.dirname(path12.dirname(roadmapPath));
|
|
6892
7223
|
const mode = loadProjectRoadmapMode(projectRoot);
|
|
6893
7224
|
if (mode === "file-less") {
|
|
6894
7225
|
const trackerCfg = loadTrackerClientConfigFromProject(projectRoot);
|
|
@@ -6941,7 +7272,7 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6941
7272
|
sendJSON2(res, 400, { error: "Title must not contain newlines or markdown headings" });
|
|
6942
7273
|
return;
|
|
6943
7274
|
}
|
|
6944
|
-
const content = await
|
|
7275
|
+
const content = await fs11.readFile(roadmapPath, "utf-8");
|
|
6945
7276
|
const roadmapResult = parseRoadmap2(content);
|
|
6946
7277
|
if (!roadmapResult.ok) {
|
|
6947
7278
|
sendJSON2(res, 500, { error: "Failed to parse roadmap file" });
|
|
@@ -6972,8 +7303,8 @@ function handleRoadmapActionsRoute(req, res, roadmapPath) {
|
|
|
6972
7303
|
roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
|
|
6973
7304
|
const tmpPath = roadmapPath + ".tmp";
|
|
6974
7305
|
const serialized = serializeRoadmap2(roadmap);
|
|
6975
|
-
await
|
|
6976
|
-
await
|
|
7306
|
+
await fs11.writeFile(tmpPath, serialized, "utf-8");
|
|
7307
|
+
await fs11.rename(tmpPath, roadmapPath);
|
|
6977
7308
|
sendJSON2(res, 201, { ok: true, featureName: parsed.title });
|
|
6978
7309
|
} catch (err) {
|
|
6979
7310
|
const msg = err instanceof Error ? err.message : "Failed to append to roadmap";
|
|
@@ -7460,88 +7791,779 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
7460
7791
|
return false;
|
|
7461
7792
|
}
|
|
7462
7793
|
|
|
7463
|
-
// src/server/routes/
|
|
7464
|
-
import * as fs11 from "fs/promises";
|
|
7465
|
-
import * as path11 from "path";
|
|
7794
|
+
// src/server/routes/v1/proposals.ts
|
|
7466
7795
|
import { z as z13 } from "zod";
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7796
|
+
import {
|
|
7797
|
+
getProposal as getProposal3,
|
|
7798
|
+
listProposals,
|
|
7799
|
+
updateProposal as updateProposal3,
|
|
7800
|
+
ProposalNotFoundError as ProposalNotFoundError3
|
|
7801
|
+
} from "@harness-engineering/core";
|
|
7802
|
+
import {
|
|
7803
|
+
EditProposalInputSchema
|
|
7804
|
+
} from "@harness-engineering/types";
|
|
7805
|
+
|
|
7806
|
+
// src/proposals/gate.ts
|
|
7807
|
+
import { parse as parseYaml2 } from "yaml";
|
|
7808
|
+
import {
|
|
7809
|
+
getProposal,
|
|
7810
|
+
updateProposal,
|
|
7811
|
+
ProposalNotFoundError
|
|
7812
|
+
} from "@harness-engineering/core";
|
|
7813
|
+
var GateRunError = class extends Error {
|
|
7814
|
+
constructor(message) {
|
|
7815
|
+
super(message);
|
|
7816
|
+
this.name = "GateRunError";
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
7820
|
+
function checkSkillYaml(yaml) {
|
|
7821
|
+
const findings = [];
|
|
7822
|
+
let doc;
|
|
7823
|
+
try {
|
|
7824
|
+
doc = parseYaml2(yaml);
|
|
7825
|
+
} catch (err) {
|
|
7826
|
+
findings.push({
|
|
7827
|
+
severity: "error",
|
|
7828
|
+
title: "skill.yaml does not parse",
|
|
7829
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
7830
|
+
});
|
|
7831
|
+
return findings;
|
|
7832
|
+
}
|
|
7833
|
+
if (!doc || typeof doc !== "object") {
|
|
7834
|
+
findings.push({
|
|
7835
|
+
severity: "error",
|
|
7836
|
+
title: "skill.yaml top-level is not a mapping",
|
|
7837
|
+
detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
|
|
7838
|
+
});
|
|
7839
|
+
return findings;
|
|
7840
|
+
}
|
|
7841
|
+
const obj = doc;
|
|
7842
|
+
if (typeof obj["name"] !== "string") {
|
|
7843
|
+
findings.push({
|
|
7844
|
+
severity: "error",
|
|
7845
|
+
title: "skill.yaml missing `name`",
|
|
7846
|
+
detail: "Every skill must declare its kebab-case name."
|
|
7847
|
+
});
|
|
7848
|
+
}
|
|
7849
|
+
if (typeof obj["version"] !== "string") {
|
|
7850
|
+
findings.push({
|
|
7851
|
+
severity: "error",
|
|
7852
|
+
title: "skill.yaml missing `version`",
|
|
7853
|
+
detail: "Every skill must declare a semver version string."
|
|
7854
|
+
});
|
|
7855
|
+
}
|
|
7856
|
+
if (typeof obj["description"] !== "string") {
|
|
7857
|
+
findings.push({
|
|
7858
|
+
severity: "warning",
|
|
7859
|
+
title: "skill.yaml missing `description`",
|
|
7860
|
+
detail: "Description is strongly recommended for discoverability."
|
|
7861
|
+
});
|
|
7862
|
+
}
|
|
7863
|
+
return findings;
|
|
7477
7864
|
}
|
|
7478
|
-
function
|
|
7479
|
-
const
|
|
7480
|
-
|
|
7481
|
-
|
|
7865
|
+
function checkSkillMd(md) {
|
|
7866
|
+
const findings = [];
|
|
7867
|
+
if (md.trim().length < 40) {
|
|
7868
|
+
findings.push({
|
|
7869
|
+
severity: "error",
|
|
7870
|
+
title: "SKILL.md is too short",
|
|
7871
|
+
detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
|
|
7872
|
+
});
|
|
7873
|
+
}
|
|
7874
|
+
if (!/^#\s+\S/m.test(md)) {
|
|
7875
|
+
findings.push({
|
|
7876
|
+
severity: "warning",
|
|
7877
|
+
title: "SKILL.md has no top-level heading",
|
|
7878
|
+
detail: "Convention: open SKILL.md with `# <Skill Name>`."
|
|
7879
|
+
});
|
|
7880
|
+
}
|
|
7881
|
+
return findings;
|
|
7482
7882
|
}
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
const content = await fs11.readFile(
|
|
7491
|
-
path11.join(sessionsDir, entry.name, "session.json"),
|
|
7492
|
-
"utf-8"
|
|
7493
|
-
);
|
|
7494
|
-
sessions.push(JSON.parse(content));
|
|
7495
|
-
} catch {
|
|
7496
|
-
}
|
|
7883
|
+
function checkName(name) {
|
|
7884
|
+
if (SKILL_NAME_RE.test(name)) return [];
|
|
7885
|
+
return [
|
|
7886
|
+
{
|
|
7887
|
+
severity: "error",
|
|
7888
|
+
title: "skill name violates the kebab-case rule",
|
|
7889
|
+
detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
|
|
7497
7890
|
}
|
|
7498
|
-
|
|
7499
|
-
|
|
7891
|
+
];
|
|
7892
|
+
}
|
|
7893
|
+
function checkDiff(diff) {
|
|
7894
|
+
const findings = [];
|
|
7895
|
+
if (!diff.includes("---") || !diff.includes("+++")) {
|
|
7896
|
+
findings.push({
|
|
7897
|
+
severity: "error",
|
|
7898
|
+
title: "Refinement diff is not in unified-diff format",
|
|
7899
|
+
detail: "Diffs must include both `---` and `+++` headers."
|
|
7900
|
+
});
|
|
7901
|
+
}
|
|
7902
|
+
if (!/^@@\s/m.test(diff)) {
|
|
7903
|
+
findings.push({
|
|
7904
|
+
severity: "warning",
|
|
7905
|
+
title: "Refinement diff has no hunk marker",
|
|
7906
|
+
detail: "A unified diff typically contains at least one `@@` line."
|
|
7907
|
+
});
|
|
7908
|
+
}
|
|
7909
|
+
return findings;
|
|
7910
|
+
}
|
|
7911
|
+
function deriveFindings(proposal) {
|
|
7912
|
+
const findings = [];
|
|
7913
|
+
findings.push(...checkName(proposal.content.name));
|
|
7914
|
+
if (proposal.kind === "new-skill") {
|
|
7915
|
+
findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
|
|
7916
|
+
findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
|
|
7917
|
+
} else if (proposal.kind === "refinement") {
|
|
7918
|
+
findings.push(...checkDiff(proposal.content.diff ?? ""));
|
|
7919
|
+
}
|
|
7920
|
+
return findings;
|
|
7921
|
+
}
|
|
7922
|
+
async function runGate(projectPath, proposalId) {
|
|
7923
|
+
const proposal = await getProposal(projectPath, proposalId);
|
|
7924
|
+
if (!proposal) throw new ProposalNotFoundError(proposalId);
|
|
7925
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7926
|
+
throw new GateRunError(
|
|
7927
|
+
`proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
|
|
7500
7928
|
);
|
|
7501
|
-
jsonResponse(res, 200, sessions);
|
|
7502
|
-
} catch (err) {
|
|
7503
|
-
if (err.code === "ENOENT") {
|
|
7504
|
-
jsonResponse(res, 200, []);
|
|
7505
|
-
return;
|
|
7506
|
-
}
|
|
7507
|
-
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
7508
7929
|
}
|
|
7930
|
+
const findings = deriveFindings(proposal);
|
|
7931
|
+
const runAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7932
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
7933
|
+
const nextStatus = hasError ? "gate-failed" : "gate-running";
|
|
7934
|
+
const updated = await updateProposal(projectPath, proposalId, {
|
|
7935
|
+
status: nextStatus,
|
|
7936
|
+
gate: { lastRunAt: runAt, findings }
|
|
7937
|
+
});
|
|
7938
|
+
return {
|
|
7939
|
+
proposalId: updated.id,
|
|
7940
|
+
status: updated.status,
|
|
7941
|
+
findings,
|
|
7942
|
+
runAt
|
|
7943
|
+
};
|
|
7509
7944
|
}
|
|
7510
|
-
|
|
7511
|
-
|
|
7512
|
-
|
|
7513
|
-
|
|
7945
|
+
|
|
7946
|
+
// src/proposals/promote.ts
|
|
7947
|
+
import * as fs12 from "fs";
|
|
7948
|
+
import * as path13 from "path";
|
|
7949
|
+
import { parse as parseYaml3, stringify as stringifyYaml } from "yaml";
|
|
7950
|
+
import {
|
|
7951
|
+
getProposal as getProposal2,
|
|
7952
|
+
updateProposal as updateProposal2,
|
|
7953
|
+
ProposalNotFoundError as ProposalNotFoundError2
|
|
7954
|
+
} from "@harness-engineering/core";
|
|
7955
|
+
var GateNotReadyError = class extends Error {
|
|
7956
|
+
constructor(message) {
|
|
7957
|
+
super(message);
|
|
7958
|
+
this.name = "GateNotReadyError";
|
|
7514
7959
|
}
|
|
7515
|
-
|
|
7516
|
-
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
jsonResponse(res, 404, { error: "Session not found" });
|
|
7521
|
-
return;
|
|
7522
|
-
}
|
|
7523
|
-
jsonResponse(res, 500, { error: "Failed to read session" });
|
|
7960
|
+
};
|
|
7961
|
+
var PromotionError = class extends Error {
|
|
7962
|
+
constructor(message) {
|
|
7963
|
+
super(message);
|
|
7964
|
+
this.name = "PromotionError";
|
|
7524
7965
|
}
|
|
7966
|
+
};
|
|
7967
|
+
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
7968
|
+
function skillDir(projectPath, name) {
|
|
7969
|
+
return path13.join(projectPath, "agents", "skills", "claude-code", name);
|
|
7525
7970
|
}
|
|
7526
|
-
|
|
7971
|
+
function readIfExists(p) {
|
|
7527
7972
|
try {
|
|
7528
|
-
|
|
7529
|
-
const result = SessionCreateSchema.safeParse(JSON.parse(body));
|
|
7530
|
-
if (!result.success) {
|
|
7531
|
-
jsonResponse(res, 400, { error: "Missing sessionId" });
|
|
7532
|
-
return;
|
|
7533
|
-
}
|
|
7534
|
-
const session = result.data;
|
|
7535
|
-
if (!isSafeId(session.sessionId)) {
|
|
7536
|
-
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7537
|
-
return;
|
|
7538
|
-
}
|
|
7539
|
-
const sessionDir = path11.join(sessionsDir, session.sessionId);
|
|
7540
|
-
await fs11.mkdir(sessionDir, { recursive: true });
|
|
7541
|
-
await fs11.writeFile(path11.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
7542
|
-
jsonResponse(res, 200, { ok: true });
|
|
7973
|
+
return fs12.readFileSync(p, "utf-8");
|
|
7543
7974
|
} catch {
|
|
7544
|
-
|
|
7975
|
+
return null;
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
7979
|
+
let doc;
|
|
7980
|
+
try {
|
|
7981
|
+
doc = parseYaml3(yamlText);
|
|
7982
|
+
} catch (err) {
|
|
7983
|
+
throw new PromotionError(
|
|
7984
|
+
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
7985
|
+
);
|
|
7986
|
+
}
|
|
7987
|
+
if (!doc || typeof doc !== "object") {
|
|
7988
|
+
throw new PromotionError("skill.yaml top-level is not a mapping");
|
|
7989
|
+
}
|
|
7990
|
+
const obj = doc;
|
|
7991
|
+
obj["provenance"] = "agent-proposed";
|
|
7992
|
+
obj["originatingProposalId"] = proposalId;
|
|
7993
|
+
return stringifyYaml(obj);
|
|
7994
|
+
}
|
|
7995
|
+
function assertGateReady(proposal) {
|
|
7996
|
+
if (proposal.status !== "gate-running") {
|
|
7997
|
+
throw new GateNotReadyError(
|
|
7998
|
+
`proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
|
|
7999
|
+
);
|
|
8000
|
+
}
|
|
8001
|
+
const findings = proposal.gate?.findings ?? [];
|
|
8002
|
+
if (findings.some((f) => f.severity === "error")) {
|
|
8003
|
+
throw new GateNotReadyError(
|
|
8004
|
+
`proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
|
|
8005
|
+
);
|
|
8006
|
+
}
|
|
8007
|
+
if (!proposal.gate?.lastRunAt) {
|
|
8008
|
+
throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
|
|
8009
|
+
}
|
|
8010
|
+
const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
|
|
8011
|
+
if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
|
|
8012
|
+
throw new GateNotReadyError(
|
|
8013
|
+
`proposal ${proposal.id} gate run is older than 24h; re-run before approving`
|
|
8014
|
+
);
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
async function promoteNewSkill(projectPath, proposal) {
|
|
8018
|
+
const target = skillDir(projectPath, proposal.content.name);
|
|
8019
|
+
if (fs12.existsSync(target)) {
|
|
8020
|
+
throw new PromotionError(
|
|
8021
|
+
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
8022
|
+
);
|
|
8023
|
+
}
|
|
8024
|
+
fs12.mkdirSync(target, { recursive: true });
|
|
8025
|
+
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
8026
|
+
fs12.writeFileSync(path13.join(target, "skill.yaml"), yamlOut);
|
|
8027
|
+
fs12.writeFileSync(path13.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
8028
|
+
return { skillPath: target };
|
|
8029
|
+
}
|
|
8030
|
+
async function promoteRefinement(projectPath, proposal) {
|
|
8031
|
+
if (!proposal.targetSkill) {
|
|
8032
|
+
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
8033
|
+
}
|
|
8034
|
+
const target = skillDir(projectPath, proposal.targetSkill);
|
|
8035
|
+
if (!fs12.existsSync(target)) {
|
|
8036
|
+
throw new PromotionError(
|
|
8037
|
+
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
8038
|
+
);
|
|
8039
|
+
}
|
|
8040
|
+
const yamlPath = path13.join(target, "skill.yaml");
|
|
8041
|
+
const before = readIfExists(yamlPath) ?? "";
|
|
8042
|
+
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
8043
|
+
if (after === before) {
|
|
8044
|
+
throw new PromotionError(
|
|
8045
|
+
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
8046
|
+
);
|
|
8047
|
+
}
|
|
8048
|
+
fs12.writeFileSync(yamlPath, after);
|
|
8049
|
+
return { skillPath: target };
|
|
8050
|
+
}
|
|
8051
|
+
async function promote(projectPath, proposalId, decidedBy) {
|
|
8052
|
+
const proposal = await getProposal2(projectPath, proposalId);
|
|
8053
|
+
if (!proposal) throw new ProposalNotFoundError2(proposalId);
|
|
8054
|
+
assertGateReady(proposal);
|
|
8055
|
+
const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
|
|
8056
|
+
await updateProposal2(projectPath, proposalId, {
|
|
8057
|
+
status: "approved",
|
|
8058
|
+
decision: {
|
|
8059
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8060
|
+
decidedBy,
|
|
8061
|
+
action: "approved"
|
|
8062
|
+
}
|
|
8063
|
+
});
|
|
8064
|
+
return {
|
|
8065
|
+
proposalId,
|
|
8066
|
+
skillPath: out.skillPath,
|
|
8067
|
+
provenance: "agent-proposed"
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
|
|
8071
|
+
// src/proposals/events.ts
|
|
8072
|
+
function emit3(bus, topic, data) {
|
|
8073
|
+
bus.emit(topic, data);
|
|
8074
|
+
}
|
|
8075
|
+
function emitProposalCreated(bus, proposal) {
|
|
8076
|
+
const data = {
|
|
8077
|
+
id: proposal.id,
|
|
8078
|
+
kind: proposal.kind,
|
|
8079
|
+
name: proposal.content.name,
|
|
8080
|
+
proposedBy: proposal.proposedBy,
|
|
8081
|
+
justification: proposal.source.justification
|
|
8082
|
+
};
|
|
8083
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
8084
|
+
emit3(bus, "proposal.created", data);
|
|
8085
|
+
}
|
|
8086
|
+
function emitProposalApproved(bus, proposal) {
|
|
8087
|
+
const data = {
|
|
8088
|
+
id: proposal.id,
|
|
8089
|
+
kind: proposal.kind,
|
|
8090
|
+
name: proposal.content.name,
|
|
8091
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
|
|
8092
|
+
};
|
|
8093
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
8094
|
+
emit3(bus, "proposal.approved", data);
|
|
8095
|
+
}
|
|
8096
|
+
function emitProposalRejected(bus, proposal) {
|
|
8097
|
+
const data = {
|
|
8098
|
+
id: proposal.id,
|
|
8099
|
+
kind: proposal.kind,
|
|
8100
|
+
name: proposal.content.name,
|
|
8101
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
|
|
8102
|
+
reason: proposal.decision?.reason ?? "(no reason given)"
|
|
8103
|
+
};
|
|
8104
|
+
emit3(bus, "proposal.rejected", data);
|
|
8105
|
+
}
|
|
8106
|
+
|
|
8107
|
+
// src/server/routes/v1/proposals.ts
|
|
8108
|
+
var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
|
|
8109
|
+
var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
|
|
8110
|
+
var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
|
|
8111
|
+
var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
|
|
8112
|
+
var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
|
|
8113
|
+
var ProposalStatusValues = [
|
|
8114
|
+
"open",
|
|
8115
|
+
"gate-running",
|
|
8116
|
+
"gate-failed",
|
|
8117
|
+
"approved",
|
|
8118
|
+
"rejected"
|
|
8119
|
+
];
|
|
8120
|
+
var RejectBody = z13.object({
|
|
8121
|
+
reason: z13.string().min(1).max(280)
|
|
8122
|
+
});
|
|
8123
|
+
function sendJSON8(res, status, body) {
|
|
8124
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8125
|
+
res.end(JSON.stringify(body));
|
|
8126
|
+
}
|
|
8127
|
+
function getDecidedBy(req, deps) {
|
|
8128
|
+
if (deps.decidedByResolver) return deps.decidedByResolver(req);
|
|
8129
|
+
const token = req._authToken;
|
|
8130
|
+
return token?.id ?? "unknown";
|
|
8131
|
+
}
|
|
8132
|
+
function parseStatusFromQuery(url) {
|
|
8133
|
+
const queryIdx = url.indexOf("?");
|
|
8134
|
+
if (queryIdx === -1) return void 0;
|
|
8135
|
+
const params = new URLSearchParams(url.slice(queryIdx + 1));
|
|
8136
|
+
const raw = params.get("status");
|
|
8137
|
+
if (!raw) return void 0;
|
|
8138
|
+
if (raw === "all") return "all";
|
|
8139
|
+
if (ProposalStatusValues.includes(raw)) return raw;
|
|
8140
|
+
return void 0;
|
|
8141
|
+
}
|
|
8142
|
+
async function handleList(req, res, deps) {
|
|
8143
|
+
const url = req.url ?? "";
|
|
8144
|
+
const status = parseStatusFromQuery(url);
|
|
8145
|
+
const proposals = await listProposals(deps.projectPath, status ? { status } : {});
|
|
8146
|
+
sendJSON8(res, 200, proposals);
|
|
8147
|
+
}
|
|
8148
|
+
async function handleGet(res, deps, id) {
|
|
8149
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
8150
|
+
if (!proposal) {
|
|
8151
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8152
|
+
return;
|
|
8153
|
+
}
|
|
8154
|
+
sendJSON8(res, 200, proposal);
|
|
8155
|
+
}
|
|
8156
|
+
async function handleRunGate(res, deps, id) {
|
|
8157
|
+
try {
|
|
8158
|
+
const result = await runGate(deps.projectPath, id);
|
|
8159
|
+
sendJSON8(res, 200, result);
|
|
8160
|
+
} catch (err) {
|
|
8161
|
+
if (err instanceof ProposalNotFoundError3) {
|
|
8162
|
+
sendJSON8(res, 404, { error: err.message });
|
|
8163
|
+
return;
|
|
8164
|
+
}
|
|
8165
|
+
if (err instanceof GateRunError) {
|
|
8166
|
+
sendJSON8(res, 409, { error: err.message });
|
|
8167
|
+
return;
|
|
8168
|
+
}
|
|
8169
|
+
sendJSON8(res, 500, {
|
|
8170
|
+
error: "gate run failed",
|
|
8171
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8172
|
+
});
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
async function handleApprove(req, res, deps, id) {
|
|
8176
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
8177
|
+
try {
|
|
8178
|
+
const result = await promote(deps.projectPath, id, decidedBy);
|
|
8179
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
8180
|
+
if (proposal) emitProposalApproved(deps.bus, proposal);
|
|
8181
|
+
sendJSON8(res, 200, { promotion: result, proposal });
|
|
8182
|
+
} catch (err) {
|
|
8183
|
+
if (err instanceof ProposalNotFoundError3) {
|
|
8184
|
+
sendJSON8(res, 404, { error: err.message });
|
|
8185
|
+
return;
|
|
8186
|
+
}
|
|
8187
|
+
if (err instanceof GateNotReadyError) {
|
|
8188
|
+
sendJSON8(res, 409, { error: err.message });
|
|
8189
|
+
return;
|
|
8190
|
+
}
|
|
8191
|
+
if (err instanceof PromotionError) {
|
|
8192
|
+
sendJSON8(res, 422, { error: err.message });
|
|
8193
|
+
return;
|
|
8194
|
+
}
|
|
8195
|
+
sendJSON8(res, 500, {
|
|
8196
|
+
error: "approve failed",
|
|
8197
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8198
|
+
});
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
async function handleReject(req, res, deps, id) {
|
|
8202
|
+
let raw;
|
|
8203
|
+
try {
|
|
8204
|
+
raw = await readBody(req);
|
|
8205
|
+
} catch (err) {
|
|
8206
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
8207
|
+
return;
|
|
8208
|
+
}
|
|
8209
|
+
let json;
|
|
8210
|
+
try {
|
|
8211
|
+
json = raw.length > 0 ? JSON.parse(raw) : {};
|
|
8212
|
+
} catch {
|
|
8213
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
8214
|
+
return;
|
|
8215
|
+
}
|
|
8216
|
+
const parsed = RejectBody.safeParse(json);
|
|
8217
|
+
if (!parsed.success) {
|
|
8218
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
8219
|
+
return;
|
|
8220
|
+
}
|
|
8221
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
8222
|
+
if (!proposal) {
|
|
8223
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8224
|
+
return;
|
|
8225
|
+
}
|
|
8226
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
8227
|
+
sendJSON8(res, 409, {
|
|
8228
|
+
error: `proposal already ${proposal.status}; cannot reject`
|
|
8229
|
+
});
|
|
8230
|
+
return;
|
|
8231
|
+
}
|
|
8232
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
8233
|
+
const updated = await updateProposal3(deps.projectPath, id, {
|
|
8234
|
+
status: "rejected",
|
|
8235
|
+
decision: {
|
|
8236
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8237
|
+
decidedBy,
|
|
8238
|
+
action: "rejected",
|
|
8239
|
+
reason: parsed.data.reason
|
|
8240
|
+
}
|
|
8241
|
+
});
|
|
8242
|
+
emitProposalRejected(deps.bus, updated);
|
|
8243
|
+
sendJSON8(res, 200, updated);
|
|
8244
|
+
}
|
|
8245
|
+
async function handleEdit(req, res, deps, id) {
|
|
8246
|
+
let raw;
|
|
8247
|
+
try {
|
|
8248
|
+
raw = await readBody(req);
|
|
8249
|
+
} catch (err) {
|
|
8250
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
8251
|
+
return;
|
|
8252
|
+
}
|
|
8253
|
+
let json;
|
|
8254
|
+
try {
|
|
8255
|
+
json = JSON.parse(raw);
|
|
8256
|
+
} catch {
|
|
8257
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
8258
|
+
return;
|
|
8259
|
+
}
|
|
8260
|
+
const parsed = EditProposalInputSchema.safeParse(json);
|
|
8261
|
+
if (!parsed.success) {
|
|
8262
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
8263
|
+
return;
|
|
8264
|
+
}
|
|
8265
|
+
const existing = await getProposal3(deps.projectPath, id);
|
|
8266
|
+
if (!existing) {
|
|
8267
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
8268
|
+
return;
|
|
8269
|
+
}
|
|
8270
|
+
if (existing.status === "approved" || existing.status === "rejected") {
|
|
8271
|
+
sendJSON8(res, 409, {
|
|
8272
|
+
error: `proposal already ${existing.status}; cannot edit`
|
|
8273
|
+
});
|
|
8274
|
+
return;
|
|
8275
|
+
}
|
|
8276
|
+
const mergedContent = {
|
|
8277
|
+
...existing.content,
|
|
8278
|
+
...parsed.data.content,
|
|
8279
|
+
name: parsed.data.content.name ?? existing.content.name,
|
|
8280
|
+
description: parsed.data.content.description ?? existing.content.description
|
|
8281
|
+
};
|
|
8282
|
+
try {
|
|
8283
|
+
const updated = await updateProposal3(deps.projectPath, id, {
|
|
8284
|
+
content: mergedContent,
|
|
8285
|
+
status: "open",
|
|
8286
|
+
gate: void 0
|
|
8287
|
+
});
|
|
8288
|
+
sendJSON8(res, 200, updated);
|
|
8289
|
+
} catch (err) {
|
|
8290
|
+
sendJSON8(res, 422, {
|
|
8291
|
+
error: "edit failed",
|
|
8292
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
8293
|
+
});
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
function handleV1ProposalsRoute(req, res, deps) {
|
|
8297
|
+
const url = req.url ?? "";
|
|
8298
|
+
const method = req.method ?? "GET";
|
|
8299
|
+
if (method === "GET" && LIST_RE.test(url)) {
|
|
8300
|
+
void handleList(req, res, deps);
|
|
8301
|
+
return true;
|
|
8302
|
+
}
|
|
8303
|
+
const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
|
|
8304
|
+
if (runGateMatch) {
|
|
8305
|
+
void handleRunGate(res, deps, runGateMatch[1]);
|
|
8306
|
+
return true;
|
|
8307
|
+
}
|
|
8308
|
+
const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
|
|
8309
|
+
if (approveMatch) {
|
|
8310
|
+
void handleApprove(req, res, deps, approveMatch[1]);
|
|
8311
|
+
return true;
|
|
8312
|
+
}
|
|
8313
|
+
const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
|
|
8314
|
+
if (rejectMatch) {
|
|
8315
|
+
void handleReject(req, res, deps, rejectMatch[1]);
|
|
8316
|
+
return true;
|
|
8317
|
+
}
|
|
8318
|
+
if (method === "PATCH") {
|
|
8319
|
+
const m = SINGLE_RE.exec(url);
|
|
8320
|
+
if (m) {
|
|
8321
|
+
void handleEdit(req, res, deps, m[1]);
|
|
8322
|
+
return true;
|
|
8323
|
+
}
|
|
8324
|
+
}
|
|
8325
|
+
if (method === "GET") {
|
|
8326
|
+
const m = SINGLE_RE.exec(url);
|
|
8327
|
+
if (m) {
|
|
8328
|
+
void handleGet(res, deps, m[1]);
|
|
8329
|
+
return true;
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
return false;
|
|
8333
|
+
}
|
|
8334
|
+
|
|
8335
|
+
// src/server/routes/v1/routing.ts
|
|
8336
|
+
import { z as z14 } from "zod";
|
|
8337
|
+
var CONFIG_RE = /^\/api\/v1\/routing\/config(?:\?.*)?$/;
|
|
8338
|
+
var DECISIONS_RE = /^\/api\/v1\/routing\/decisions(?:\?.*)?$/;
|
|
8339
|
+
var TRACE_RE = /^\/api\/v1\/routing\/trace(?:\?.*)?$/;
|
|
8340
|
+
function sendJSON9(res, status, body) {
|
|
8341
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8342
|
+
res.end(JSON.stringify(body));
|
|
8343
|
+
}
|
|
8344
|
+
function unavailable(res) {
|
|
8345
|
+
sendJSON9(res, 503, { error: "BackendRouter not available" });
|
|
8346
|
+
return true;
|
|
8347
|
+
}
|
|
8348
|
+
function resolveChain(value, backends) {
|
|
8349
|
+
return toArray(value).map((c) => ({ candidate: c, exists: c in backends }));
|
|
8350
|
+
}
|
|
8351
|
+
function buildResolvedChains(routing, backends) {
|
|
8352
|
+
const out = {};
|
|
8353
|
+
out["default"] = resolveChain(routing.default, backends);
|
|
8354
|
+
for (const tier of ["quick-fix", "guided-change", "full-exploration", "diagnostic"]) {
|
|
8355
|
+
const v = routing[tier];
|
|
8356
|
+
if (v !== void 0) out[`tier:${tier}`] = resolveChain(v, backends);
|
|
8357
|
+
}
|
|
8358
|
+
if (routing.intelligence) {
|
|
8359
|
+
for (const [layer, v] of Object.entries(routing.intelligence)) {
|
|
8360
|
+
if (v !== void 0) out[`intelligence:${layer}`] = resolveChain(v, backends);
|
|
8361
|
+
}
|
|
8362
|
+
}
|
|
8363
|
+
if (routing.isolation) {
|
|
8364
|
+
for (const [tier, v] of Object.entries(routing.isolation)) {
|
|
8365
|
+
if (v !== void 0) out[`isolation:${tier}`] = resolveChain(v, backends);
|
|
8366
|
+
}
|
|
8367
|
+
}
|
|
8368
|
+
if (routing.skills) {
|
|
8369
|
+
for (const [name, v] of Object.entries(routing.skills)) {
|
|
8370
|
+
if (v !== void 0) out[`skill:${name}`] = resolveChain(v, backends);
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
if (routing.modes) {
|
|
8374
|
+
for (const [mode, v] of Object.entries(routing.modes)) {
|
|
8375
|
+
if (v !== void 0) out[`mode:${mode}`] = resolveChain(v, backends);
|
|
8376
|
+
}
|
|
8377
|
+
}
|
|
8378
|
+
return out;
|
|
8379
|
+
}
|
|
8380
|
+
function handleConfig(res, deps) {
|
|
8381
|
+
if (!deps.router || !deps.routing || !deps.backends) return unavailable(res);
|
|
8382
|
+
sendJSON9(res, 200, {
|
|
8383
|
+
routing: deps.routing,
|
|
8384
|
+
resolvedChains: buildResolvedChains(deps.routing, deps.backends),
|
|
8385
|
+
backends: Object.keys(deps.backends)
|
|
8386
|
+
});
|
|
8387
|
+
return true;
|
|
8388
|
+
}
|
|
8389
|
+
function parseDecisionsQuery(url) {
|
|
8390
|
+
const qIdx = url.indexOf("?");
|
|
8391
|
+
if (qIdx === -1) return {};
|
|
8392
|
+
const p = new URLSearchParams(url.slice(qIdx + 1));
|
|
8393
|
+
const filter = {};
|
|
8394
|
+
const skill = p.get("skill");
|
|
8395
|
+
const mode = p.get("mode");
|
|
8396
|
+
const backend = p.get("backend");
|
|
8397
|
+
const limit = p.get("limit");
|
|
8398
|
+
if (skill) filter.skillName = skill;
|
|
8399
|
+
if (mode) filter.mode = mode;
|
|
8400
|
+
if (backend) filter.backendName = backend;
|
|
8401
|
+
if (limit) {
|
|
8402
|
+
const n = Number(limit);
|
|
8403
|
+
if (Number.isFinite(n) && n > 0) filter.limit = Math.floor(n);
|
|
8404
|
+
}
|
|
8405
|
+
return filter;
|
|
8406
|
+
}
|
|
8407
|
+
function handleDecisions(req, res, deps) {
|
|
8408
|
+
if (!deps.bus) return unavailable(res);
|
|
8409
|
+
const filter = parseDecisionsQuery(req.url ?? "");
|
|
8410
|
+
sendJSON9(res, 200, { decisions: deps.bus.recent(filter) });
|
|
8411
|
+
return true;
|
|
8412
|
+
}
|
|
8413
|
+
var UseCaseSchema = z14.discriminatedUnion("kind", [
|
|
8414
|
+
z14.object({
|
|
8415
|
+
kind: z14.literal("tier"),
|
|
8416
|
+
tier: z14.enum(["quick-fix", "guided-change", "full-exploration", "diagnostic"])
|
|
8417
|
+
}),
|
|
8418
|
+
z14.object({ kind: z14.literal("intelligence"), layer: z14.enum(["sel", "pesl"]) }),
|
|
8419
|
+
z14.object({ kind: z14.literal("isolation"), tier: z14.string() }),
|
|
8420
|
+
z14.object({ kind: z14.literal("maintenance") }),
|
|
8421
|
+
z14.object({ kind: z14.literal("chat") }),
|
|
8422
|
+
z14.object({
|
|
8423
|
+
kind: z14.literal("skill"),
|
|
8424
|
+
skillName: z14.string().min(1),
|
|
8425
|
+
cognitiveMode: z14.string().optional()
|
|
8426
|
+
}),
|
|
8427
|
+
z14.object({ kind: z14.literal("mode"), cognitiveMode: z14.string().min(1) })
|
|
8428
|
+
]);
|
|
8429
|
+
var TraceBodySchema = z14.object({
|
|
8430
|
+
useCase: UseCaseSchema,
|
|
8431
|
+
invocationOverride: z14.string().min(1).optional()
|
|
8432
|
+
});
|
|
8433
|
+
async function handleTrace(req, res, deps) {
|
|
8434
|
+
if (!deps.routing || !deps.backends) {
|
|
8435
|
+
unavailable(res);
|
|
8436
|
+
return true;
|
|
8437
|
+
}
|
|
8438
|
+
let raw;
|
|
8439
|
+
try {
|
|
8440
|
+
raw = await readBody(req);
|
|
8441
|
+
} catch {
|
|
8442
|
+
sendJSON9(res, 400, { error: "body read failed" });
|
|
8443
|
+
return true;
|
|
8444
|
+
}
|
|
8445
|
+
let parsed;
|
|
8446
|
+
try {
|
|
8447
|
+
parsed = JSON.parse(raw);
|
|
8448
|
+
} catch {
|
|
8449
|
+
sendJSON9(res, 400, { error: "invalid JSON body" });
|
|
8450
|
+
return true;
|
|
8451
|
+
}
|
|
8452
|
+
const r = TraceBodySchema.safeParse(parsed);
|
|
8453
|
+
if (!r.success) {
|
|
8454
|
+
sendJSON9(res, 400, { error: r.error.message });
|
|
8455
|
+
return true;
|
|
8456
|
+
}
|
|
8457
|
+
const opts = r.data.invocationOverride !== void 0 ? { invocationOverride: r.data.invocationOverride } : void 0;
|
|
8458
|
+
try {
|
|
8459
|
+
const dryRunRouter = new BackendRouter({
|
|
8460
|
+
backends: deps.backends,
|
|
8461
|
+
routing: deps.routing
|
|
8462
|
+
});
|
|
8463
|
+
const { decision, def } = dryRunRouter.resolveDecisionAndDef(
|
|
8464
|
+
r.data.useCase,
|
|
8465
|
+
opts
|
|
8466
|
+
);
|
|
8467
|
+
sendJSON9(res, 200, { decision, def: { type: def.type } });
|
|
8468
|
+
} catch (err) {
|
|
8469
|
+
sendJSON9(res, 500, { error: String(err) });
|
|
8470
|
+
}
|
|
8471
|
+
return true;
|
|
8472
|
+
}
|
|
8473
|
+
function handleV1RoutingRoute(req, res, deps) {
|
|
8474
|
+
const url = req.url ?? "";
|
|
8475
|
+
const method = req.method ?? "GET";
|
|
8476
|
+
if (method === "GET" && CONFIG_RE.test(url)) return handleConfig(res, deps);
|
|
8477
|
+
if (method === "GET" && DECISIONS_RE.test(url)) return handleDecisions(req, res, deps);
|
|
8478
|
+
if (method === "POST" && TRACE_RE.test(url)) {
|
|
8479
|
+
void handleTrace(req, res, deps);
|
|
8480
|
+
return true;
|
|
8481
|
+
}
|
|
8482
|
+
return false;
|
|
8483
|
+
}
|
|
8484
|
+
|
|
8485
|
+
// src/server/routes/sessions.ts
|
|
8486
|
+
import * as fs13 from "fs/promises";
|
|
8487
|
+
import * as path14 from "path";
|
|
8488
|
+
import { z as z15 } from "zod";
|
|
8489
|
+
var SessionCreateSchema = z15.object({
|
|
8490
|
+
sessionId: z15.string().min(1)
|
|
8491
|
+
}).passthrough();
|
|
8492
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8493
|
+
function isSafeId(id) {
|
|
8494
|
+
return UUID_RE2.test(id) || path14.basename(id) === id && !id.includes("..");
|
|
8495
|
+
}
|
|
8496
|
+
function jsonResponse(res, status, data) {
|
|
8497
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8498
|
+
res.end(JSON.stringify(data));
|
|
8499
|
+
}
|
|
8500
|
+
function extractSessionId(url) {
|
|
8501
|
+
const segments = new URL(url, "http://localhost").pathname.split(path14.posix.sep);
|
|
8502
|
+
const id = segments.pop();
|
|
8503
|
+
return id && id !== "sessions" ? id : null;
|
|
8504
|
+
}
|
|
8505
|
+
async function handleList2(res, sessionsDir) {
|
|
8506
|
+
try {
|
|
8507
|
+
const entries = await fs13.readdir(sessionsDir, { withFileTypes: true });
|
|
8508
|
+
const sessions = [];
|
|
8509
|
+
for (const entry of entries) {
|
|
8510
|
+
if (!entry.isDirectory()) continue;
|
|
8511
|
+
try {
|
|
8512
|
+
const content = await fs13.readFile(
|
|
8513
|
+
path14.join(sessionsDir, entry.name, "session.json"),
|
|
8514
|
+
"utf-8"
|
|
8515
|
+
);
|
|
8516
|
+
sessions.push(JSON.parse(content));
|
|
8517
|
+
} catch {
|
|
8518
|
+
}
|
|
8519
|
+
}
|
|
8520
|
+
sessions.sort(
|
|
8521
|
+
(a, b) => new Date(b.lastActiveAt).getTime() - new Date(a.lastActiveAt).getTime()
|
|
8522
|
+
);
|
|
8523
|
+
jsonResponse(res, 200, sessions);
|
|
8524
|
+
} catch (err) {
|
|
8525
|
+
if (err.code === "ENOENT") {
|
|
8526
|
+
jsonResponse(res, 200, []);
|
|
8527
|
+
return;
|
|
8528
|
+
}
|
|
8529
|
+
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
8530
|
+
}
|
|
8531
|
+
}
|
|
8532
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
8533
|
+
if (!isSafeId(id)) {
|
|
8534
|
+
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
8535
|
+
return;
|
|
8536
|
+
}
|
|
8537
|
+
try {
|
|
8538
|
+
const content = await fs13.readFile(path14.join(sessionsDir, id, "session.json"), "utf-8");
|
|
8539
|
+
jsonResponse(res, 200, JSON.parse(content));
|
|
8540
|
+
} catch (err) {
|
|
8541
|
+
if (err.code === "ENOENT") {
|
|
8542
|
+
jsonResponse(res, 404, { error: "Session not found" });
|
|
8543
|
+
return;
|
|
8544
|
+
}
|
|
8545
|
+
jsonResponse(res, 500, { error: "Failed to read session" });
|
|
8546
|
+
}
|
|
8547
|
+
}
|
|
8548
|
+
async function handleCreate(req, res, sessionsDir) {
|
|
8549
|
+
try {
|
|
8550
|
+
const body = await readBody(req);
|
|
8551
|
+
const result = SessionCreateSchema.safeParse(JSON.parse(body));
|
|
8552
|
+
if (!result.success) {
|
|
8553
|
+
jsonResponse(res, 400, { error: "Missing sessionId" });
|
|
8554
|
+
return;
|
|
8555
|
+
}
|
|
8556
|
+
const session = result.data;
|
|
8557
|
+
if (!isSafeId(session.sessionId)) {
|
|
8558
|
+
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
8559
|
+
return;
|
|
8560
|
+
}
|
|
8561
|
+
const sessionDir = path14.join(sessionsDir, session.sessionId);
|
|
8562
|
+
await fs13.mkdir(sessionDir, { recursive: true });
|
|
8563
|
+
await fs13.writeFile(path14.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
8564
|
+
jsonResponse(res, 200, { ok: true });
|
|
8565
|
+
} catch {
|
|
8566
|
+
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
7545
8567
|
}
|
|
7546
8568
|
}
|
|
7547
8569
|
async function handleUpdate(req, res, url, sessionsDir) {
|
|
@@ -7552,10 +8574,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
7552
8574
|
return;
|
|
7553
8575
|
}
|
|
7554
8576
|
const body = await readBody(req);
|
|
7555
|
-
const updates =
|
|
7556
|
-
const sessionFilePath =
|
|
7557
|
-
const current = JSON.parse(await
|
|
7558
|
-
await
|
|
8577
|
+
const updates = z15.record(z15.unknown()).parse(JSON.parse(body));
|
|
8578
|
+
const sessionFilePath = path14.join(sessionsDir, id, "session.json");
|
|
8579
|
+
const current = JSON.parse(await fs13.readFile(sessionFilePath, "utf-8"));
|
|
8580
|
+
await fs13.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
7559
8581
|
jsonResponse(res, 200, { ok: true });
|
|
7560
8582
|
} catch {
|
|
7561
8583
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -7568,7 +8590,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
7568
8590
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
7569
8591
|
return;
|
|
7570
8592
|
}
|
|
7571
|
-
await
|
|
8593
|
+
await fs13.rm(path14.join(sessionsDir, id), { recursive: true, force: true });
|
|
7572
8594
|
jsonResponse(res, 200, { ok: true });
|
|
7573
8595
|
} catch {
|
|
7574
8596
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7581,8 +8603,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7581
8603
|
switch (method) {
|
|
7582
8604
|
case "GET": {
|
|
7583
8605
|
const id = extractSessionId(url);
|
|
7584
|
-
if (id) void
|
|
7585
|
-
else void
|
|
8606
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8607
|
+
else void handleList2(res, sessionsDir);
|
|
7586
8608
|
return true;
|
|
7587
8609
|
}
|
|
7588
8610
|
case "POST":
|
|
@@ -7672,20 +8694,20 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7672
8694
|
}
|
|
7673
8695
|
|
|
7674
8696
|
// src/server/routes/auth.ts
|
|
7675
|
-
import { z as
|
|
8697
|
+
import { z as z16 } from "zod";
|
|
7676
8698
|
import {
|
|
7677
8699
|
TokenScopeSchema,
|
|
7678
8700
|
BridgeKindSchema,
|
|
7679
8701
|
AuthTokenPublicSchema
|
|
7680
8702
|
} from "@harness-engineering/types";
|
|
7681
|
-
var CreateBodySchema =
|
|
7682
|
-
name:
|
|
7683
|
-
scopes:
|
|
8703
|
+
var CreateBodySchema = z16.object({
|
|
8704
|
+
name: z16.string().min(1).max(100),
|
|
8705
|
+
scopes: z16.array(TokenScopeSchema).min(1),
|
|
7684
8706
|
bridgeKind: BridgeKindSchema.optional(),
|
|
7685
|
-
tenantId:
|
|
7686
|
-
expiresAt:
|
|
8707
|
+
tenantId: z16.string().optional(),
|
|
8708
|
+
expiresAt: z16.string().datetime().optional()
|
|
7687
8709
|
});
|
|
7688
|
-
function
|
|
8710
|
+
function sendJSON10(res, status, body) {
|
|
7689
8711
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7690
8712
|
res.end(JSON.stringify(body));
|
|
7691
8713
|
}
|
|
@@ -7695,19 +8717,19 @@ async function handlePost(req, res, store) {
|
|
|
7695
8717
|
raw = await readBody(req);
|
|
7696
8718
|
} catch (err) {
|
|
7697
8719
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7698
|
-
|
|
8720
|
+
sendJSON10(res, 413, { error: msg });
|
|
7699
8721
|
return;
|
|
7700
8722
|
}
|
|
7701
8723
|
let json;
|
|
7702
8724
|
try {
|
|
7703
8725
|
json = JSON.parse(raw);
|
|
7704
8726
|
} catch {
|
|
7705
|
-
|
|
8727
|
+
sendJSON10(res, 400, { error: "Invalid JSON body" });
|
|
7706
8728
|
return;
|
|
7707
8729
|
}
|
|
7708
8730
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7709
8731
|
if (!parsed.success) {
|
|
7710
|
-
|
|
8732
|
+
sendJSON10(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7711
8733
|
return;
|
|
7712
8734
|
}
|
|
7713
8735
|
try {
|
|
@@ -7720,37 +8742,37 @@ async function handlePost(req, res, store) {
|
|
|
7720
8742
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7721
8743
|
const result = await store.create(input);
|
|
7722
8744
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
7723
|
-
|
|
8745
|
+
sendJSON10(res, 200, {
|
|
7724
8746
|
...publicRecord,
|
|
7725
8747
|
token: result.token
|
|
7726
8748
|
});
|
|
7727
8749
|
} catch (err) {
|
|
7728
8750
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7729
8751
|
if (msg.includes("already exists")) {
|
|
7730
|
-
|
|
8752
|
+
sendJSON10(res, 409, { error: msg });
|
|
7731
8753
|
return;
|
|
7732
8754
|
}
|
|
7733
|
-
|
|
8755
|
+
sendJSON10(res, 500, { error: "Internal error creating token" });
|
|
7734
8756
|
}
|
|
7735
8757
|
}
|
|
7736
|
-
async function
|
|
8758
|
+
async function handleList3(res, store) {
|
|
7737
8759
|
try {
|
|
7738
8760
|
const list = await store.list();
|
|
7739
|
-
|
|
8761
|
+
sendJSON10(res, 200, list);
|
|
7740
8762
|
} catch {
|
|
7741
|
-
|
|
8763
|
+
sendJSON10(res, 500, { error: "Internal error listing tokens" });
|
|
7742
8764
|
}
|
|
7743
8765
|
}
|
|
7744
8766
|
async function handleDelete2(res, store, id) {
|
|
7745
8767
|
try {
|
|
7746
8768
|
const ok = await store.revoke(id);
|
|
7747
8769
|
if (!ok) {
|
|
7748
|
-
|
|
8770
|
+
sendJSON10(res, 404, { error: "Token not found" });
|
|
7749
8771
|
return;
|
|
7750
8772
|
}
|
|
7751
|
-
|
|
8773
|
+
sendJSON10(res, 200, { deleted: true });
|
|
7752
8774
|
} catch {
|
|
7753
|
-
|
|
8775
|
+
sendJSON10(res, 500, { error: "Internal error revoking token" });
|
|
7754
8776
|
}
|
|
7755
8777
|
}
|
|
7756
8778
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7764,7 +8786,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7764
8786
|
return true;
|
|
7765
8787
|
}
|
|
7766
8788
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7767
|
-
void
|
|
8789
|
+
void handleList3(res, store);
|
|
7768
8790
|
return true;
|
|
7769
8791
|
}
|
|
7770
8792
|
if (method === "DELETE") {
|
|
@@ -7775,12 +8797,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7775
8797
|
return true;
|
|
7776
8798
|
}
|
|
7777
8799
|
}
|
|
7778
|
-
|
|
8800
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7779
8801
|
return true;
|
|
7780
8802
|
}
|
|
7781
8803
|
|
|
7782
8804
|
// src/server/routes/local-model.ts
|
|
7783
|
-
function
|
|
8805
|
+
function sendJSON11(res, status, body) {
|
|
7784
8806
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7785
8807
|
res.end(JSON.stringify(body));
|
|
7786
8808
|
}
|
|
@@ -7788,36 +8810,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7788
8810
|
const { method, url } = req;
|
|
7789
8811
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7790
8812
|
if (method !== "GET") {
|
|
7791
|
-
|
|
8813
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
7792
8814
|
return true;
|
|
7793
8815
|
}
|
|
7794
8816
|
if (!getStatus) {
|
|
7795
|
-
|
|
8817
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
7796
8818
|
return true;
|
|
7797
8819
|
}
|
|
7798
8820
|
const status = getStatus();
|
|
7799
8821
|
if (!status) {
|
|
7800
|
-
|
|
8822
|
+
sendJSON11(res, 503, { error: "Local backend not configured" });
|
|
7801
8823
|
return true;
|
|
7802
8824
|
}
|
|
7803
|
-
|
|
8825
|
+
sendJSON11(res, 200, status);
|
|
7804
8826
|
return true;
|
|
7805
8827
|
}
|
|
7806
8828
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7807
8829
|
const { method, url } = req;
|
|
7808
8830
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7809
8831
|
if (method !== "GET") {
|
|
7810
|
-
|
|
8832
|
+
sendJSON11(res, 405, { error: "Method not allowed" });
|
|
7811
8833
|
return true;
|
|
7812
8834
|
}
|
|
7813
8835
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7814
|
-
|
|
8836
|
+
sendJSON11(res, 200, statuses);
|
|
7815
8837
|
return true;
|
|
7816
8838
|
}
|
|
7817
8839
|
|
|
7818
8840
|
// src/server/static.ts
|
|
7819
|
-
import * as
|
|
7820
|
-
import * as
|
|
8841
|
+
import * as fs14 from "fs";
|
|
8842
|
+
import * as path15 from "path";
|
|
7821
8843
|
var MIME_TYPES = {
|
|
7822
8844
|
".html": "text/html; charset=utf-8",
|
|
7823
8845
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7837,29 +8859,29 @@ var MIME_TYPES = {
|
|
|
7837
8859
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7838
8860
|
const { method, url } = req;
|
|
7839
8861
|
if (method !== "GET") return false;
|
|
7840
|
-
const apiPrefix =
|
|
7841
|
-
const wsPath =
|
|
8862
|
+
const apiPrefix = path15.posix.join(path15.posix.sep, "api", path15.posix.sep);
|
|
8863
|
+
const wsPath = path15.posix.join(path15.posix.sep, "ws");
|
|
7842
8864
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7843
8865
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7844
|
-
const requestedPath =
|
|
7845
|
-
const resolved =
|
|
7846
|
-
if (!resolved.startsWith(
|
|
7847
|
-
return serveFile(
|
|
8866
|
+
const requestedPath = path15.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8867
|
+
const resolved = path15.resolve(requestedPath);
|
|
8868
|
+
if (!resolved.startsWith(path15.resolve(dashboardDir))) {
|
|
8869
|
+
return serveFile(path15.join(dashboardDir, "index.html"), res);
|
|
7848
8870
|
}
|
|
7849
|
-
if (
|
|
8871
|
+
if (fs14.existsSync(resolved) && fs14.statSync(resolved).isFile()) {
|
|
7850
8872
|
return serveFile(resolved, res);
|
|
7851
8873
|
}
|
|
7852
|
-
const indexPath =
|
|
7853
|
-
if (
|
|
8874
|
+
const indexPath = path15.join(dashboardDir, "index.html");
|
|
8875
|
+
if (fs14.existsSync(indexPath)) {
|
|
7854
8876
|
return serveFile(indexPath, res);
|
|
7855
8877
|
}
|
|
7856
8878
|
return false;
|
|
7857
8879
|
}
|
|
7858
8880
|
function serveFile(filePath, res) {
|
|
7859
|
-
const ext =
|
|
8881
|
+
const ext = path15.extname(filePath).toLowerCase();
|
|
7860
8882
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7861
8883
|
try {
|
|
7862
|
-
const content =
|
|
8884
|
+
const content = fs14.readFileSync(filePath);
|
|
7863
8885
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7864
8886
|
res.end(content);
|
|
7865
8887
|
return true;
|
|
@@ -7869,8 +8891,8 @@ function serveFile(filePath, res) {
|
|
|
7869
8891
|
}
|
|
7870
8892
|
|
|
7871
8893
|
// src/server/plan-watcher.ts
|
|
7872
|
-
import * as
|
|
7873
|
-
import * as
|
|
8894
|
+
import * as fs15 from "fs";
|
|
8895
|
+
import * as path16 from "path";
|
|
7874
8896
|
var PlanWatcher = class {
|
|
7875
8897
|
plansDir;
|
|
7876
8898
|
queue;
|
|
@@ -7884,11 +8906,11 @@ var PlanWatcher = class {
|
|
|
7884
8906
|
* Creates the directory if it does not exist.
|
|
7885
8907
|
*/
|
|
7886
8908
|
start() {
|
|
7887
|
-
|
|
7888
|
-
this.watcher =
|
|
8909
|
+
fs15.mkdirSync(this.plansDir, { recursive: true });
|
|
8910
|
+
this.watcher = fs15.watch(this.plansDir, (eventType, filename) => {
|
|
7889
8911
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7890
|
-
const filePath =
|
|
7891
|
-
if (
|
|
8912
|
+
const filePath = path16.join(this.plansDir, filename);
|
|
8913
|
+
if (fs15.existsSync(filePath)) {
|
|
7892
8914
|
void this.handleNewPlan(filename);
|
|
7893
8915
|
}
|
|
7894
8916
|
}
|
|
@@ -7921,7 +8943,7 @@ var PlanWatcher = class {
|
|
|
7921
8943
|
// src/auth/tokens.ts
|
|
7922
8944
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
7923
8945
|
import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7, rename as rename2 } from "fs/promises";
|
|
7924
|
-
import { dirname as
|
|
8946
|
+
import { dirname as dirname5 } from "path";
|
|
7925
8947
|
import bcrypt from "bcryptjs";
|
|
7926
8948
|
import {
|
|
7927
8949
|
AuthTokenSchema,
|
|
@@ -7941,8 +8963,8 @@ function parseToken(raw) {
|
|
|
7941
8963
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7942
8964
|
}
|
|
7943
8965
|
var TokenStore = class {
|
|
7944
|
-
constructor(
|
|
7945
|
-
this.path =
|
|
8966
|
+
constructor(path24) {
|
|
8967
|
+
this.path = path24;
|
|
7946
8968
|
}
|
|
7947
8969
|
path;
|
|
7948
8970
|
cache = null;
|
|
@@ -7963,7 +8985,7 @@ var TokenStore = class {
|
|
|
7963
8985
|
return this.cache;
|
|
7964
8986
|
}
|
|
7965
8987
|
async persist(records) {
|
|
7966
|
-
await mkdir7(
|
|
8988
|
+
await mkdir7(dirname5(this.path), { recursive: true });
|
|
7967
8989
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes3(4).toString("hex")}`;
|
|
7968
8990
|
await writeFile8(tmp, JSON.stringify(records, null, 2), "utf8");
|
|
7969
8991
|
await rename2(tmp, this.path);
|
|
@@ -8046,11 +9068,11 @@ var TokenStore = class {
|
|
|
8046
9068
|
|
|
8047
9069
|
// src/auth/audit.ts
|
|
8048
9070
|
import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
8049
|
-
import { dirname as
|
|
9071
|
+
import { dirname as dirname6 } from "path";
|
|
8050
9072
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
8051
9073
|
var AuditLogger = class {
|
|
8052
|
-
constructor(
|
|
8053
|
-
this.path =
|
|
9074
|
+
constructor(path24, opts = {}) {
|
|
9075
|
+
this.path = path24;
|
|
8054
9076
|
this.opts = opts;
|
|
8055
9077
|
}
|
|
8056
9078
|
path;
|
|
@@ -8077,7 +9099,7 @@ var AuditLogger = class {
|
|
|
8077
9099
|
async writeLine(line) {
|
|
8078
9100
|
try {
|
|
8079
9101
|
if (this.opts.createDir !== false && !this.dirEnsured) {
|
|
8080
|
-
await mkdir8(
|
|
9102
|
+
await mkdir8(dirname6(this.path), { recursive: true });
|
|
8081
9103
|
this.dirEnsured = true;
|
|
8082
9104
|
}
|
|
8083
9105
|
await appendFile(this.path, line, "utf8");
|
|
@@ -8134,20 +9156,79 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8134
9156
|
scope: "subscribe-webhook",
|
|
8135
9157
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
8136
9158
|
},
|
|
9159
|
+
// Hermes Phase 4 — skill proposal review queue.
|
|
9160
|
+
{
|
|
9161
|
+
method: "GET",
|
|
9162
|
+
pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
|
|
9163
|
+
scope: "read-status",
|
|
9164
|
+
description: "List skill proposals (open + decided)."
|
|
9165
|
+
},
|
|
9166
|
+
{
|
|
9167
|
+
method: "GET",
|
|
9168
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
9169
|
+
scope: "read-status",
|
|
9170
|
+
description: "Get a single skill proposal."
|
|
9171
|
+
},
|
|
9172
|
+
{
|
|
9173
|
+
method: "POST",
|
|
9174
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
|
|
9175
|
+
scope: "manage-proposals",
|
|
9176
|
+
description: "Run the soundness-review gate against a proposal."
|
|
9177
|
+
},
|
|
9178
|
+
{
|
|
9179
|
+
method: "POST",
|
|
9180
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
|
|
9181
|
+
scope: "manage-proposals",
|
|
9182
|
+
description: "Approve a proposal \u2014 promotes the skill into the catalog."
|
|
9183
|
+
},
|
|
9184
|
+
{
|
|
9185
|
+
method: "POST",
|
|
9186
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
|
|
9187
|
+
scope: "manage-proposals",
|
|
9188
|
+
description: "Reject a proposal with a one-line reason."
|
|
9189
|
+
},
|
|
9190
|
+
{
|
|
9191
|
+
method: "PATCH",
|
|
9192
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
9193
|
+
scope: "manage-proposals",
|
|
9194
|
+
description: "Edit proposal content (resets gate to not-run)."
|
|
9195
|
+
},
|
|
8137
9196
|
// ── Phase 5 bridge primitives ──
|
|
8138
9197
|
{
|
|
8139
9198
|
method: "GET",
|
|
8140
9199
|
pattern: /^\/api\/v1\/telemetry\/cache\/stats(?:\?.*)?$/,
|
|
8141
9200
|
scope: "read-telemetry",
|
|
8142
9201
|
description: "Prompt-cache hit/miss snapshot (rolling window)."
|
|
9202
|
+
},
|
|
9203
|
+
// ── Spec B Phase 5 routing observability ──
|
|
9204
|
+
// D-OP-1: all three reuse `read-telemetry` — matches the cacheMetrics
|
|
9205
|
+
// precedent (read-only observability). A dedicated `read-routing`
|
|
9206
|
+
// scope was rejected to avoid a TokenScopeSchema + ADR cascade.
|
|
9207
|
+
{
|
|
9208
|
+
method: "GET",
|
|
9209
|
+
pattern: /^\/api\/v1\/routing\/config(?:\?.*)?$/,
|
|
9210
|
+
scope: "read-telemetry",
|
|
9211
|
+
description: "Current routing config + resolved fallback chains + known backends."
|
|
9212
|
+
},
|
|
9213
|
+
{
|
|
9214
|
+
method: "GET",
|
|
9215
|
+
pattern: /^\/api\/v1\/routing\/decisions(?:\?.*)?$/,
|
|
9216
|
+
scope: "read-telemetry",
|
|
9217
|
+
description: "Recent routing decisions (newest-first), filterable by skill/mode/backend."
|
|
9218
|
+
},
|
|
9219
|
+
{
|
|
9220
|
+
method: "POST",
|
|
9221
|
+
pattern: /^\/api\/v1\/routing\/trace(?:\?.*)?$/,
|
|
9222
|
+
scope: "read-telemetry",
|
|
9223
|
+
description: "Dry-run a routing decision without side effects (no bus emit, no dispatch)."
|
|
8143
9224
|
}
|
|
8144
9225
|
];
|
|
8145
9226
|
function isV1Bridge(method, url) {
|
|
8146
9227
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8147
9228
|
}
|
|
8148
|
-
function requiredBridgeScope(method,
|
|
9229
|
+
function requiredBridgeScope(method, path24) {
|
|
8149
9230
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8150
|
-
if (r.method === method && r.pattern.test(
|
|
9231
|
+
if (r.method === method && r.pattern.test(path24)) return r.scope;
|
|
8151
9232
|
}
|
|
8152
9233
|
return null;
|
|
8153
9234
|
}
|
|
@@ -8157,24 +9238,24 @@ function hasScope(held, required) {
|
|
|
8157
9238
|
if (held.includes("admin")) return true;
|
|
8158
9239
|
return held.includes(required);
|
|
8159
9240
|
}
|
|
8160
|
-
function requiredScopeForRoute(method,
|
|
8161
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
9241
|
+
function requiredScopeForRoute(method, path24) {
|
|
9242
|
+
const bridgeScope = requiredBridgeScope(method, path24);
|
|
8162
9243
|
if (bridgeScope) return bridgeScope;
|
|
8163
|
-
if (
|
|
8164
|
-
if (
|
|
8165
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
8166
|
-
if ((
|
|
8167
|
-
if (
|
|
8168
|
-
if (
|
|
8169
|
-
if (
|
|
8170
|
-
if (
|
|
8171
|
-
if (
|
|
8172
|
-
if (
|
|
9244
|
+
if (path24 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
9245
|
+
if (path24 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
9246
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path24) && method === "DELETE") return "admin";
|
|
9247
|
+
if ((path24 === "/api/state" || path24 === "/api/v1/state") && method === "GET") return "read-status";
|
|
9248
|
+
if (path24.startsWith("/api/interactions")) return "resolve-interaction";
|
|
9249
|
+
if (path24.startsWith("/api/plans")) return "read-status";
|
|
9250
|
+
if (path24.startsWith("/api/analyze") || path24.startsWith("/api/analyses")) return "read-status";
|
|
9251
|
+
if (path24.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
9252
|
+
if (path24.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
9253
|
+
if (path24.startsWith("/api/local-model") || path24.startsWith("/api/local-models"))
|
|
8173
9254
|
return "read-status";
|
|
8174
|
-
if (
|
|
8175
|
-
if (
|
|
8176
|
-
if (
|
|
8177
|
-
if (
|
|
9255
|
+
if (path24.startsWith("/api/maintenance")) return "trigger-job";
|
|
9256
|
+
if (path24.startsWith("/api/streams")) return "read-status";
|
|
9257
|
+
if (path24.startsWith("/api/sessions")) return "read-status";
|
|
9258
|
+
if (path24.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
8178
9259
|
return null;
|
|
8179
9260
|
}
|
|
8180
9261
|
|
|
@@ -8228,11 +9309,25 @@ var OrchestratorServer = class {
|
|
|
8228
9309
|
roadmapPath;
|
|
8229
9310
|
dispatchAdHoc;
|
|
8230
9311
|
sessionsDir;
|
|
9312
|
+
/**
|
|
9313
|
+
* Project root used by file-backed routes (Phase 4 proposals at
|
|
9314
|
+
* `.harness/proposals/`). Defaults to process.cwd().
|
|
9315
|
+
*/
|
|
9316
|
+
projectPath;
|
|
8231
9317
|
maintenanceDeps = null;
|
|
8232
9318
|
getLocalModelStatus = null;
|
|
8233
9319
|
getLocalModelStatuses = null;
|
|
8234
9320
|
webhooks;
|
|
8235
9321
|
cacheMetrics;
|
|
9322
|
+
// Spec B Phase 5 — routing observability accessor closures + the WS
|
|
9323
|
+
// broadcaster unsubscribe handle (D-OP-4 dual safety net: server.stop()
|
|
9324
|
+
// calls it explicitly; clearListeners in Orchestrator.stop() is the
|
|
9325
|
+
// belt-and-suspenders second line).
|
|
9326
|
+
getBackendRouterFn = null;
|
|
9327
|
+
getRoutingDecisionBusFn = null;
|
|
9328
|
+
getRoutingConfigFn = null;
|
|
9329
|
+
getBackendsFn = null;
|
|
9330
|
+
routingDecisionUnsubscribe = null;
|
|
8236
9331
|
recorder = null;
|
|
8237
9332
|
planWatcher = null;
|
|
8238
9333
|
tokenStore;
|
|
@@ -8245,8 +9340,8 @@ var OrchestratorServer = class {
|
|
|
8245
9340
|
this.orchestrator = orchestrator;
|
|
8246
9341
|
this.port = port;
|
|
8247
9342
|
this.initDependencies(deps);
|
|
8248
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8249
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
9343
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path17.resolve(".harness", "tokens.json");
|
|
9344
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path17.resolve(".harness", "audit.log");
|
|
8250
9345
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8251
9346
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8252
9347
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8259,19 +9354,24 @@ var OrchestratorServer = class {
|
|
|
8259
9354
|
}
|
|
8260
9355
|
initDependencies(deps) {
|
|
8261
9356
|
this.interactionQueue = deps?.interactionQueue;
|
|
8262
|
-
this.plansDir = deps?.plansDir ??
|
|
8263
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
9357
|
+
this.plansDir = deps?.plansDir ?? path17.resolve("docs", "plans");
|
|
9358
|
+
this.dashboardDir = deps?.dashboardDir ?? path17.resolve("packages", "dashboard", "dist", "client");
|
|
8264
9359
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8265
9360
|
this.pipeline = deps?.pipeline ?? null;
|
|
8266
9361
|
this.analysisArchive = deps?.analysisArchive;
|
|
8267
9362
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8268
9363
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8269
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
9364
|
+
this.sessionsDir = deps?.sessionsDir ?? path17.resolve(".harness", "sessions");
|
|
9365
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8270
9366
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8271
9367
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8272
9368
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
8273
9369
|
this.webhooks = deps?.webhooks;
|
|
8274
9370
|
this.cacheMetrics = deps?.cacheMetrics;
|
|
9371
|
+
this.getBackendRouterFn = deps?.getBackendRouter ?? null;
|
|
9372
|
+
this.getRoutingDecisionBusFn = deps?.getRoutingDecisionBus ?? null;
|
|
9373
|
+
this.getRoutingConfigFn = deps?.getRoutingConfig ?? null;
|
|
9374
|
+
this.getBackendsFn = deps?.getBackends ?? null;
|
|
8275
9375
|
}
|
|
8276
9376
|
wireEvents() {
|
|
8277
9377
|
this.stateChangeListener = (snapshot) => {
|
|
@@ -8282,6 +9382,12 @@ var OrchestratorServer = class {
|
|
|
8282
9382
|
};
|
|
8283
9383
|
this.orchestrator.on("state_change", this.stateChangeListener);
|
|
8284
9384
|
this.orchestrator.on("agent_event", this.agentEventListener);
|
|
9385
|
+
const bus = this.getRoutingDecisionBusFn?.() ?? null;
|
|
9386
|
+
if (bus) {
|
|
9387
|
+
this.routingDecisionUnsubscribe = bus.subscribe((decision) => {
|
|
9388
|
+
this.broadcaster.broadcast("routing:decision", decision);
|
|
9389
|
+
});
|
|
9390
|
+
}
|
|
8285
9391
|
}
|
|
8286
9392
|
/**
|
|
8287
9393
|
* Broadcast a new interaction to all WebSocket clients.
|
|
@@ -8437,6 +9543,23 @@ var OrchestratorServer = class {
|
|
|
8437
9543
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
8438
9544
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
8439
9545
|
}),
|
|
9546
|
+
// Spec B Phase 5 — routing observability. Returns 503 when the
|
|
9547
|
+
// backendFactory is null (legacy single-backend configs).
|
|
9548
|
+
(req, res) => handleV1RoutingRoute(req, res, {
|
|
9549
|
+
router: this.getBackendRouterFn?.() ?? null,
|
|
9550
|
+
bus: this.getRoutingDecisionBusFn?.() ?? null,
|
|
9551
|
+
routing: this.getRoutingConfigFn?.() ?? null,
|
|
9552
|
+
backends: this.getBackendsFn?.() ?? null
|
|
9553
|
+
}),
|
|
9554
|
+
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9555
|
+
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9556
|
+
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
9557
|
+
// business logic. `projectPath` defaults to process.cwd() — that is
|
|
9558
|
+
// where `.harness/proposals/` lives in every deployment we ship.
|
|
9559
|
+
(req, res) => handleV1ProposalsRoute(req, res, {
|
|
9560
|
+
projectPath: this.projectPath,
|
|
9561
|
+
bus: this.orchestrator
|
|
9562
|
+
}),
|
|
8440
9563
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
8441
9564
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
8442
9565
|
];
|
|
@@ -8524,17 +9647,21 @@ var OrchestratorServer = class {
|
|
|
8524
9647
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
8525
9648
|
this.planWatcher.start();
|
|
8526
9649
|
}
|
|
8527
|
-
return new Promise((
|
|
9650
|
+
return new Promise((resolve8) => {
|
|
8528
9651
|
const host = getBindHost();
|
|
8529
9652
|
this.httpServer.listen(this.port, host, () => {
|
|
8530
9653
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
8531
|
-
|
|
9654
|
+
resolve8();
|
|
8532
9655
|
});
|
|
8533
9656
|
});
|
|
8534
9657
|
}
|
|
8535
9658
|
stop() {
|
|
8536
9659
|
this.orchestrator.removeListener("state_change", this.stateChangeListener);
|
|
8537
9660
|
this.orchestrator.removeListener("agent_event", this.agentEventListener);
|
|
9661
|
+
if (this.routingDecisionUnsubscribe) {
|
|
9662
|
+
this.routingDecisionUnsubscribe();
|
|
9663
|
+
this.routingDecisionUnsubscribe = null;
|
|
9664
|
+
}
|
|
8538
9665
|
if (this.planWatcher) {
|
|
8539
9666
|
this.planWatcher.stop();
|
|
8540
9667
|
this.planWatcher = null;
|
|
@@ -8547,7 +9674,7 @@ var OrchestratorServer = class {
|
|
|
8547
9674
|
// src/gateway/webhooks/store.ts
|
|
8548
9675
|
import { randomBytes as randomBytes4 } from "crypto";
|
|
8549
9676
|
import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir9, rename as rename3, chmod } from "fs/promises";
|
|
8550
|
-
import { dirname as
|
|
9677
|
+
import { dirname as dirname7 } from "path";
|
|
8551
9678
|
import { WebhookSubscriptionSchema } from "@harness-engineering/types";
|
|
8552
9679
|
|
|
8553
9680
|
// src/gateway/webhooks/signer.ts
|
|
@@ -8578,8 +9705,8 @@ function genSecret2() {
|
|
|
8578
9705
|
return randomBytes4(32).toString("base64url");
|
|
8579
9706
|
}
|
|
8580
9707
|
var WebhookStore = class {
|
|
8581
|
-
constructor(
|
|
8582
|
-
this.path =
|
|
9708
|
+
constructor(path24) {
|
|
9709
|
+
this.path = path24;
|
|
8583
9710
|
}
|
|
8584
9711
|
path;
|
|
8585
9712
|
cache = null;
|
|
@@ -8602,7 +9729,7 @@ var WebhookStore = class {
|
|
|
8602
9729
|
async persist(records) {
|
|
8603
9730
|
const tmp = `${this.path}.tmp-${process.pid}-${Date.now()}-${randomBytes4(4).toString("hex")}`;
|
|
8604
9731
|
try {
|
|
8605
|
-
await mkdir9(
|
|
9732
|
+
await mkdir9(dirname7(this.path), { recursive: true });
|
|
8606
9733
|
await writeFile9(tmp, JSON.stringify(records, null, 2), { encoding: "utf8", mode: 384 });
|
|
8607
9734
|
await rename3(tmp, this.path);
|
|
8608
9735
|
await chmod(this.path, 384);
|
|
@@ -8970,7 +10097,12 @@ var WEBHOOK_TOPICS = [
|
|
|
8970
10097
|
"maintenance:completed",
|
|
8971
10098
|
"maintenance:error",
|
|
8972
10099
|
"webhook.subscription.created",
|
|
8973
|
-
"webhook.subscription.deleted"
|
|
10100
|
+
"webhook.subscription.deleted",
|
|
10101
|
+
// Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
|
|
10102
|
+
// `proposal.*` glob pattern to receive all three.
|
|
10103
|
+
"proposal.created",
|
|
10104
|
+
"proposal.approved",
|
|
10105
|
+
"proposal.rejected"
|
|
8974
10106
|
];
|
|
8975
10107
|
function newEventId2() {
|
|
8976
10108
|
return `evt_${randomBytes6(8).toString("hex")}`;
|
|
@@ -9395,6 +10527,33 @@ var ENVELOPE_DERIVERS = {
|
|
|
9395
10527
|
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9396
10528
|
severity: "info"
|
|
9397
10529
|
};
|
|
10530
|
+
},
|
|
10531
|
+
// Hermes Phase 4 — skill proposal lifecycle events.
|
|
10532
|
+
"proposal.created": (event) => {
|
|
10533
|
+
const data = asObj(event.data);
|
|
10534
|
+
const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
|
|
10535
|
+
return {
|
|
10536
|
+
title: `New skill proposal: ${label}`,
|
|
10537
|
+
summary: truncate(data.justification ?? "No justification provided.", 240),
|
|
10538
|
+
severity: "info"
|
|
10539
|
+
};
|
|
10540
|
+
},
|
|
10541
|
+
"proposal.approved": (event) => {
|
|
10542
|
+
const data = asObj(event.data);
|
|
10543
|
+
const label = data.name ?? data.targetSkill ?? "(unknown skill)";
|
|
10544
|
+
return {
|
|
10545
|
+
title: `Skill proposal approved: ${label}`,
|
|
10546
|
+
summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
|
|
10547
|
+
severity: "success"
|
|
10548
|
+
};
|
|
10549
|
+
},
|
|
10550
|
+
"proposal.rejected": (event) => {
|
|
10551
|
+
const data = asObj(event.data);
|
|
10552
|
+
return {
|
|
10553
|
+
title: "Skill proposal rejected",
|
|
10554
|
+
summary: truncate(data.reason ?? "No reason provided.", 240),
|
|
10555
|
+
severity: "warning"
|
|
10556
|
+
};
|
|
9398
10557
|
}
|
|
9399
10558
|
};
|
|
9400
10559
|
function truncate(s, max) {
|
|
@@ -9439,7 +10598,11 @@ var NOTIFICATION_TOPICS = [
|
|
|
9439
10598
|
"interaction.resolved",
|
|
9440
10599
|
"maintenance:started",
|
|
9441
10600
|
"maintenance:completed",
|
|
9442
|
-
"maintenance:error"
|
|
10601
|
+
"maintenance:error",
|
|
10602
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10603
|
+
"proposal.created",
|
|
10604
|
+
"proposal.approved",
|
|
10605
|
+
"proposal.rejected"
|
|
9443
10606
|
];
|
|
9444
10607
|
function newEventId4() {
|
|
9445
10608
|
return `evt_${randomBytes8(8).toString("hex")}`;
|
|
@@ -9535,8 +10698,8 @@ var StructuredLogger = class {
|
|
|
9535
10698
|
};
|
|
9536
10699
|
|
|
9537
10700
|
// src/workspace/config-scanner.ts
|
|
9538
|
-
import { existsSync as
|
|
9539
|
-
import { join as
|
|
10701
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
10702
|
+
import { join as join15, relative } from "path";
|
|
9540
10703
|
import {
|
|
9541
10704
|
scanForInjection,
|
|
9542
10705
|
SecurityScanner,
|
|
@@ -9560,10 +10723,10 @@ function adjustFindingSeverity(findings) {
|
|
|
9560
10723
|
});
|
|
9561
10724
|
}
|
|
9562
10725
|
async function scanSingleFile(filePath, targetDir, scanner) {
|
|
9563
|
-
if (!
|
|
10726
|
+
if (!existsSync6(filePath)) return null;
|
|
9564
10727
|
let content;
|
|
9565
10728
|
try {
|
|
9566
|
-
content =
|
|
10729
|
+
content = readFileSync6(filePath, "utf8");
|
|
9567
10730
|
} catch {
|
|
9568
10731
|
return null;
|
|
9569
10732
|
}
|
|
@@ -9582,7 +10745,7 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
9582
10745
|
const scanner = new SecurityScanner(parseSecurityConfig({}));
|
|
9583
10746
|
const results = [];
|
|
9584
10747
|
for (const configFile of CONFIG_FILES) {
|
|
9585
|
-
const result = await scanSingleFile(
|
|
10748
|
+
const result = await scanSingleFile(join15(workspacePath, configFile), workspacePath, scanner);
|
|
9586
10749
|
if (result) results.push(result);
|
|
9587
10750
|
}
|
|
9588
10751
|
return { exitCode: computeScanExitCode(results), results };
|
|
@@ -9768,6 +10931,19 @@ var BUILT_IN_TASKS = [
|
|
|
9768
10931
|
schedule: "*/15 * * * *",
|
|
9769
10932
|
branch: null,
|
|
9770
10933
|
checkCommand: ["harness", "sync-main", "--json"]
|
|
10934
|
+
},
|
|
10935
|
+
// Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
|
|
10936
|
+
// on every existing catalog skill. Schedule is Feb 31 (a date that never
|
|
10937
|
+
// exists) so the cron loop never fires it automatically; operators trigger
|
|
10938
|
+
// it once via the dashboard "Run now" button or `harness backfill-skill-
|
|
10939
|
+
// provenance` after upgrading to Phase 4.
|
|
10940
|
+
{
|
|
10941
|
+
id: "proposal-provenance-backfill",
|
|
10942
|
+
type: "housekeeping",
|
|
10943
|
+
description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
|
|
10944
|
+
schedule: "0 0 31 2 *",
|
|
10945
|
+
branch: null,
|
|
10946
|
+
checkCommand: ["backfill-skill-provenance"]
|
|
9771
10947
|
}
|
|
9772
10948
|
];
|
|
9773
10949
|
|
|
@@ -9860,24 +11036,49 @@ var MaintenanceScheduler = class {
|
|
|
9860
11036
|
this.resolvedTasks = this.resolveTasks();
|
|
9861
11037
|
}
|
|
9862
11038
|
/**
|
|
9863
|
-
* Merge built-in task definitions with config overrides
|
|
9864
|
-
*
|
|
9865
|
-
*
|
|
11039
|
+
* Merge built-in task definitions with config overrides, then append
|
|
11040
|
+
* Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
|
|
11041
|
+
* overrides). Tasks with `enabled: false` are filtered out. Schedule
|
|
11042
|
+
* overrides replace the default cron expression.
|
|
9866
11043
|
*/
|
|
9867
11044
|
resolveTasks() {
|
|
9868
11045
|
const overrides = this.config.tasks ?? {};
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
return true;
|
|
9873
|
-
}).map((task) => {
|
|
11046
|
+
const customs = this.config.customTasks ?? {};
|
|
11047
|
+
const merged = [];
|
|
11048
|
+
for (const task of BUILT_IN_TASKS) {
|
|
9874
11049
|
const override = overrides[task.id];
|
|
9875
|
-
if (
|
|
9876
|
-
|
|
11050
|
+
if (override?.enabled === false) continue;
|
|
11051
|
+
merged.push({
|
|
9877
11052
|
...task,
|
|
9878
|
-
...override
|
|
9879
|
-
};
|
|
9880
|
-
}
|
|
11053
|
+
...override?.schedule !== void 0 && { schedule: override.schedule }
|
|
11054
|
+
});
|
|
11055
|
+
}
|
|
11056
|
+
for (const [id, def] of Object.entries(customs)) {
|
|
11057
|
+
const override = overrides[id];
|
|
11058
|
+
if (override?.enabled === false) continue;
|
|
11059
|
+
merged.push({
|
|
11060
|
+
id,
|
|
11061
|
+
type: def.type,
|
|
11062
|
+
description: def.description,
|
|
11063
|
+
schedule: override?.schedule ?? def.schedule,
|
|
11064
|
+
branch: def.branch,
|
|
11065
|
+
...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
|
|
11066
|
+
...def.checkScript !== void 0 && { checkScript: def.checkScript },
|
|
11067
|
+
...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
|
|
11068
|
+
...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
|
|
11069
|
+
...def.inlineSkillsBudgetTokens !== void 0 && {
|
|
11070
|
+
inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
|
|
11071
|
+
},
|
|
11072
|
+
...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
|
|
11073
|
+
...def.contextFromMaxAgeMinutes !== void 0 && {
|
|
11074
|
+
contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
|
|
11075
|
+
},
|
|
11076
|
+
...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
|
|
11077
|
+
...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
|
|
11078
|
+
isCustom: true
|
|
11079
|
+
});
|
|
11080
|
+
}
|
|
11081
|
+
return merged;
|
|
9881
11082
|
}
|
|
9882
11083
|
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
9883
11084
|
getResolvedTasks() {
|
|
@@ -10058,19 +11259,19 @@ var SingleProcessLeaderElector = class {
|
|
|
10058
11259
|
};
|
|
10059
11260
|
|
|
10060
11261
|
// src/maintenance/reporter.ts
|
|
10061
|
-
import * as
|
|
10062
|
-
import * as
|
|
10063
|
-
import { z as
|
|
10064
|
-
var RunResultSchema =
|
|
10065
|
-
taskId:
|
|
10066
|
-
startedAt:
|
|
10067
|
-
completedAt:
|
|
10068
|
-
status:
|
|
10069
|
-
findings:
|
|
10070
|
-
fixed:
|
|
10071
|
-
prUrl:
|
|
10072
|
-
prUpdated:
|
|
10073
|
-
error:
|
|
11262
|
+
import * as fs16 from "fs";
|
|
11263
|
+
import * as path18 from "path";
|
|
11264
|
+
import { z as z17 } from "zod";
|
|
11265
|
+
var RunResultSchema = z17.object({
|
|
11266
|
+
taskId: z17.string(),
|
|
11267
|
+
startedAt: z17.string(),
|
|
11268
|
+
completedAt: z17.string(),
|
|
11269
|
+
status: z17.enum(["success", "failure", "skipped", "no-issues"]),
|
|
11270
|
+
findings: z17.number(),
|
|
11271
|
+
fixed: z17.number(),
|
|
11272
|
+
prUrl: z17.string().nullable(),
|
|
11273
|
+
prUpdated: z17.boolean(),
|
|
11274
|
+
error: z17.string().optional()
|
|
10074
11275
|
});
|
|
10075
11276
|
var MAX_HISTORY = 500;
|
|
10076
11277
|
var fallbackLogger = {
|
|
@@ -10094,10 +11295,10 @@ var MaintenanceReporter = class {
|
|
|
10094
11295
|
*/
|
|
10095
11296
|
async load() {
|
|
10096
11297
|
try {
|
|
10097
|
-
await
|
|
10098
|
-
const filePath =
|
|
10099
|
-
const data = await
|
|
10100
|
-
const parsed =
|
|
11298
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11299
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11300
|
+
const data = await fs16.promises.readFile(filePath, "utf-8");
|
|
11301
|
+
const parsed = z17.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
10101
11302
|
if (parsed.success) {
|
|
10102
11303
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10103
11304
|
}
|
|
@@ -10130,9 +11331,9 @@ var MaintenanceReporter = class {
|
|
|
10130
11331
|
*/
|
|
10131
11332
|
async persist() {
|
|
10132
11333
|
try {
|
|
10133
|
-
await
|
|
10134
|
-
const filePath =
|
|
10135
|
-
await
|
|
11334
|
+
await fs16.promises.mkdir(this.persistDir, { recursive: true });
|
|
11335
|
+
const filePath = path18.join(this.persistDir, "history.json");
|
|
11336
|
+
await fs16.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
10136
11337
|
} catch (err) {
|
|
10137
11338
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10138
11339
|
}
|
|
@@ -10148,6 +11349,9 @@ var TaskRunner = class {
|
|
|
10148
11349
|
cwd;
|
|
10149
11350
|
prManager;
|
|
10150
11351
|
baseBranch;
|
|
11352
|
+
checkScriptRunner;
|
|
11353
|
+
contextResolver;
|
|
11354
|
+
outputStore;
|
|
10151
11355
|
constructor(options) {
|
|
10152
11356
|
this.config = options.config;
|
|
10153
11357
|
this.checkRunner = options.checkRunner;
|
|
@@ -10156,27 +11360,49 @@ var TaskRunner = class {
|
|
|
10156
11360
|
this.cwd = options.cwd;
|
|
10157
11361
|
this.prManager = options.prManager ?? null;
|
|
10158
11362
|
this.baseBranch = options.baseBranch ?? "main";
|
|
11363
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
11364
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
11365
|
+
this.outputStore = options.outputStore ?? null;
|
|
10159
11366
|
}
|
|
10160
11367
|
/**
|
|
10161
11368
|
* Run a maintenance task and return the result.
|
|
10162
11369
|
* Dispatches to the appropriate execution path based on task type.
|
|
10163
11370
|
* Never throws -- errors are captured in the RunResult.
|
|
11371
|
+
*
|
|
11372
|
+
* @param task - Resolved task definition.
|
|
11373
|
+
* @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
|
|
11374
|
+
* when called from the scheduler path.
|
|
10164
11375
|
*/
|
|
10165
|
-
async run(task) {
|
|
11376
|
+
async run(task, origin = "cron") {
|
|
10166
11377
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11378
|
+
let result;
|
|
11379
|
+
let captured;
|
|
10167
11380
|
try {
|
|
10168
11381
|
switch (task.type) {
|
|
10169
|
-
case "mechanical-ai":
|
|
10170
|
-
|
|
11382
|
+
case "mechanical-ai": {
|
|
11383
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
11384
|
+
result = out.result;
|
|
11385
|
+
captured = out.captured;
|
|
11386
|
+
break;
|
|
11387
|
+
}
|
|
10171
11388
|
case "pure-ai":
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
11389
|
+
result = await this.runPureAI(task, startedAt);
|
|
11390
|
+
break;
|
|
11391
|
+
case "report-only": {
|
|
11392
|
+
const out = await this.runReportOnly(task, startedAt);
|
|
11393
|
+
result = out.result;
|
|
11394
|
+
captured = out.captured;
|
|
11395
|
+
break;
|
|
11396
|
+
}
|
|
11397
|
+
case "housekeeping": {
|
|
11398
|
+
const out = await this.runHousekeeping(task, startedAt);
|
|
11399
|
+
result = out.result;
|
|
11400
|
+
captured = out.captured;
|
|
11401
|
+
break;
|
|
11402
|
+
}
|
|
10177
11403
|
default: {
|
|
10178
11404
|
const _exhaustive = task.type;
|
|
10179
|
-
|
|
11405
|
+
result = this.failureResult(
|
|
10180
11406
|
task.id,
|
|
10181
11407
|
startedAt,
|
|
10182
11408
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -10184,69 +11410,174 @@ var TaskRunner = class {
|
|
|
10184
11410
|
}
|
|
10185
11411
|
}
|
|
10186
11412
|
} catch (err) {
|
|
10187
|
-
|
|
11413
|
+
result = this.failureResult(task.id, startedAt, String(err));
|
|
11414
|
+
}
|
|
11415
|
+
result.origin = origin;
|
|
11416
|
+
await this.persistOutput(task, result, captured, origin);
|
|
11417
|
+
return result;
|
|
11418
|
+
}
|
|
11419
|
+
async persistOutput(task, result, captured, origin) {
|
|
11420
|
+
if (!this.outputStore) return;
|
|
11421
|
+
const entry = {
|
|
11422
|
+
taskId: result.taskId,
|
|
11423
|
+
startedAt: result.startedAt,
|
|
11424
|
+
completedAt: result.completedAt,
|
|
11425
|
+
status: result.status,
|
|
11426
|
+
findings: result.findings,
|
|
11427
|
+
fixed: result.fixed,
|
|
11428
|
+
prUrl: result.prUrl,
|
|
11429
|
+
prUpdated: result.prUpdated,
|
|
11430
|
+
origin,
|
|
11431
|
+
...result.error !== void 0 && { error: result.error },
|
|
11432
|
+
...result.costUsd !== void 0 && { costUsd: result.costUsd },
|
|
11433
|
+
...captured?.stdout !== void 0 && { stdout: captured.stdout },
|
|
11434
|
+
...captured?.stderr !== void 0 && { stderr: captured.stderr },
|
|
11435
|
+
...captured?.structured !== void 0 && { structured: captured.structured },
|
|
11436
|
+
...captured?.context !== void 0 && { context: captured.context }
|
|
11437
|
+
};
|
|
11438
|
+
try {
|
|
11439
|
+
await this.outputStore.write(task.id, entry, task.outputRetention);
|
|
11440
|
+
} catch {
|
|
10188
11441
|
}
|
|
10189
11442
|
}
|
|
10190
11443
|
/**
|
|
10191
|
-
*
|
|
11444
|
+
* Run the check step using whichever runner the task asks for. Custom
|
|
11445
|
+
* tasks that declare `checkScript` go through the Hermes Phase 2
|
|
11446
|
+
* `CheckScriptRunner`; built-ins (and customs that use the legacy
|
|
11447
|
+
* `checkCommand` shape) go through the original heuristic runner.
|
|
10192
11448
|
*/
|
|
10193
|
-
async
|
|
11449
|
+
async runCheckStep(task) {
|
|
11450
|
+
if (task.checkScript) {
|
|
11451
|
+
if (!this.checkScriptRunner) {
|
|
11452
|
+
throw new Error(
|
|
11453
|
+
`task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
|
|
11454
|
+
);
|
|
11455
|
+
}
|
|
11456
|
+
const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
|
|
11457
|
+
return {
|
|
11458
|
+
passed: r2.passed,
|
|
11459
|
+
findings: r2.findings,
|
|
11460
|
+
stdout: r2.output,
|
|
11461
|
+
stderr: r2.stderr,
|
|
11462
|
+
structured: r2.structured ? r2.structured : null
|
|
11463
|
+
};
|
|
11464
|
+
}
|
|
10194
11465
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
10195
|
-
|
|
11466
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
10196
11467
|
}
|
|
11468
|
+
const r = await this.checkRunner.run(task.checkCommand, this.cwd);
|
|
11469
|
+
return {
|
|
11470
|
+
passed: r.passed,
|
|
11471
|
+
findings: r.findings,
|
|
11472
|
+
stdout: r.output,
|
|
11473
|
+
stderr: "",
|
|
11474
|
+
structured: null
|
|
11475
|
+
};
|
|
11476
|
+
}
|
|
11477
|
+
/**
|
|
11478
|
+
* Hermes Phase 2 — Compose the agent prompt-context block from inlined
|
|
11479
|
+
* skills + upstream task outputs. Returns an empty string when nothing
|
|
11480
|
+
* is configured (or when the resolver is absent), which is the safe
|
|
11481
|
+
* no-op default.
|
|
11482
|
+
*/
|
|
11483
|
+
async composePromptContext(task) {
|
|
11484
|
+
if (!this.contextResolver) return "";
|
|
11485
|
+
const skills = await this.contextResolver.resolveInlineSkills(
|
|
11486
|
+
task.inlineSkills,
|
|
11487
|
+
task.inlineSkillsBudgetTokens ?? 8e3
|
|
11488
|
+
);
|
|
11489
|
+
const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
|
|
11490
|
+
maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
|
|
11491
|
+
});
|
|
11492
|
+
return [skills, upstream].filter(Boolean).join("\n");
|
|
11493
|
+
}
|
|
11494
|
+
/**
|
|
11495
|
+
* Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
|
|
11496
|
+
* only if fixable findings exist; persist captured stdout/stderr/context
|
|
11497
|
+
* via the output store on the way out.
|
|
11498
|
+
*/
|
|
11499
|
+
async runMechanicalAI(task, startedAt) {
|
|
10197
11500
|
if (!task.fixSkill) {
|
|
10198
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
11501
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
10199
11502
|
}
|
|
10200
11503
|
if (!task.branch) {
|
|
10201
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
11504
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
11505
|
+
}
|
|
11506
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11507
|
+
return wrap(
|
|
11508
|
+
this.failureResult(
|
|
11509
|
+
task.id,
|
|
11510
|
+
startedAt,
|
|
11511
|
+
"mechanical-ai task missing checkCommand or checkScript"
|
|
11512
|
+
)
|
|
11513
|
+
);
|
|
10202
11514
|
}
|
|
10203
|
-
|
|
10204
|
-
|
|
11515
|
+
let check;
|
|
11516
|
+
try {
|
|
11517
|
+
check = await this.runCheckStep(task);
|
|
11518
|
+
} catch (err) {
|
|
11519
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11520
|
+
}
|
|
11521
|
+
const promptContext = await this.composePromptContext(task);
|
|
11522
|
+
const baseCaptured = {
|
|
11523
|
+
stdout: check.stdout,
|
|
11524
|
+
stderr: check.stderr,
|
|
11525
|
+
structured: check.structured,
|
|
11526
|
+
...promptContext ? { context: promptContext } : {}
|
|
11527
|
+
};
|
|
11528
|
+
const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
|
|
11529
|
+
if (check.findings === 0 || wakeAgentExplicitlyFalse) {
|
|
10205
11530
|
return {
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
11531
|
+
result: {
|
|
11532
|
+
taskId: task.id,
|
|
11533
|
+
startedAt,
|
|
11534
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11535
|
+
status: "no-issues",
|
|
11536
|
+
findings: check.findings,
|
|
11537
|
+
fixed: 0,
|
|
11538
|
+
prUrl: null,
|
|
11539
|
+
prUpdated: false
|
|
11540
|
+
},
|
|
11541
|
+
captured: baseCaptured
|
|
10214
11542
|
};
|
|
10215
11543
|
}
|
|
10216
11544
|
if (this.prManager) {
|
|
10217
11545
|
try {
|
|
10218
11546
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
10219
11547
|
} catch (err) {
|
|
10220
|
-
return
|
|
11548
|
+
return wrap(
|
|
11549
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11550
|
+
baseCaptured
|
|
11551
|
+
);
|
|
10221
11552
|
}
|
|
10222
11553
|
}
|
|
10223
11554
|
const backendName = this.resolveBackend(task.id);
|
|
10224
11555
|
let agentResult;
|
|
10225
11556
|
try {
|
|
10226
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
backendName,
|
|
10230
|
-
this.cwd
|
|
10231
|
-
);
|
|
11557
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11558
|
+
promptContext
|
|
11559
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10232
11560
|
} catch (err) {
|
|
10233
11561
|
return {
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
11562
|
+
result: {
|
|
11563
|
+
taskId: task.id,
|
|
11564
|
+
startedAt,
|
|
11565
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11566
|
+
status: "failure",
|
|
11567
|
+
findings: check.findings,
|
|
11568
|
+
fixed: 0,
|
|
11569
|
+
prUrl: null,
|
|
11570
|
+
prUpdated: false,
|
|
11571
|
+
error: `Agent dispatch failed: ${String(err)}`
|
|
11572
|
+
},
|
|
11573
|
+
captured: baseCaptured
|
|
10243
11574
|
};
|
|
10244
11575
|
}
|
|
10245
11576
|
let prUrl = null;
|
|
10246
11577
|
let prUpdated = false;
|
|
10247
11578
|
if (this.prManager && agentResult.producedCommits) {
|
|
10248
11579
|
try {
|
|
10249
|
-
const summary = `Findings: ${
|
|
11580
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
10250
11581
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
10251
11582
|
prUrl = prResult.prUrl;
|
|
10252
11583
|
prUpdated = prResult.prUpdated;
|
|
@@ -10255,14 +11586,17 @@ var TaskRunner = class {
|
|
|
10255
11586
|
}
|
|
10256
11587
|
}
|
|
10257
11588
|
return {
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
11589
|
+
result: {
|
|
11590
|
+
taskId: task.id,
|
|
11591
|
+
startedAt,
|
|
11592
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11593
|
+
status: "success",
|
|
11594
|
+
findings: check.findings,
|
|
11595
|
+
fixed: agentResult.fixed,
|
|
11596
|
+
prUrl,
|
|
11597
|
+
prUpdated
|
|
11598
|
+
},
|
|
11599
|
+
captured: baseCaptured
|
|
10266
11600
|
};
|
|
10267
11601
|
}
|
|
10268
11602
|
/**
|
|
@@ -10282,15 +11616,13 @@ var TaskRunner = class {
|
|
|
10282
11616
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
10283
11617
|
}
|
|
10284
11618
|
}
|
|
11619
|
+
const promptContext = await this.composePromptContext(task);
|
|
10285
11620
|
const backendName = this.resolveBackend(task.id);
|
|
10286
11621
|
let agentResult;
|
|
10287
11622
|
try {
|
|
10288
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
backendName,
|
|
10292
|
-
this.cwd
|
|
10293
|
-
);
|
|
11623
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11624
|
+
promptContext
|
|
11625
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10294
11626
|
} catch (err) {
|
|
10295
11627
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
10296
11628
|
}
|
|
@@ -10318,7 +11650,7 @@ var TaskRunner = class {
|
|
|
10318
11650
|
};
|
|
10319
11651
|
}
|
|
10320
11652
|
/**
|
|
10321
|
-
* Report-only: run check
|
|
11653
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
10322
11654
|
*
|
|
10323
11655
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
10324
11656
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -10328,13 +11660,24 @@ var TaskRunner = class {
|
|
|
10328
11660
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
10329
11661
|
*/
|
|
10330
11662
|
async runReportOnly(task, startedAt) {
|
|
10331
|
-
if (!task.checkCommand
|
|
10332
|
-
return
|
|
11663
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11664
|
+
return wrap(
|
|
11665
|
+
this.failureResult(
|
|
11666
|
+
task.id,
|
|
11667
|
+
startedAt,
|
|
11668
|
+
"report-only task missing checkCommand or checkScript"
|
|
11669
|
+
)
|
|
11670
|
+
);
|
|
10333
11671
|
}
|
|
10334
|
-
|
|
10335
|
-
|
|
11672
|
+
let check;
|
|
11673
|
+
try {
|
|
11674
|
+
check = await this.runCheckStep(task);
|
|
11675
|
+
} catch (err) {
|
|
11676
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11677
|
+
}
|
|
11678
|
+
const parsed = parseStatusLine(check.stdout);
|
|
10336
11679
|
const status = parsed?.status ?? "success";
|
|
10337
|
-
const findings = parsed === null ?
|
|
11680
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
10338
11681
|
const result = {
|
|
10339
11682
|
taskId: task.id,
|
|
10340
11683
|
startedAt,
|
|
@@ -10348,7 +11691,10 @@ var TaskRunner = class {
|
|
|
10348
11691
|
if (parsed?.error) {
|
|
10349
11692
|
result.error = parsed.error;
|
|
10350
11693
|
}
|
|
10351
|
-
return
|
|
11694
|
+
return {
|
|
11695
|
+
result,
|
|
11696
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11697
|
+
};
|
|
10352
11698
|
}
|
|
10353
11699
|
/**
|
|
10354
11700
|
* Housekeeping: run command directly, no AI, no PR.
|
|
@@ -10359,17 +11705,39 @@ var TaskRunner = class {
|
|
|
10359
11705
|
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
10360
11706
|
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
10361
11707
|
* status: 'success', findings: 0.
|
|
11708
|
+
*
|
|
11709
|
+
* Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
|
|
11710
|
+
* tasks; the runner falls through to the same JSON-status parsing path.
|
|
10362
11711
|
*/
|
|
10363
11712
|
async runHousekeeping(task, startedAt) {
|
|
10364
|
-
if (!task.checkCommand
|
|
10365
|
-
return
|
|
11713
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11714
|
+
return wrap(
|
|
11715
|
+
this.failureResult(
|
|
11716
|
+
task.id,
|
|
11717
|
+
startedAt,
|
|
11718
|
+
"housekeeping task missing checkCommand or checkScript"
|
|
11719
|
+
)
|
|
11720
|
+
);
|
|
10366
11721
|
}
|
|
10367
11722
|
let stdout;
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
11723
|
+
let stderr = "";
|
|
11724
|
+
let structured = null;
|
|
11725
|
+
if (task.checkScript) {
|
|
11726
|
+
try {
|
|
11727
|
+
const r = await this.runCheckStep(task);
|
|
11728
|
+
stdout = r.stdout;
|
|
11729
|
+
stderr = r.stderr;
|
|
11730
|
+
structured = r.structured;
|
|
11731
|
+
} catch (err) {
|
|
11732
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11733
|
+
}
|
|
11734
|
+
} else {
|
|
11735
|
+
try {
|
|
11736
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
11737
|
+
stdout = out.stdout ?? "";
|
|
11738
|
+
} catch (err) {
|
|
11739
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11740
|
+
}
|
|
10373
11741
|
}
|
|
10374
11742
|
const parsed = parseStatusLine(stdout);
|
|
10375
11743
|
const status = parsed?.status ?? "success";
|
|
@@ -10384,7 +11752,7 @@ var TaskRunner = class {
|
|
|
10384
11752
|
prUpdated: false
|
|
10385
11753
|
};
|
|
10386
11754
|
if (parsed?.error) result.error = parsed.error;
|
|
10387
|
-
return result;
|
|
11755
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
10388
11756
|
}
|
|
10389
11757
|
/**
|
|
10390
11758
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -10409,6 +11777,9 @@ var TaskRunner = class {
|
|
|
10409
11777
|
};
|
|
10410
11778
|
}
|
|
10411
11779
|
};
|
|
11780
|
+
function wrap(result, captured) {
|
|
11781
|
+
return captured ? { result, captured } : { result };
|
|
11782
|
+
}
|
|
10412
11783
|
function parseStatusLine(output) {
|
|
10413
11784
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10414
11785
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -10446,12 +11817,561 @@ function parseStatusLine(output) {
|
|
|
10446
11817
|
return null;
|
|
10447
11818
|
}
|
|
10448
11819
|
|
|
10449
|
-
// src/
|
|
10450
|
-
|
|
10451
|
-
|
|
10452
|
-
|
|
10453
|
-
|
|
11820
|
+
// src/maintenance/check-script-runner.ts
|
|
11821
|
+
import { execFile as execFile6 } from "child_process";
|
|
11822
|
+
import { promisify as promisify3 } from "util";
|
|
11823
|
+
import * as path19 from "path";
|
|
11824
|
+
var execFileAsync = promisify3(execFile6);
|
|
11825
|
+
var CheckScriptRunner = class {
|
|
11826
|
+
constructor(cwd) {
|
|
11827
|
+
this.cwd = cwd;
|
|
11828
|
+
}
|
|
11829
|
+
cwd;
|
|
11830
|
+
async run(spec, cwd) {
|
|
11831
|
+
const projectRoot = cwd ?? this.cwd;
|
|
11832
|
+
const captured = await captureScript(spec, projectRoot);
|
|
11833
|
+
const parseJson = spec.parseStdoutJson !== false;
|
|
11834
|
+
const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
|
|
11835
|
+
if (structured) {
|
|
11836
|
+
return mapStructured(structured, captured.stdout, captured.stderr);
|
|
11837
|
+
}
|
|
11838
|
+
return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
|
|
11839
|
+
}
|
|
11840
|
+
};
|
|
11841
|
+
async function captureScript(spec, projectRoot) {
|
|
11842
|
+
const resolved = path19.isAbsolute(spec.path) ? spec.path : path19.resolve(projectRoot, spec.path);
|
|
11843
|
+
const args = spec.args ?? [];
|
|
11844
|
+
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11845
|
+
try {
|
|
11846
|
+
const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
|
|
11847
|
+
return {
|
|
11848
|
+
stdout: String(result.stdout ?? ""),
|
|
11849
|
+
stderr: String(result.stderr ?? ""),
|
|
11850
|
+
exitedAbnormally: false
|
|
11851
|
+
};
|
|
11852
|
+
} catch (err) {
|
|
11853
|
+
const e = err;
|
|
11854
|
+
return {
|
|
11855
|
+
stdout: String(e.stdout ?? ""),
|
|
11856
|
+
stderr: String(e.stderr ?? ""),
|
|
11857
|
+
exitedAbnormally: true
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11860
|
+
}
|
|
11861
|
+
function parseStatusEnvelope(stdout) {
|
|
11862
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11863
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11864
|
+
const env = classifyLine2(lines[i]);
|
|
11865
|
+
if (env) return env;
|
|
11866
|
+
}
|
|
11867
|
+
return null;
|
|
11868
|
+
}
|
|
11869
|
+
var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
|
|
11870
|
+
function classifyLine2(line) {
|
|
11871
|
+
const obj = tryParseJsonObject(line);
|
|
11872
|
+
if (!obj) return null;
|
|
11873
|
+
const s = obj.status;
|
|
11874
|
+
if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
|
|
11875
|
+
return buildEnvelope(s, obj);
|
|
11876
|
+
}
|
|
11877
|
+
function tryParseJsonObject(line) {
|
|
11878
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
|
|
11879
|
+
try {
|
|
11880
|
+
return JSON.parse(line);
|
|
11881
|
+
} catch {
|
|
11882
|
+
return null;
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
function buildEnvelope(status, obj) {
|
|
11886
|
+
const env = { status };
|
|
11887
|
+
if (typeof obj.findings === "number") env.findings = obj.findings;
|
|
11888
|
+
if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
|
|
11889
|
+
if (typeof obj.message === "string") env.message = obj.message;
|
|
11890
|
+
if (obj.outputs && typeof obj.outputs === "object") {
|
|
11891
|
+
env.outputs = obj.outputs;
|
|
11892
|
+
}
|
|
11893
|
+
return env;
|
|
11894
|
+
}
|
|
11895
|
+
function mapStructured(env, stdout, stderr) {
|
|
11896
|
+
const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
|
|
11897
|
+
switch (env.status) {
|
|
11898
|
+
case "ok":
|
|
11899
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11900
|
+
case "findings": {
|
|
11901
|
+
const wake = env.wakeAgent ?? findings > 0;
|
|
11902
|
+
return { passed: !wake, findings, output: stdout, stderr, structured: env };
|
|
11903
|
+
}
|
|
11904
|
+
case "skip":
|
|
11905
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11906
|
+
case "error":
|
|
11907
|
+
return {
|
|
11908
|
+
passed: false,
|
|
11909
|
+
findings: Math.max(findings, 1),
|
|
11910
|
+
output: stdout,
|
|
11911
|
+
stderr,
|
|
11912
|
+
structured: env
|
|
11913
|
+
};
|
|
11914
|
+
default:
|
|
11915
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11916
|
+
}
|
|
11917
|
+
}
|
|
11918
|
+
function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
11919
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
11920
|
+
const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
11921
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
|
|
11922
|
+
return {
|
|
11923
|
+
passed: findings === 0 && !exitedAbnormally,
|
|
11924
|
+
findings,
|
|
11925
|
+
output: stdout,
|
|
11926
|
+
stderr,
|
|
11927
|
+
structured: null
|
|
11928
|
+
};
|
|
11929
|
+
}
|
|
11930
|
+
|
|
11931
|
+
// src/maintenance/output-store.ts
|
|
11932
|
+
import * as fs17 from "fs";
|
|
11933
|
+
import * as path20 from "path";
|
|
11934
|
+
var DEFAULT_RETENTION = {
|
|
11935
|
+
runs: 50,
|
|
11936
|
+
maxAgeDays: 30
|
|
11937
|
+
};
|
|
11938
|
+
var fallbackLogger2 = {
|
|
11939
|
+
info: () => {
|
|
11940
|
+
},
|
|
11941
|
+
warn: (m, c) => console.warn(m, c),
|
|
11942
|
+
error: (m, c) => console.error(m, c)
|
|
11943
|
+
};
|
|
11944
|
+
var TaskOutputStore = class {
|
|
11945
|
+
rootDir;
|
|
11946
|
+
retentionDefaults;
|
|
11947
|
+
logger;
|
|
11948
|
+
constructor(options) {
|
|
11949
|
+
this.rootDir = options.rootDir;
|
|
11950
|
+
this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
|
|
11951
|
+
this.logger = options.logger ?? fallbackLogger2;
|
|
11952
|
+
}
|
|
11953
|
+
/**
|
|
11954
|
+
* Reject task IDs that don't match the validator's kebab-case pattern —
|
|
11955
|
+
* defends `dirFor()` against caller-supplied path-traversal segments
|
|
11956
|
+
* (`'../foo'`) when the store is invoked from CLI surfaces that don't
|
|
11957
|
+
* round-trip through `validateCustomTasks`.
|
|
11958
|
+
*/
|
|
11959
|
+
ensureSafeTaskId(taskId) {
|
|
11960
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
|
|
11961
|
+
throw new Error(
|
|
11962
|
+
`TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
|
|
11963
|
+
);
|
|
11964
|
+
}
|
|
11965
|
+
}
|
|
11966
|
+
/**
|
|
11967
|
+
* Persist a single run entry. Retention is applied after the write so
|
|
11968
|
+
* the latest record is durable even if pruning fails.
|
|
11969
|
+
*/
|
|
11970
|
+
async write(taskId, entry, retention) {
|
|
11971
|
+
this.ensureSafeTaskId(taskId);
|
|
11972
|
+
const dir = this.dirFor(taskId);
|
|
11973
|
+
await fs17.promises.mkdir(dir, { recursive: true });
|
|
11974
|
+
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11975
|
+
const filePath = path20.join(dir, fileName);
|
|
11976
|
+
const tmpPath = `${filePath}.tmp`;
|
|
11977
|
+
const payload = JSON.stringify(entry, null, 2);
|
|
11978
|
+
await fs17.promises.writeFile(tmpPath, payload, "utf-8");
|
|
11979
|
+
await fs17.promises.rename(tmpPath, filePath);
|
|
11980
|
+
try {
|
|
11981
|
+
await this.applyRetention(taskId, retention);
|
|
11982
|
+
} catch (err) {
|
|
11983
|
+
this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
|
|
11984
|
+
}
|
|
11985
|
+
}
|
|
11986
|
+
/**
|
|
11987
|
+
* Return the most recent persisted entry for the task, or null if none.
|
|
11988
|
+
*/
|
|
11989
|
+
async latest(taskId) {
|
|
11990
|
+
const entries = await this.list(taskId, 1, 0);
|
|
11991
|
+
return entries[0] ?? null;
|
|
11992
|
+
}
|
|
11993
|
+
/**
|
|
11994
|
+
* List entries newest-first with offset+limit pagination.
|
|
11995
|
+
*/
|
|
11996
|
+
async list(taskId, limit, offset) {
|
|
11997
|
+
this.ensureSafeTaskId(taskId);
|
|
11998
|
+
const dir = this.dirFor(taskId);
|
|
11999
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
12000
|
+
const slice = fileNames.slice(offset, offset + limit);
|
|
12001
|
+
const out = [];
|
|
12002
|
+
for (const name of slice) {
|
|
12003
|
+
const entry = await this.readEntry(path20.join(dir, name));
|
|
12004
|
+
if (entry) out.push(entry);
|
|
12005
|
+
}
|
|
12006
|
+
return out;
|
|
12007
|
+
}
|
|
12008
|
+
/**
|
|
12009
|
+
* Lookup a specific run by its file name (without the `.json` suffix) or
|
|
12010
|
+
* by its raw completion timestamp.
|
|
12011
|
+
*/
|
|
12012
|
+
async get(taskId, runId) {
|
|
12013
|
+
this.ensureSafeTaskId(taskId);
|
|
12014
|
+
if (/[\\/]|\.\./.test(runId)) {
|
|
12015
|
+
throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
|
|
12016
|
+
}
|
|
12017
|
+
const dir = this.dirFor(taskId);
|
|
12018
|
+
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
12019
|
+
return this.readEntry(path20.join(dir, fileName));
|
|
12020
|
+
}
|
|
12021
|
+
/**
|
|
12022
|
+
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
12023
|
+
* outputs from outside the store API.
|
|
12024
|
+
*/
|
|
12025
|
+
dirFor(taskId) {
|
|
12026
|
+
return path20.join(this.rootDir, taskId, "outputs");
|
|
12027
|
+
}
|
|
12028
|
+
async readEntry(filePath) {
|
|
12029
|
+
try {
|
|
12030
|
+
const buf = await fs17.promises.readFile(filePath, "utf-8");
|
|
12031
|
+
const parsed = JSON.parse(buf);
|
|
12032
|
+
return parsed;
|
|
12033
|
+
} catch {
|
|
12034
|
+
return null;
|
|
12035
|
+
}
|
|
12036
|
+
}
|
|
12037
|
+
async applyRetention(taskId, retention) {
|
|
12038
|
+
const runs = retention?.runs ?? this.retentionDefaults.runs;
|
|
12039
|
+
const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
|
|
12040
|
+
const dir = this.dirFor(taskId);
|
|
12041
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
12042
|
+
const overflow = fileNames.slice(runs);
|
|
12043
|
+
const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
12044
|
+
const aged = [];
|
|
12045
|
+
for (const name of fileNames) {
|
|
12046
|
+
const ts = parseIsoFromFileName(name);
|
|
12047
|
+
if (ts !== null && ts < cutoffMs) aged.push(name);
|
|
12048
|
+
}
|
|
12049
|
+
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
12050
|
+
for (const name of toRemove) {
|
|
12051
|
+
try {
|
|
12052
|
+
await fs17.promises.unlink(path20.join(dir, name));
|
|
12053
|
+
} catch {
|
|
12054
|
+
}
|
|
12055
|
+
}
|
|
12056
|
+
}
|
|
12057
|
+
};
|
|
12058
|
+
async function listJsonFilesDescending(dir) {
|
|
12059
|
+
let names;
|
|
12060
|
+
try {
|
|
12061
|
+
names = await fs17.promises.readdir(dir);
|
|
12062
|
+
} catch {
|
|
12063
|
+
return [];
|
|
12064
|
+
}
|
|
12065
|
+
return names.filter((n) => n.endsWith(".json")).sort().reverse();
|
|
12066
|
+
}
|
|
12067
|
+
function sanitizeIso(iso) {
|
|
12068
|
+
return iso.replace(/:/g, "-");
|
|
12069
|
+
}
|
|
12070
|
+
function parseIsoFromFileName(fileName) {
|
|
12071
|
+
const stem = fileName.replace(/\.json$/, "");
|
|
12072
|
+
const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
|
|
12073
|
+
const ms = Date.parse(restored);
|
|
12074
|
+
return Number.isFinite(ms) ? ms : null;
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12077
|
+
// src/maintenance/context-resolver.ts
|
|
12078
|
+
var ContextResolver = class {
|
|
12079
|
+
outputStore;
|
|
12080
|
+
skillReader;
|
|
12081
|
+
logger;
|
|
12082
|
+
perUpstreamMaxChars;
|
|
12083
|
+
constructor(options) {
|
|
12084
|
+
this.outputStore = options.outputStore;
|
|
12085
|
+
this.skillReader = options.skillReader ?? null;
|
|
12086
|
+
this.logger = options.logger ?? fallbackLogger3;
|
|
12087
|
+
this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
|
|
12088
|
+
}
|
|
12089
|
+
async resolveContextFrom(upstreamTaskIds, options = {}) {
|
|
12090
|
+
if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
|
|
12091
|
+
const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
|
|
12092
|
+
const now = Date.now();
|
|
12093
|
+
const sections = [];
|
|
12094
|
+
for (const id of upstreamTaskIds) {
|
|
12095
|
+
const entry = await this.outputStore.latest(id);
|
|
12096
|
+
sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
|
|
12097
|
+
}
|
|
12098
|
+
return `## Upstream context
|
|
12099
|
+
|
|
12100
|
+
${sections.join("\n\n")}
|
|
12101
|
+
`;
|
|
12102
|
+
}
|
|
12103
|
+
async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
|
|
12104
|
+
if (!skillNames || skillNames.length === 0) return "";
|
|
12105
|
+
if (!this.skillReader) return "";
|
|
12106
|
+
const charBudget = budgetTokens * 4;
|
|
12107
|
+
let used = 0;
|
|
12108
|
+
const sections = [];
|
|
12109
|
+
let truncatedAt = -1;
|
|
12110
|
+
for (let i = 0; i < skillNames.length; i++) {
|
|
12111
|
+
const name = skillNames[i];
|
|
12112
|
+
const body = await this.skillReader.read(name);
|
|
12113
|
+
if (body === null) {
|
|
12114
|
+
this.logger.warn("inlineSkills: skill not found in registry", { name });
|
|
12115
|
+
continue;
|
|
12116
|
+
}
|
|
12117
|
+
const block = `### ${name}
|
|
12118
|
+
|
|
12119
|
+
${body}`;
|
|
12120
|
+
if (used + block.length > charBudget) {
|
|
12121
|
+
truncatedAt = i;
|
|
12122
|
+
break;
|
|
12123
|
+
}
|
|
12124
|
+
used += block.length;
|
|
12125
|
+
sections.push(block);
|
|
12126
|
+
}
|
|
12127
|
+
if (truncatedAt >= 0) {
|
|
12128
|
+
this.logger.warn(
|
|
12129
|
+
`inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
|
|
12130
|
+
);
|
|
12131
|
+
}
|
|
12132
|
+
if (sections.length === 0) return "";
|
|
12133
|
+
return `## Reference skills
|
|
12134
|
+
|
|
12135
|
+
${sections.join("\n\n")}
|
|
12136
|
+
`;
|
|
12137
|
+
}
|
|
12138
|
+
formatUpstream(id, entry, now, maxAgeMs) {
|
|
12139
|
+
if (!entry) {
|
|
12140
|
+
return `### ${id}
|
|
12141
|
+
|
|
12142
|
+
_[no prior run]_`;
|
|
12143
|
+
}
|
|
12144
|
+
const completedMs = Date.parse(entry.completedAt);
|
|
12145
|
+
if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
|
|
12146
|
+
return `### ${id} (last run ${entry.completedAt}, stale)
|
|
12147
|
+
|
|
12148
|
+
_[stale: omitted]_`;
|
|
12149
|
+
}
|
|
12150
|
+
const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
|
|
12151
|
+
const body = (entry.stdout ?? "").trim();
|
|
12152
|
+
const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
|
|
12153
|
+
|
|
12154
|
+
_[truncated]_` : body;
|
|
12155
|
+
return `${head}
|
|
12156
|
+
|
|
12157
|
+
${truncated || "_[no stdout captured]_"}`;
|
|
12158
|
+
}
|
|
12159
|
+
};
|
|
12160
|
+
var fallbackLogger3 = {
|
|
12161
|
+
info: () => {
|
|
12162
|
+
},
|
|
12163
|
+
warn: () => {
|
|
12164
|
+
},
|
|
12165
|
+
error: () => {
|
|
12166
|
+
}
|
|
12167
|
+
};
|
|
12168
|
+
|
|
12169
|
+
// src/maintenance/custom-task-validator.ts
|
|
12170
|
+
import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
|
|
12171
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
12172
|
+
var REQUIRED_FIELDS_BY_TYPE = {
|
|
12173
|
+
"mechanical-ai": ["branch", "fixSkill"],
|
|
12174
|
+
"pure-ai": ["branch", "fixSkill"],
|
|
12175
|
+
"report-only": [],
|
|
12176
|
+
housekeeping: []
|
|
12177
|
+
};
|
|
12178
|
+
function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
12179
|
+
const errors = [];
|
|
12180
|
+
if (!customTasks) return Ok23(void 0);
|
|
12181
|
+
const builtInIds = new Set(builtIns.map((t) => t.id));
|
|
12182
|
+
const customIds = Object.keys(customTasks);
|
|
12183
|
+
const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
|
|
12184
|
+
for (const id of customIds) {
|
|
12185
|
+
const task = customTasks[id];
|
|
12186
|
+
if (!task) continue;
|
|
12187
|
+
validateOne(id, task, builtInIds, allIds, deps, errors);
|
|
12188
|
+
}
|
|
12189
|
+
detectCycles(customTasks, builtIns, errors);
|
|
12190
|
+
return errors.length === 0 ? Ok23(void 0) : Err20(errors);
|
|
12191
|
+
}
|
|
12192
|
+
function validateOne(id, task, builtInIds, allIds, deps, errors) {
|
|
12193
|
+
const prefix = `customTasks.${id}`;
|
|
12194
|
+
if (!ID_PATTERN.test(id)) {
|
|
12195
|
+
errors.push({
|
|
12196
|
+
path: prefix,
|
|
12197
|
+
message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
|
|
12198
|
+
});
|
|
12199
|
+
}
|
|
12200
|
+
if (builtInIds.has(id)) {
|
|
12201
|
+
errors.push({
|
|
12202
|
+
path: prefix,
|
|
12203
|
+
message: `task ID '${id}' collides with a built-in task; choose a different name`
|
|
12204
|
+
});
|
|
12205
|
+
}
|
|
12206
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
12207
|
+
errors.push({ path: `${prefix}.description`, message: "description is required" });
|
|
12208
|
+
}
|
|
12209
|
+
if (!task.schedule || task.schedule.trim().length === 0) {
|
|
12210
|
+
errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
|
|
12211
|
+
}
|
|
12212
|
+
validateCheckShape(prefix, task, errors);
|
|
12213
|
+
validateRequiredByType(prefix, task, errors);
|
|
12214
|
+
validateContextFrom(prefix, id, task, allIds, errors);
|
|
12215
|
+
validateInlineSkills(prefix, task, deps, errors);
|
|
12216
|
+
validateScriptPath(prefix, task, deps, errors);
|
|
12217
|
+
}
|
|
12218
|
+
function validateCheckShape(prefix, task, errors) {
|
|
12219
|
+
const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
|
|
12220
|
+
const hasScript = task.checkScript !== void 0;
|
|
12221
|
+
if (hasCommand && hasScript) {
|
|
12222
|
+
errors.push({
|
|
12223
|
+
path: prefix,
|
|
12224
|
+
message: "a task may declare checkCommand OR checkScript, not both"
|
|
12225
|
+
});
|
|
12226
|
+
}
|
|
12227
|
+
const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
|
|
12228
|
+
if (needsCheck && !hasCommand && !hasScript) {
|
|
12229
|
+
errors.push({
|
|
12230
|
+
path: prefix,
|
|
12231
|
+
message: `${task.type} task must declare either checkCommand or checkScript`
|
|
12232
|
+
});
|
|
12233
|
+
}
|
|
12234
|
+
if (hasScript) {
|
|
12235
|
+
const path24 = task.checkScript?.path;
|
|
12236
|
+
if (!path24 || path24.trim().length === 0) {
|
|
12237
|
+
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
12238
|
+
}
|
|
12239
|
+
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
12240
|
+
errors.push({
|
|
12241
|
+
path: `${prefix}.checkScript.timeoutMs`,
|
|
12242
|
+
message: "timeoutMs must be a positive integer"
|
|
12243
|
+
});
|
|
12244
|
+
}
|
|
12245
|
+
}
|
|
12246
|
+
}
|
|
12247
|
+
function validateRequiredByType(prefix, task, errors) {
|
|
12248
|
+
const required = REQUIRED_FIELDS_BY_TYPE[task.type];
|
|
12249
|
+
if (!required) {
|
|
12250
|
+
errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
|
|
12251
|
+
return;
|
|
12252
|
+
}
|
|
12253
|
+
for (const field of required) {
|
|
12254
|
+
const value = task[field];
|
|
12255
|
+
if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
|
|
12256
|
+
errors.push({
|
|
12257
|
+
path: `${prefix}.${String(field)}`,
|
|
12258
|
+
message: `${task.type} task requires ${String(field)}`
|
|
12259
|
+
});
|
|
12260
|
+
}
|
|
12261
|
+
}
|
|
12262
|
+
if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
|
|
12263
|
+
errors.push({
|
|
12264
|
+
path: `${prefix}.branch`,
|
|
12265
|
+
message: `${task.type} task requires a non-null branch`
|
|
12266
|
+
});
|
|
12267
|
+
}
|
|
12268
|
+
}
|
|
12269
|
+
function validateContextFrom(prefix, selfId, task, allIds, errors) {
|
|
12270
|
+
if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
|
|
12271
|
+
errors.push({
|
|
12272
|
+
path: `${prefix}.contextFromMaxAgeMinutes`,
|
|
12273
|
+
message: "contextFromMaxAgeMinutes must be a positive integer"
|
|
12274
|
+
});
|
|
12275
|
+
}
|
|
12276
|
+
if (!task.contextFrom) return;
|
|
12277
|
+
for (let i = 0; i < task.contextFrom.length; i++) {
|
|
12278
|
+
const upstreamId = task.contextFrom[i];
|
|
12279
|
+
if (!upstreamId) continue;
|
|
12280
|
+
if (upstreamId === selfId) {
|
|
12281
|
+
errors.push({
|
|
12282
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
12283
|
+
message: `task '${selfId}' cannot reference itself in contextFrom`
|
|
12284
|
+
});
|
|
12285
|
+
}
|
|
12286
|
+
if (!allIds.has(upstreamId)) {
|
|
12287
|
+
errors.push({
|
|
12288
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
12289
|
+
message: `references unknown task '${upstreamId}'`
|
|
12290
|
+
});
|
|
12291
|
+
}
|
|
12292
|
+
}
|
|
12293
|
+
}
|
|
12294
|
+
function validateInlineSkills(prefix, task, deps, errors) {
|
|
12295
|
+
if (!task.inlineSkills) return;
|
|
12296
|
+
if (!deps.skillExists) return;
|
|
12297
|
+
for (let i = 0; i < task.inlineSkills.length; i++) {
|
|
12298
|
+
const name = task.inlineSkills[i];
|
|
12299
|
+
if (!name) continue;
|
|
12300
|
+
if (!deps.skillExists(name)) {
|
|
12301
|
+
errors.push({
|
|
12302
|
+
path: `${prefix}.inlineSkills[${i}]`,
|
|
12303
|
+
message: `skill '${name}' not found in the registry`
|
|
12304
|
+
});
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12307
|
+
if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
|
|
12308
|
+
errors.push({
|
|
12309
|
+
path: `${prefix}.inlineSkillsBudgetTokens`,
|
|
12310
|
+
message: "inlineSkillsBudgetTokens must be a positive integer"
|
|
12311
|
+
});
|
|
12312
|
+
}
|
|
12313
|
+
}
|
|
12314
|
+
function validateScriptPath(prefix, task, deps, errors) {
|
|
12315
|
+
if (!task.checkScript?.path) return;
|
|
12316
|
+
if (!deps.scriptExists) return;
|
|
12317
|
+
if (!deps.scriptExists(task.checkScript.path)) {
|
|
12318
|
+
errors.push({
|
|
12319
|
+
path: `${prefix}.checkScript.path`,
|
|
12320
|
+
message: `executable not found: ${task.checkScript.path}`
|
|
12321
|
+
});
|
|
12322
|
+
}
|
|
12323
|
+
}
|
|
12324
|
+
function detectCycles(customTasks, builtIns, errors) {
|
|
12325
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
12326
|
+
for (const t of builtIns) adjacency.set(t.id, []);
|
|
12327
|
+
for (const [id, task] of Object.entries(customTasks)) {
|
|
12328
|
+
adjacency.set(id, (task.contextFrom ?? []).slice());
|
|
12329
|
+
}
|
|
12330
|
+
const color = /* @__PURE__ */ new Map();
|
|
12331
|
+
for (const id of adjacency.keys()) color.set(id, "white");
|
|
12332
|
+
const reported = /* @__PURE__ */ new Set();
|
|
12333
|
+
for (const id of Object.keys(customTasks)) {
|
|
12334
|
+
if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
|
|
12335
|
+
}
|
|
12336
|
+
}
|
|
12337
|
+
function visitFromRoot(start, adjacency, color, errors, reported) {
|
|
12338
|
+
const stack = [{ id: start, nextIdx: 0, path: [start] }];
|
|
12339
|
+
color.set(start, "grey");
|
|
12340
|
+
while (stack.length) {
|
|
12341
|
+
const top = stack[stack.length - 1];
|
|
12342
|
+
const neighbors = adjacency.get(top.id) ?? [];
|
|
12343
|
+
if (top.nextIdx >= neighbors.length) {
|
|
12344
|
+
color.set(top.id, "black");
|
|
12345
|
+
stack.pop();
|
|
12346
|
+
continue;
|
|
12347
|
+
}
|
|
12348
|
+
const next = neighbors[top.nextIdx++];
|
|
12349
|
+
if (!next || !adjacency.has(next)) continue;
|
|
12350
|
+
handleEdge(top, next, color, stack, errors, reported);
|
|
12351
|
+
}
|
|
12352
|
+
}
|
|
12353
|
+
function handleEdge(top, next, color, stack, errors, reported) {
|
|
12354
|
+
const nextColor = color.get(next);
|
|
12355
|
+
if (nextColor === "grey") {
|
|
12356
|
+
reportCycle(top.path, next, errors, reported);
|
|
12357
|
+
} else if (nextColor === "white") {
|
|
12358
|
+
color.set(next, "grey");
|
|
12359
|
+
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12362
|
+
function reportCycle(path24, next, errors, reported) {
|
|
12363
|
+
const cycleStart = path24.indexOf(next);
|
|
12364
|
+
const cyclePath = cycleStart >= 0 ? [...path24.slice(cycleStart), next] : [...path24, next];
|
|
12365
|
+
const key = cyclePath.join("\u2192");
|
|
12366
|
+
if (reported.has(key)) return;
|
|
12367
|
+
reported.add(key);
|
|
12368
|
+
errors.push({
|
|
12369
|
+
path: `customTasks.${cyclePath[0]}.contextFrom`,
|
|
12370
|
+
message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
|
|
12371
|
+
});
|
|
10454
12372
|
}
|
|
12373
|
+
|
|
12374
|
+
// src/orchestrator.ts
|
|
10455
12375
|
var Orchestrator = class extends EventEmitter {
|
|
10456
12376
|
state;
|
|
10457
12377
|
config;
|
|
@@ -10476,6 +12396,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10476
12396
|
* construction time. Eliminating this fallback is autopilot Phase 4+.
|
|
10477
12397
|
*/
|
|
10478
12398
|
backendFactory;
|
|
12399
|
+
/**
|
|
12400
|
+
* Spec B Phase 4 (D8): per-orchestrator in-process bus for
|
|
12401
|
+
* `RoutingDecision` events. Constructed alongside backendFactory when
|
|
12402
|
+
* agent.backends synthesis succeeds; null when legacy single-backend
|
|
12403
|
+
* config bypassed backends. Phase 5+ consumers (HTTP, WS, dashboard)
|
|
12404
|
+
* subscribe via `getRoutingDecisionBus()`.
|
|
12405
|
+
*/
|
|
12406
|
+
routingDecisionBus;
|
|
10479
12407
|
/**
|
|
10480
12408
|
* Test-only: when overrides.backend is provided, dispatch uses this
|
|
10481
12409
|
* instance directly (bypassing the factory). Mirrors Phase 1
|
|
@@ -10498,6 +12426,15 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10498
12426
|
* so this map is the single source of truth post-migration.
|
|
10499
12427
|
*/
|
|
10500
12428
|
localResolvers = /* @__PURE__ */ new Map();
|
|
12429
|
+
/**
|
|
12430
|
+
* Spec B Phase 3: skill catalog (name + cognitiveMode) read once at
|
|
12431
|
+
* construction from `projectRoot/agents/skills/`. Consulted by
|
|
12432
|
+
* `buildRoutingUseCase` at dispatch start to construct
|
|
12433
|
+
* `{ kind: 'skill', skillName, cognitiveMode }` RoutingUseCases.
|
|
12434
|
+
* Empty when the orchestrator runs outside a harness project root
|
|
12435
|
+
* (then dispatch falls through to per-tier, preserving F11/N2).
|
|
12436
|
+
*/
|
|
12437
|
+
skillCatalog;
|
|
10501
12438
|
/**
|
|
10502
12439
|
* Per-resolver `onStatusChange` unsubscribe callbacks. Spec 2 Phase 5
|
|
10503
12440
|
* (SC39): each local/pi resolver gets its own listener emitting a
|
|
@@ -10546,7 +12483,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10546
12483
|
completionHandler;
|
|
10547
12484
|
/** Project root directory, derived from workspace root. */
|
|
10548
12485
|
get projectRoot() {
|
|
10549
|
-
return
|
|
12486
|
+
return path21.resolve(this.config.workspace.root, "..", "..");
|
|
10550
12487
|
}
|
|
10551
12488
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
10552
12489
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -10590,6 +12527,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10590
12527
|
`migrateAgentConfig failed; continuing with legacy fields. Error: ${err instanceof Error ? err.message : String(err)}`
|
|
10591
12528
|
);
|
|
10592
12529
|
}
|
|
12530
|
+
const skillCatalogRoot = path21.resolve(this.config.workspace.root, "..", "..");
|
|
12531
|
+
this.skillCatalog = discoverSkillCatalog(skillCatalogRoot);
|
|
12532
|
+
if (this.skillCatalog.length === 0) {
|
|
12533
|
+
this.logger.warn(
|
|
12534
|
+
`Spec B Phase 3: skill catalog discovery returned 0 entries; per-skill / per-mode routing will fall through to per-tier. Looked under ${path21.join(skillCatalogRoot, "agents/skills")}.`
|
|
12535
|
+
);
|
|
12536
|
+
}
|
|
10593
12537
|
this.tracker = overrides?.tracker || this.createTracker();
|
|
10594
12538
|
this.workspace = new WorkspaceManager(config.workspace, {
|
|
10595
12539
|
emitEvent: (event) => {
|
|
@@ -10601,10 +12545,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10601
12545
|
this.renderer = new PromptRenderer();
|
|
10602
12546
|
this.overrideBackend = overrides?.backend ?? null;
|
|
10603
12547
|
this.interactionQueue = new InteractionQueue(
|
|
10604
|
-
|
|
12548
|
+
path21.join(config.workspace.root, "..", "interactions"),
|
|
10605
12549
|
this
|
|
10606
12550
|
);
|
|
10607
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12551
|
+
this.analysisArchive = new AnalysisArchive(path21.join(config.workspace.root, "..", "analyses"));
|
|
10608
12552
|
const backendsMap = this.config.agent.backends ?? {};
|
|
10609
12553
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
10610
12554
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -10625,6 +12569,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10625
12569
|
const routing = this.config.agent.routing ?? {
|
|
10626
12570
|
default: firstBackendName ?? "primary"
|
|
10627
12571
|
};
|
|
12572
|
+
this.routingDecisionBus = new RoutingDecisionBus({
|
|
12573
|
+
capacity: 500,
|
|
12574
|
+
logger: this.logger
|
|
12575
|
+
});
|
|
10628
12576
|
this.backendFactory = new OrchestratorBackendFactory({
|
|
10629
12577
|
backends: this.config.agent.backends,
|
|
10630
12578
|
routing,
|
|
@@ -10632,6 +12580,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10632
12580
|
...this.config.agent.container !== void 0 ? { container: this.config.agent.container } : {},
|
|
10633
12581
|
...this.config.agent.secrets !== void 0 ? { secrets: this.config.agent.secrets } : {},
|
|
10634
12582
|
cacheMetrics: this.cacheMetrics,
|
|
12583
|
+
decisionBus: this.routingDecisionBus,
|
|
10635
12584
|
getResolverModelFor: (name) => {
|
|
10636
12585
|
const resolver = this.localResolvers.get(name);
|
|
10637
12586
|
return resolver ? () => resolver.resolveModel() : void 0;
|
|
@@ -10639,6 +12588,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10639
12588
|
});
|
|
10640
12589
|
} else {
|
|
10641
12590
|
this.backendFactory = null;
|
|
12591
|
+
this.routingDecisionBus = null;
|
|
10642
12592
|
}
|
|
10643
12593
|
this.pipeline = null;
|
|
10644
12594
|
this.orchestratorIdPromise = resolveOrchestratorId(config.orchestratorId);
|
|
@@ -10648,7 +12598,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10648
12598
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
10649
12599
|
});
|
|
10650
12600
|
this.recorder = new StreamRecorder(
|
|
10651
|
-
|
|
12601
|
+
path21.resolve(config.workspace.root, "..", "streams"),
|
|
10652
12602
|
this.logger
|
|
10653
12603
|
);
|
|
10654
12604
|
const self = this;
|
|
@@ -10679,10 +12629,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10679
12629
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
10680
12630
|
if (config.server?.port) {
|
|
10681
12631
|
const webhookStore = new WebhookStore(
|
|
10682
|
-
|
|
12632
|
+
path21.join(this.projectRoot, ".harness", "webhooks.json")
|
|
10683
12633
|
);
|
|
10684
12634
|
this.webhookQueue = new WebhookQueue(
|
|
10685
|
-
|
|
12635
|
+
path21.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
10686
12636
|
);
|
|
10687
12637
|
const webhookDelivery = new WebhookDelivery({
|
|
10688
12638
|
queue: this.webhookQueue,
|
|
@@ -10720,7 +12670,16 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10720
12670
|
queue: this.webhookQueue
|
|
10721
12671
|
},
|
|
10722
12672
|
cacheMetrics: this.cacheMetrics,
|
|
10723
|
-
|
|
12673
|
+
// Spec B Phase 5: routing observability accessors. Closures so the
|
|
12674
|
+
// server re-reads on every request — stop() / start() do not
|
|
12675
|
+
// require server reconstruction. Returns null if no backendFactory
|
|
12676
|
+
// (legacy single-backend configs), and the route handler renders
|
|
12677
|
+
// 503 in that case.
|
|
12678
|
+
getBackendRouter: () => this.getBackendRouter(),
|
|
12679
|
+
getRoutingDecisionBus: () => this.getRoutingDecisionBus(),
|
|
12680
|
+
getRoutingConfig: () => this.getRoutingConfig(),
|
|
12681
|
+
getBackends: () => this.getBackends(),
|
|
12682
|
+
plansDir: path21.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
10724
12683
|
pipeline: this.pipeline,
|
|
10725
12684
|
analysisArchive: this.analysisArchive,
|
|
10726
12685
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -10776,13 +12735,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10776
12735
|
const logger = this.logger;
|
|
10777
12736
|
const checkRunner = {
|
|
10778
12737
|
run: async (command, cwd) => {
|
|
10779
|
-
const { execFile:
|
|
10780
|
-
const { promisify:
|
|
10781
|
-
const
|
|
12738
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12739
|
+
const { promisify: promisify5 } = await import("util");
|
|
12740
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10782
12741
|
const [cmd, ...args] = command;
|
|
10783
12742
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
10784
12743
|
try {
|
|
10785
|
-
const { stdout } = await
|
|
12744
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10786
12745
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
10787
12746
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
10788
12747
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -10811,13 +12770,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10811
12770
|
};
|
|
10812
12771
|
const commandExecutor = {
|
|
10813
12772
|
exec: async (command, cwd) => {
|
|
10814
|
-
const { execFile:
|
|
10815
|
-
const { promisify:
|
|
10816
|
-
const
|
|
12773
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12774
|
+
const { promisify: promisify5 } = await import("util");
|
|
12775
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10817
12776
|
const [cmd, ...args] = command;
|
|
10818
12777
|
if (!cmd) return { stdout: "" };
|
|
10819
12778
|
try {
|
|
10820
|
-
const { stdout } = await
|
|
12779
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10821
12780
|
return { stdout: String(stdout) };
|
|
10822
12781
|
} catch (err) {
|
|
10823
12782
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -10829,12 +12788,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10829
12788
|
}
|
|
10830
12789
|
}
|
|
10831
12790
|
};
|
|
12791
|
+
const outputStore = new TaskOutputStore({
|
|
12792
|
+
rootDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
12793
|
+
logger: this.logger
|
|
12794
|
+
});
|
|
12795
|
+
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
12796
|
+
const skillReader = {
|
|
12797
|
+
// The orchestrator does not own the skill registry; CLI-side skill
|
|
12798
|
+
// resolution wires this in via direct injection. Default: skill not
|
|
12799
|
+
// resolvable from the orchestrator boundary.
|
|
12800
|
+
read: async () => null
|
|
12801
|
+
};
|
|
12802
|
+
const contextResolver = new ContextResolver({
|
|
12803
|
+
outputStore,
|
|
12804
|
+
skillReader,
|
|
12805
|
+
logger: this.logger
|
|
12806
|
+
});
|
|
10832
12807
|
return new TaskRunner({
|
|
10833
12808
|
config: maintenanceConfig,
|
|
10834
12809
|
checkRunner,
|
|
10835
12810
|
agentDispatcher,
|
|
10836
12811
|
commandExecutor,
|
|
10837
|
-
cwd: this.projectRoot
|
|
12812
|
+
cwd: this.projectRoot,
|
|
12813
|
+
checkScriptRunner,
|
|
12814
|
+
contextResolver,
|
|
12815
|
+
outputStore
|
|
10838
12816
|
});
|
|
10839
12817
|
}
|
|
10840
12818
|
/**
|
|
@@ -10842,8 +12820,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10842
12820
|
* Extracted from start() to keep function length under threshold.
|
|
10843
12821
|
*/
|
|
10844
12822
|
async initMaintenance(maintenanceConfig) {
|
|
12823
|
+
const validation = validateCustomTasks(
|
|
12824
|
+
maintenanceConfig.customTasks,
|
|
12825
|
+
BUILT_IN_TASKS
|
|
12826
|
+
);
|
|
12827
|
+
if (!validation.ok) {
|
|
12828
|
+
const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
12829
|
+
throw new Error(`Invalid maintenance.customTasks configuration:
|
|
12830
|
+
${messages}`);
|
|
12831
|
+
}
|
|
10845
12832
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
10846
|
-
persistDir:
|
|
12833
|
+
persistDir: path21.join(this.projectRoot, ".harness", "maintenance"),
|
|
10847
12834
|
logger: this.logger
|
|
10848
12835
|
});
|
|
10849
12836
|
await this.maintenanceReporter.load();
|
|
@@ -10894,10 +12881,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10894
12881
|
}
|
|
10895
12882
|
}
|
|
10896
12883
|
createIntelligencePipeline() {
|
|
12884
|
+
if (!this.backendFactory) {
|
|
12885
|
+
this.logger.warn(
|
|
12886
|
+
"intelligence pipeline disabled: no backendFactory available (legacy config without agent.backends)"
|
|
12887
|
+
);
|
|
12888
|
+
return null;
|
|
12889
|
+
}
|
|
10897
12890
|
const bundle = buildIntelligencePipeline({
|
|
10898
12891
|
config: this.config,
|
|
10899
12892
|
localResolvers: this.localResolvers,
|
|
10900
|
-
logger: this.logger
|
|
12893
|
+
logger: this.logger,
|
|
12894
|
+
router: this.backendFactory.getRouter()
|
|
10901
12895
|
});
|
|
10902
12896
|
if (!bundle) return null;
|
|
10903
12897
|
this.graphStore = bundle.graphStore;
|
|
@@ -10948,11 +12942,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10948
12942
|
simulationResults,
|
|
10949
12943
|
personaRecommendations
|
|
10950
12944
|
} = pipelineResult ?? {};
|
|
12945
|
+
const selfAssignee = await this.orchestratorIdPromise;
|
|
10951
12946
|
const tickEvent = {
|
|
10952
12947
|
type: "tick",
|
|
10953
12948
|
candidates,
|
|
10954
12949
|
runningStates: runningStatesResult.value,
|
|
10955
12950
|
nowMs,
|
|
12951
|
+
selfAssignee,
|
|
10956
12952
|
...concernSignals !== void 0 && { concernSignals },
|
|
10957
12953
|
...enrichedSpecs !== void 0 && { enrichedSpecs },
|
|
10958
12954
|
...complexityScores !== void 0 && { complexityScores },
|
|
@@ -11376,14 +13372,24 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11376
13372
|
issue,
|
|
11377
13373
|
attempt: attempt || 1
|
|
11378
13374
|
});
|
|
11379
|
-
const useCase =
|
|
13375
|
+
const useCase = buildRoutingUseCase(issue, backend, this.skillCatalog);
|
|
13376
|
+
const invocationOverride = process.env.HARNESS_BACKEND_OVERRIDE;
|
|
13377
|
+
const routerOpts = invocationOverride ? { invocationOverride } : void 0;
|
|
13378
|
+
if (invocationOverride) {
|
|
13379
|
+
this.logger.info(
|
|
13380
|
+
`Spec B Phase 3: HARNESS_BACKEND_OVERRIDE='${invocationOverride}' taking effect for ${issue.identifier}`,
|
|
13381
|
+
{ issueId: issue.id }
|
|
13382
|
+
);
|
|
13383
|
+
}
|
|
11380
13384
|
let routedBackendName;
|
|
11381
13385
|
if (this.overrideBackend !== null) {
|
|
11382
13386
|
routedBackendName = this.overrideBackend.name;
|
|
11383
13387
|
} else if (this.backendFactory !== null) {
|
|
11384
|
-
routedBackendName = this.backendFactory.resolveName(useCase);
|
|
13388
|
+
routedBackendName = this.backendFactory.resolveName(useCase, routerOpts);
|
|
11385
13389
|
} else {
|
|
11386
|
-
|
|
13390
|
+
const routingDefault = this.config.agent.routing?.default;
|
|
13391
|
+
const routingDefaultScalar = routingDefault !== void 0 ? toArray(routingDefault)[0] : void 0;
|
|
13392
|
+
routedBackendName = routingDefaultScalar ?? this.config.agent.backend ?? "unknown";
|
|
11387
13393
|
}
|
|
11388
13394
|
const session = {
|
|
11389
13395
|
sessionId: `pending-${Date.now()}`,
|
|
@@ -11422,7 +13428,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11422
13428
|
if (this.overrideBackend !== null) {
|
|
11423
13429
|
agentBackend = this.overrideBackend;
|
|
11424
13430
|
} else if (this.backendFactory !== null) {
|
|
11425
|
-
agentBackend = this.backendFactory.forUseCase(useCase);
|
|
13431
|
+
agentBackend = this.backendFactory.forUseCase(useCase, routerOpts);
|
|
11426
13432
|
} else {
|
|
11427
13433
|
throw new Error(
|
|
11428
13434
|
`Cannot dispatch ${issue.identifier}: agent.backends not synthesized (migration failed) and no override backend supplied. Migrate to agent.backends/agent.routing per docs/guides/multi-backend-routing.md.`
|
|
@@ -11712,6 +13718,8 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11712
13718
|
unsub();
|
|
11713
13719
|
}
|
|
11714
13720
|
this.localModelStatusUnsubscribes = [];
|
|
13721
|
+
this.routingDecisionBus?.clearListeners();
|
|
13722
|
+
this.routingDecisionBus = null;
|
|
11715
13723
|
for (const resolver of this.localResolvers.values()) {
|
|
11716
13724
|
resolver.stop();
|
|
11717
13725
|
}
|
|
@@ -11785,6 +13793,42 @@ var Orchestrator = class extends EventEmitter {
|
|
|
11785
13793
|
tickActivity: this.tickActivity
|
|
11786
13794
|
};
|
|
11787
13795
|
}
|
|
13796
|
+
/**
|
|
13797
|
+
* Spec B Phase 4 (D8): expose the bus for Phase 5 (HTTP routes) and
|
|
13798
|
+
* Phase 7 (dashboard WS broadcast). Returns null when the legacy
|
|
13799
|
+
* single-backend config bypassed agent.backends synthesis.
|
|
13800
|
+
*/
|
|
13801
|
+
getRoutingDecisionBus() {
|
|
13802
|
+
return this.routingDecisionBus;
|
|
13803
|
+
}
|
|
13804
|
+
/**
|
|
13805
|
+
* Spec B Phase 5: live BackendRouter for HTTP routes. The orchestrator
|
|
13806
|
+
* dispatch path uses the factory-owned router directly; observability
|
|
13807
|
+
* routes (config / decisions) reach it through this accessor. Returns
|
|
13808
|
+
* null when the legacy single-backend config bypassed agent.backends
|
|
13809
|
+
* synthesis (no backendFactory built).
|
|
13810
|
+
*/
|
|
13811
|
+
getBackendRouter() {
|
|
13812
|
+
return this.backendFactory?.getRouter() ?? null;
|
|
13813
|
+
}
|
|
13814
|
+
/**
|
|
13815
|
+
* Spec B Phase 5: snapshot of the active RoutingConfig for the config
|
|
13816
|
+
* route and the trace route's bus-less router construction. Returns
|
|
13817
|
+
* null when the operator's harness.config.json carries no
|
|
13818
|
+
* `agent.routing` block.
|
|
13819
|
+
*/
|
|
13820
|
+
getRoutingConfig() {
|
|
13821
|
+
return this.config.agent.routing ?? null;
|
|
13822
|
+
}
|
|
13823
|
+
/**
|
|
13824
|
+
* Spec B Phase 5: snapshot of `agent.backends` for the config route
|
|
13825
|
+
* (existence annotations) and the trace route (bus-less router
|
|
13826
|
+
* construction). Returns null when no synthesized backends map exists
|
|
13827
|
+
* (legacy single-backend configs).
|
|
13828
|
+
*/
|
|
13829
|
+
getBackends() {
|
|
13830
|
+
return this.config.agent.backends ?? null;
|
|
13831
|
+
}
|
|
11788
13832
|
/** Returns the maintenance scheduler status, or null if maintenance is not enabled. */
|
|
11789
13833
|
getMaintenanceStatus() {
|
|
11790
13834
|
return this.maintenanceScheduler?.getStatus() ?? null;
|
|
@@ -12020,10 +14064,10 @@ function launchTUI(orchestrator) {
|
|
|
12020
14064
|
|
|
12021
14065
|
// src/maintenance/sync-main.ts
|
|
12022
14066
|
import { execFile as nodeExecFile } from "child_process";
|
|
12023
|
-
import { promisify as
|
|
14067
|
+
import { promisify as promisify4 } from "util";
|
|
12024
14068
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
12025
14069
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
12026
|
-
const exec =
|
|
14070
|
+
const exec = promisify4(execFileFn);
|
|
12027
14071
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
12028
14072
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
12029
14073
|
}
|
|
@@ -12163,8 +14207,8 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
12163
14207
|
}
|
|
12164
14208
|
|
|
12165
14209
|
// src/sessions/search-index.ts
|
|
12166
|
-
import * as
|
|
12167
|
-
import * as
|
|
14210
|
+
import * as fs18 from "fs";
|
|
14211
|
+
import * as path22 from "path";
|
|
12168
14212
|
import Database2 from "better-sqlite3";
|
|
12169
14213
|
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
12170
14214
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
@@ -12209,7 +14253,7 @@ function normalizeFts5Query(query) {
|
|
|
12209
14253
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
12210
14254
|
}
|
|
12211
14255
|
function searchIndexPath(projectPath) {
|
|
12212
|
-
return
|
|
14256
|
+
return path22.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
12213
14257
|
}
|
|
12214
14258
|
var FILE_KIND_TO_FILENAME = {
|
|
12215
14259
|
summary: "summary.md",
|
|
@@ -12224,7 +14268,7 @@ var SqliteSearchIndex = class {
|
|
|
12224
14268
|
removeSessionStmt;
|
|
12225
14269
|
totalStmt;
|
|
12226
14270
|
constructor(dbPath) {
|
|
12227
|
-
|
|
14271
|
+
fs18.mkdirSync(path22.dirname(dbPath), { recursive: true });
|
|
12228
14272
|
this.db = new Database2(dbPath);
|
|
12229
14273
|
this.db.pragma("journal_mode = WAL");
|
|
12230
14274
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -12329,14 +14373,14 @@ function indexSessionDirectory(idx, args) {
|
|
|
12329
14373
|
let docsWritten = 0;
|
|
12330
14374
|
for (const kind of kinds) {
|
|
12331
14375
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
12332
|
-
const filePath =
|
|
12333
|
-
if (!
|
|
12334
|
-
let body =
|
|
14376
|
+
const filePath = path22.join(args.sessionDir, fileName);
|
|
14377
|
+
if (!fs18.existsSync(filePath)) continue;
|
|
14378
|
+
let body = fs18.readFileSync(filePath, "utf8");
|
|
12335
14379
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
12336
14380
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
12337
14381
|
}
|
|
12338
|
-
const stat =
|
|
12339
|
-
const relPath =
|
|
14382
|
+
const stat = fs18.statSync(filePath);
|
|
14383
|
+
const relPath = path22.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
12340
14384
|
idx.upsertSessionDoc({
|
|
12341
14385
|
sessionId: args.sessionId,
|
|
12342
14386
|
archived: args.archived,
|
|
@@ -12351,17 +14395,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
12351
14395
|
}
|
|
12352
14396
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
12353
14397
|
const start = Date.now();
|
|
12354
|
-
const archiveBase =
|
|
14398
|
+
const archiveBase = path22.join(projectPath, ".harness", "archive", "sessions");
|
|
12355
14399
|
const idx = openSearchIndex(projectPath);
|
|
12356
14400
|
try {
|
|
12357
14401
|
idx.resetArchived();
|
|
12358
14402
|
let sessionsIndexed = 0;
|
|
12359
14403
|
let docsWritten = 0;
|
|
12360
|
-
if (
|
|
12361
|
-
const entries =
|
|
14404
|
+
if (fs18.existsSync(archiveBase)) {
|
|
14405
|
+
const entries = fs18.readdirSync(archiveBase, { withFileTypes: true });
|
|
12362
14406
|
for (const entry of entries) {
|
|
12363
14407
|
if (!entry.isDirectory()) continue;
|
|
12364
|
-
const sessionDir =
|
|
14408
|
+
const sessionDir = path22.join(archiveBase, entry.name);
|
|
12365
14409
|
const result = indexSessionDirectory(idx, {
|
|
12366
14410
|
sessionId: entry.name,
|
|
12367
14411
|
sessionDir,
|
|
@@ -12381,12 +14425,12 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
12381
14425
|
}
|
|
12382
14426
|
|
|
12383
14427
|
// src/sessions/summarize.ts
|
|
12384
|
-
import * as
|
|
12385
|
-
import * as
|
|
14428
|
+
import * as fs19 from "fs";
|
|
14429
|
+
import * as path23 from "path";
|
|
12386
14430
|
import {
|
|
12387
14431
|
SessionSummarySchema
|
|
12388
14432
|
} from "@harness-engineering/types";
|
|
12389
|
-
import { Ok as
|
|
14433
|
+
import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
|
|
12390
14434
|
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
12391
14435
|
var SUMMARY_INPUT_FILES = [
|
|
12392
14436
|
{ filename: "summary.md", kind: "summary" },
|
|
@@ -12412,10 +14456,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
12412
14456
|
function readInputCorpus(archiveDir) {
|
|
12413
14457
|
const parts = [];
|
|
12414
14458
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
12415
|
-
const p =
|
|
12416
|
-
if (!
|
|
14459
|
+
const p = path23.join(archiveDir, filename);
|
|
14460
|
+
if (!fs19.existsSync(p)) continue;
|
|
12417
14461
|
try {
|
|
12418
|
-
const content =
|
|
14462
|
+
const content = fs19.readFileSync(p, "utf8");
|
|
12419
14463
|
if (content.trim().length === 0) continue;
|
|
12420
14464
|
parts.push(`## FILE: ${kind}
|
|
12421
14465
|
|
|
@@ -12466,7 +14510,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
12466
14510
|
return lines.join("\n");
|
|
12467
14511
|
}
|
|
12468
14512
|
function writeStubMarkdown(archiveDir, reason) {
|
|
12469
|
-
const filePath =
|
|
14513
|
+
const filePath = path23.join(archiveDir, LLM_SUMMARY_FILE);
|
|
12470
14514
|
const body = `---
|
|
12471
14515
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
12472
14516
|
schemaVersion: 1
|
|
@@ -12477,17 +14521,17 @@ status: failed
|
|
|
12477
14521
|
|
|
12478
14522
|
- reason: ${reason}
|
|
12479
14523
|
`;
|
|
12480
|
-
|
|
14524
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
12481
14525
|
return filePath;
|
|
12482
14526
|
}
|
|
12483
14527
|
async function summarizeArchivedSession(ctx) {
|
|
12484
14528
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
12485
|
-
if (!
|
|
12486
|
-
return
|
|
14529
|
+
if (!fs19.existsSync(ctx.archiveDir)) {
|
|
14530
|
+
return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
12487
14531
|
}
|
|
12488
14532
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
12489
14533
|
if (corpus.trim().length === 0) {
|
|
12490
|
-
return
|
|
14534
|
+
return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
12491
14535
|
}
|
|
12492
14536
|
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
12493
14537
|
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
@@ -12520,7 +14564,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12520
14564
|
} catch {
|
|
12521
14565
|
}
|
|
12522
14566
|
}
|
|
12523
|
-
return
|
|
14567
|
+
return Err21(
|
|
12524
14568
|
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
12525
14569
|
);
|
|
12526
14570
|
}
|
|
@@ -12534,7 +14578,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12534
14578
|
} catch {
|
|
12535
14579
|
}
|
|
12536
14580
|
}
|
|
12537
|
-
return
|
|
14581
|
+
return Err21(new Error(reason));
|
|
12538
14582
|
}
|
|
12539
14583
|
const meta = {
|
|
12540
14584
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12543,10 +14587,10 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12543
14587
|
outputTokens: response.tokenUsage.outputTokens,
|
|
12544
14588
|
schemaVersion: 1
|
|
12545
14589
|
};
|
|
12546
|
-
const filePath =
|
|
14590
|
+
const filePath = path23.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
12547
14591
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
12548
|
-
|
|
12549
|
-
return
|
|
14592
|
+
fs19.writeFileSync(filePath, body, "utf8");
|
|
14593
|
+
return Ok24({ summary: parsed.data, meta, filePath });
|
|
12550
14594
|
}
|
|
12551
14595
|
function isSummaryEnabled(config) {
|
|
12552
14596
|
if (!config) return false;
|
|
@@ -12622,8 +14666,11 @@ function buildArchiveHooks(opts) {
|
|
|
12622
14666
|
}
|
|
12623
14667
|
export {
|
|
12624
14668
|
AnalysisArchive,
|
|
14669
|
+
BUILT_IN_TASKS,
|
|
12625
14670
|
BackendRouter,
|
|
12626
14671
|
ClaimManager,
|
|
14672
|
+
GateNotReadyError,
|
|
14673
|
+
GateRunError,
|
|
12627
14674
|
InteractionQueue,
|
|
12628
14675
|
LinearGraphQLStub,
|
|
12629
14676
|
MAX_ATTEMPTS,
|
|
@@ -12632,6 +14679,7 @@ export {
|
|
|
12632
14679
|
Orchestrator,
|
|
12633
14680
|
OrchestratorBackendFactory,
|
|
12634
14681
|
PRDetector,
|
|
14682
|
+
PromotionError,
|
|
12635
14683
|
PromptRenderer,
|
|
12636
14684
|
RETRY_DELAYS_MS,
|
|
12637
14685
|
RoadmapTrackerAdapter,
|
|
@@ -12640,6 +14688,7 @@ export {
|
|
|
12640
14688
|
SlackSink,
|
|
12641
14689
|
SqliteSearchIndex,
|
|
12642
14690
|
StreamRecorder,
|
|
14691
|
+
TaskOutputStore,
|
|
12643
14692
|
TokenStore,
|
|
12644
14693
|
WebhookQueue,
|
|
12645
14694
|
WorkflowLoader,
|
|
@@ -12653,7 +14702,13 @@ export {
|
|
|
12653
14702
|
computeRateLimitDelay,
|
|
12654
14703
|
createBackend,
|
|
12655
14704
|
createEmptyState,
|
|
14705
|
+
crossFieldRoutingIssues,
|
|
12656
14706
|
detectScopeTier,
|
|
14707
|
+
discoverSkillCatalog,
|
|
14708
|
+
discoverSkillCatalogNames,
|
|
14709
|
+
emitProposalApproved,
|
|
14710
|
+
emitProposalCreated,
|
|
14711
|
+
emitProposalRejected,
|
|
12657
14712
|
extractHighlights,
|
|
12658
14713
|
extractTitlePrefix,
|
|
12659
14714
|
getAvailableSlots,
|
|
@@ -12667,6 +14722,7 @@ export {
|
|
|
12667
14722
|
migrateAgentConfig,
|
|
12668
14723
|
normalizeFts5Query,
|
|
12669
14724
|
openSearchIndex,
|
|
14725
|
+
promote,
|
|
12670
14726
|
reconcile,
|
|
12671
14727
|
reindexFromArchive,
|
|
12672
14728
|
renderAnalysisComment,
|
|
@@ -12675,6 +14731,8 @@ export {
|
|
|
12675
14731
|
resolveEscalationConfig,
|
|
12676
14732
|
resolveOrchestratorId,
|
|
12677
14733
|
routeIssue,
|
|
14734
|
+
routingWarnings,
|
|
14735
|
+
runGate,
|
|
12678
14736
|
savePublishedIndex,
|
|
12679
14737
|
searchIndexPath,
|
|
12680
14738
|
selectCandidates,
|
|
@@ -12683,6 +14741,7 @@ export {
|
|
|
12683
14741
|
syncMain,
|
|
12684
14742
|
triageIssue,
|
|
12685
14743
|
truncateForBudget,
|
|
14744
|
+
validateCustomTasks,
|
|
12686
14745
|
validateWorkflowConfig,
|
|
12687
14746
|
wireNotificationSinks,
|
|
12688
14747
|
wrapAsEnvelope
|