@harness-engineering/orchestrator 0.4.6 → 0.6.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 +599 -5
- package/dist/index.d.ts +599 -5
- package/dist/index.js +3313 -455
- package/dist/index.mjs +3283 -429
- package/package.json +5 -5
package/dist/index.mjs
CHANGED
|
@@ -1144,7 +1144,7 @@ var ClaimManager = class {
|
|
|
1144
1144
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1145
1145
|
if (!claimResult.ok) return claimResult;
|
|
1146
1146
|
if (this.verifyDelayMs > 0) {
|
|
1147
|
-
await new Promise((
|
|
1147
|
+
await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
|
|
1148
1148
|
}
|
|
1149
1149
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1150
1150
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1870,11 +1870,11 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
|
|
|
1870
1870
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1871
1871
|
const issues = [];
|
|
1872
1872
|
const names = new Set(Object.keys(backends));
|
|
1873
|
-
const checkRef = (
|
|
1873
|
+
const checkRef = (path22, name) => {
|
|
1874
1874
|
if (name !== void 0 && !names.has(name)) {
|
|
1875
1875
|
issues.push({
|
|
1876
|
-
path:
|
|
1877
|
-
message: `routing.${
|
|
1876
|
+
path: path22,
|
|
1877
|
+
message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1878
1878
|
});
|
|
1879
1879
|
}
|
|
1880
1880
|
};
|
|
@@ -2544,7 +2544,7 @@ var WorkspaceHooks = class {
|
|
|
2544
2544
|
if (!command) {
|
|
2545
2545
|
return Ok7(void 0);
|
|
2546
2546
|
}
|
|
2547
|
-
return new Promise((
|
|
2547
|
+
return new Promise((resolve7) => {
|
|
2548
2548
|
const filteredEnv = {};
|
|
2549
2549
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2550
2550
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2557,19 +2557,19 @@ var WorkspaceHooks = class {
|
|
|
2557
2557
|
});
|
|
2558
2558
|
const timeout = setTimeout(() => {
|
|
2559
2559
|
child.kill();
|
|
2560
|
-
|
|
2560
|
+
resolve7(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2561
2561
|
}, this.config.timeoutMs);
|
|
2562
2562
|
child.on("exit", (code) => {
|
|
2563
2563
|
clearTimeout(timeout);
|
|
2564
2564
|
if (code === 0) {
|
|
2565
|
-
|
|
2565
|
+
resolve7(Ok7(void 0));
|
|
2566
2566
|
} else {
|
|
2567
|
-
|
|
2567
|
+
resolve7(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2568
2568
|
}
|
|
2569
2569
|
});
|
|
2570
2570
|
child.on("error", (error) => {
|
|
2571
2571
|
clearTimeout(timeout);
|
|
2572
|
-
|
|
2572
|
+
resolve7(Err5(error));
|
|
2573
2573
|
});
|
|
2574
2574
|
});
|
|
2575
2575
|
}
|
|
@@ -2609,7 +2609,7 @@ var MockBackend = class {
|
|
|
2609
2609
|
content: "Thinking...",
|
|
2610
2610
|
sessionId: session.sessionId
|
|
2611
2611
|
};
|
|
2612
|
-
await new Promise((
|
|
2612
|
+
await new Promise((resolve7) => setTimeout(resolve7, 100));
|
|
2613
2613
|
yield {
|
|
2614
2614
|
type: "thought",
|
|
2615
2615
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2661,7 +2661,7 @@ var PromptRenderer = class {
|
|
|
2661
2661
|
|
|
2662
2662
|
// src/orchestrator.ts
|
|
2663
2663
|
import { EventEmitter } from "events";
|
|
2664
|
-
import * as
|
|
2664
|
+
import * as path19 from "path";
|
|
2665
2665
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
2666
2666
|
import { writeTaint } from "@harness-engineering/core";
|
|
2667
2667
|
|
|
@@ -3635,11 +3635,11 @@ function detectLegacyFields(agent) {
|
|
|
3635
3635
|
}
|
|
3636
3636
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3637
3637
|
const warnings = [];
|
|
3638
|
-
for (const
|
|
3639
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3640
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3638
|
+
for (const path22 of presentLegacy) {
|
|
3639
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
|
|
3640
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
|
|
3641
3641
|
warnings.push(
|
|
3642
|
-
`Ignoring legacy field '${
|
|
3642
|
+
`Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3643
3643
|
);
|
|
3644
3644
|
}
|
|
3645
3645
|
return warnings;
|
|
@@ -3667,7 +3667,7 @@ function migrateAgentConfig(agent) {
|
|
|
3667
3667
|
}
|
|
3668
3668
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3669
3669
|
const warnings = presentLegacy.map(
|
|
3670
|
-
(
|
|
3670
|
+
(path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3671
3671
|
);
|
|
3672
3672
|
return {
|
|
3673
3673
|
config: { ...agent, backends, routing },
|
|
@@ -3756,6 +3756,10 @@ var BackendRouter = class {
|
|
|
3756
3756
|
const intel = this.routing.intelligence;
|
|
3757
3757
|
return intel?.[useCase.layer] ?? this.routing.default;
|
|
3758
3758
|
}
|
|
3759
|
+
case "isolation": {
|
|
3760
|
+
const iso = this.routing.isolation;
|
|
3761
|
+
return iso?.[useCase.tier] ?? this.routing.default;
|
|
3762
|
+
}
|
|
3759
3763
|
case "maintenance":
|
|
3760
3764
|
case "chat":
|
|
3761
3765
|
return this.routing.default;
|
|
@@ -3779,8 +3783,8 @@ var BackendRouter = class {
|
|
|
3779
3783
|
validateReferences() {
|
|
3780
3784
|
const known = new Set(Object.keys(this.backends));
|
|
3781
3785
|
const missing = [];
|
|
3782
|
-
const check = (
|
|
3783
|
-
if (name !== void 0 && !known.has(name)) missing.push({ path:
|
|
3786
|
+
const check = (path22, name) => {
|
|
3787
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
|
|
3784
3788
|
};
|
|
3785
3789
|
check("default", this.routing.default);
|
|
3786
3790
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3789,8 +3793,11 @@ var BackendRouter = class {
|
|
|
3789
3793
|
check("diagnostic", this.routing.diagnostic);
|
|
3790
3794
|
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3791
3795
|
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3796
|
+
check("isolation.none", this.routing.isolation?.none);
|
|
3797
|
+
check("isolation.container", this.routing.isolation?.container);
|
|
3798
|
+
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
3792
3799
|
if (missing.length > 0) {
|
|
3793
|
-
const detail = missing.map(({ path:
|
|
3800
|
+
const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
|
|
3794
3801
|
const known_ = [...known].join(", ") || "(none)";
|
|
3795
3802
|
throw new Error(
|
|
3796
3803
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3807,11 +3814,11 @@ import {
|
|
|
3807
3814
|
Ok as Ok10,
|
|
3808
3815
|
Err as Err7
|
|
3809
3816
|
} from "@harness-engineering/types";
|
|
3810
|
-
function resolveExitCode(code, command,
|
|
3817
|
+
function resolveExitCode(code, command, resolve7) {
|
|
3811
3818
|
if (code === 0) {
|
|
3812
|
-
|
|
3819
|
+
resolve7(Ok10(void 0));
|
|
3813
3820
|
} else {
|
|
3814
|
-
|
|
3821
|
+
resolve7(
|
|
3815
3822
|
Err7({
|
|
3816
3823
|
category: "agent_not_found",
|
|
3817
3824
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3819,8 +3826,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3819
3826
|
);
|
|
3820
3827
|
}
|
|
3821
3828
|
}
|
|
3822
|
-
function resolveSpawnError(command,
|
|
3823
|
-
|
|
3829
|
+
function resolveSpawnError(command, resolve7) {
|
|
3830
|
+
resolve7(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3824
3831
|
}
|
|
3825
3832
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3826
3833
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4133,10 +4140,10 @@ var ClaudeBackend = class {
|
|
|
4133
4140
|
errRl.close();
|
|
4134
4141
|
}
|
|
4135
4142
|
if (exitCode === null) {
|
|
4136
|
-
await new Promise((
|
|
4143
|
+
await new Promise((resolve7) => {
|
|
4137
4144
|
child.on("exit", (code) => {
|
|
4138
4145
|
exitCode = code;
|
|
4139
|
-
|
|
4146
|
+
resolve7(null);
|
|
4140
4147
|
});
|
|
4141
4148
|
});
|
|
4142
4149
|
}
|
|
@@ -4158,10 +4165,10 @@ var ClaudeBackend = class {
|
|
|
4158
4165
|
return Ok10(void 0);
|
|
4159
4166
|
}
|
|
4160
4167
|
async healthCheck() {
|
|
4161
|
-
return new Promise((
|
|
4168
|
+
return new Promise((resolve7) => {
|
|
4162
4169
|
const child = spawn2(this.command, ["--version"]);
|
|
4163
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4164
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4170
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
|
|
4171
|
+
child.on("error", () => resolveSpawnError(this.command, resolve7));
|
|
4165
4172
|
});
|
|
4166
4173
|
}
|
|
4167
4174
|
};
|
|
@@ -4784,7 +4791,7 @@ var PiBackend = class {
|
|
|
4784
4791
|
} else {
|
|
4785
4792
|
resolvedModelName = this.config.model;
|
|
4786
4793
|
}
|
|
4787
|
-
const piSdk = await import("@
|
|
4794
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4788
4795
|
const model = buildLocalModel({
|
|
4789
4796
|
model: resolvedModelName,
|
|
4790
4797
|
endpoint: this.config.endpoint,
|
|
@@ -4939,7 +4946,7 @@ var PiBackend = class {
|
|
|
4939
4946
|
}
|
|
4940
4947
|
async healthCheck() {
|
|
4941
4948
|
try {
|
|
4942
|
-
await import("@
|
|
4949
|
+
await import("@earendil-works/pi-coding-agent");
|
|
4943
4950
|
return Ok15(void 0);
|
|
4944
4951
|
} catch (err) {
|
|
4945
4952
|
return Err12({
|
|
@@ -4950,6 +4957,547 @@ var PiBackend = class {
|
|
|
4950
4957
|
}
|
|
4951
4958
|
};
|
|
4952
4959
|
|
|
4960
|
+
// src/agent/backends/ssh.ts
|
|
4961
|
+
import { spawn as spawn3 } from "child_process";
|
|
4962
|
+
import {
|
|
4963
|
+
Ok as Ok16,
|
|
4964
|
+
Err as Err13
|
|
4965
|
+
} from "@harness-engineering/types";
|
|
4966
|
+
var DEFAULT_TIMEOUT_MS2 = 9e4;
|
|
4967
|
+
var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
|
|
4968
|
+
var SshBackend = class {
|
|
4969
|
+
name = "ssh";
|
|
4970
|
+
config;
|
|
4971
|
+
spawnImpl;
|
|
4972
|
+
constructor(config) {
|
|
4973
|
+
if (!config.host || typeof config.host !== "string") {
|
|
4974
|
+
throw new Error("SshBackend: `host` is required");
|
|
4975
|
+
}
|
|
4976
|
+
if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
|
|
4977
|
+
throw new Error(
|
|
4978
|
+
`SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
|
|
4979
|
+
);
|
|
4980
|
+
}
|
|
4981
|
+
if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
|
|
4982
|
+
throw new Error("SshBackend: `remoteCommand` is required");
|
|
4983
|
+
}
|
|
4984
|
+
if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
|
|
4985
|
+
throw new Error(`SshBackend: invalid user '${config.user}'`);
|
|
4986
|
+
}
|
|
4987
|
+
this.config = {
|
|
4988
|
+
host: config.host,
|
|
4989
|
+
remoteCommand: config.remoteCommand,
|
|
4990
|
+
sshBinary: config.sshBinary ?? "ssh",
|
|
4991
|
+
sshOptions: config.sshOptions ?? [],
|
|
4992
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
4993
|
+
...config.user !== void 0 ? { user: config.user } : {},
|
|
4994
|
+
...config.port !== void 0 ? { port: config.port } : {},
|
|
4995
|
+
...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
|
|
4996
|
+
};
|
|
4997
|
+
this.spawnImpl = config.spawnImpl ?? spawn3;
|
|
4998
|
+
}
|
|
4999
|
+
/**
|
|
5000
|
+
* Builds the argv passed to the `ssh` binary. Exported as a method on
|
|
5001
|
+
* the class so tests can assert the exact shape without spawning.
|
|
5002
|
+
*
|
|
5003
|
+
* Layout: `[options..., target, '--', remoteCommand]`
|
|
5004
|
+
*/
|
|
5005
|
+
buildSshArgs() {
|
|
5006
|
+
const args = [];
|
|
5007
|
+
if (this.config.identityFile) {
|
|
5008
|
+
args.push("-i", this.config.identityFile);
|
|
5009
|
+
}
|
|
5010
|
+
if (this.config.port !== void 0) {
|
|
5011
|
+
args.push("-p", String(this.config.port));
|
|
5012
|
+
}
|
|
5013
|
+
args.push("-o", "BatchMode=yes");
|
|
5014
|
+
for (const opt of this.config.sshOptions) {
|
|
5015
|
+
args.push("-o", opt);
|
|
5016
|
+
}
|
|
5017
|
+
const target = this.config.user ? `${this.config.user}@${this.config.host}` : this.config.host;
|
|
5018
|
+
args.push(target);
|
|
5019
|
+
args.push("--");
|
|
5020
|
+
args.push(this.config.remoteCommand);
|
|
5021
|
+
return args;
|
|
5022
|
+
}
|
|
5023
|
+
async startSession(params) {
|
|
5024
|
+
const session = {
|
|
5025
|
+
sessionId: `ssh-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5026
|
+
workspacePath: params.workspacePath,
|
|
5027
|
+
backendName: this.name,
|
|
5028
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5029
|
+
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
5030
|
+
};
|
|
5031
|
+
return Ok16(session);
|
|
5032
|
+
}
|
|
5033
|
+
async *runTurn(session, params) {
|
|
5034
|
+
const child = this.spawnImpl(this.config.sshBinary, this.buildSshArgs(), {
|
|
5035
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5036
|
+
});
|
|
5037
|
+
const payload = JSON.stringify({
|
|
5038
|
+
kind: "turn",
|
|
5039
|
+
prompt: params.prompt,
|
|
5040
|
+
isContinuation: params.isContinuation,
|
|
5041
|
+
systemPrompt: session.systemPrompt
|
|
5042
|
+
});
|
|
5043
|
+
try {
|
|
5044
|
+
child.stdin.write(payload + "\n");
|
|
5045
|
+
child.stdin.end();
|
|
5046
|
+
} catch (err) {
|
|
5047
|
+
const message = err instanceof Error ? err.message : "failed to write to ssh stdin";
|
|
5048
|
+
try {
|
|
5049
|
+
child.kill("SIGTERM");
|
|
5050
|
+
} catch {
|
|
5051
|
+
}
|
|
5052
|
+
return errResult(session.sessionId, message);
|
|
5053
|
+
}
|
|
5054
|
+
const timeout = setTimeout(() => {
|
|
5055
|
+
try {
|
|
5056
|
+
child.kill("SIGTERM");
|
|
5057
|
+
} catch {
|
|
5058
|
+
}
|
|
5059
|
+
}, this.config.timeoutMs);
|
|
5060
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5061
|
+
let success = true;
|
|
5062
|
+
let lastError;
|
|
5063
|
+
try {
|
|
5064
|
+
for await (const line of readLines(child.stdout)) {
|
|
5065
|
+
let event;
|
|
5066
|
+
try {
|
|
5067
|
+
event = parseEvent(line, session.sessionId);
|
|
5068
|
+
} catch (err) {
|
|
5069
|
+
const message = err instanceof Error ? err.message : "unparseable ssh event";
|
|
5070
|
+
success = false;
|
|
5071
|
+
lastError = message;
|
|
5072
|
+
break;
|
|
5073
|
+
}
|
|
5074
|
+
if (!event) continue;
|
|
5075
|
+
if (event.usage) finalUsage = event.usage;
|
|
5076
|
+
if (event.type === "error" && typeof event.content === "string") {
|
|
5077
|
+
lastError = event.content;
|
|
5078
|
+
success = false;
|
|
5079
|
+
}
|
|
5080
|
+
yield event;
|
|
5081
|
+
}
|
|
5082
|
+
const exitCode = await waitForExit(child);
|
|
5083
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
5084
|
+
success = false;
|
|
5085
|
+
lastError = lastError ?? `ssh exited with code ${exitCode}`;
|
|
5086
|
+
}
|
|
5087
|
+
} finally {
|
|
5088
|
+
clearTimeout(timeout);
|
|
5089
|
+
}
|
|
5090
|
+
return {
|
|
5091
|
+
success,
|
|
5092
|
+
sessionId: session.sessionId,
|
|
5093
|
+
usage: finalUsage,
|
|
5094
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5095
|
+
};
|
|
5096
|
+
}
|
|
5097
|
+
async stopSession(_session) {
|
|
5098
|
+
return Ok16(void 0);
|
|
5099
|
+
}
|
|
5100
|
+
async healthCheck() {
|
|
5101
|
+
const args = [...this.buildSshArgs()];
|
|
5102
|
+
args[args.length - 1] = "true";
|
|
5103
|
+
return new Promise((resolve7) => {
|
|
5104
|
+
let child;
|
|
5105
|
+
try {
|
|
5106
|
+
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5107
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
5108
|
+
});
|
|
5109
|
+
} catch (err) {
|
|
5110
|
+
resolve7(
|
|
5111
|
+
Err13({
|
|
5112
|
+
category: "agent_not_found",
|
|
5113
|
+
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
5114
|
+
})
|
|
5115
|
+
);
|
|
5116
|
+
return;
|
|
5117
|
+
}
|
|
5118
|
+
let stderr = "";
|
|
5119
|
+
child.stderr?.on("data", (chunk) => {
|
|
5120
|
+
stderr += chunk.toString();
|
|
5121
|
+
});
|
|
5122
|
+
const timer = setTimeout(() => {
|
|
5123
|
+
try {
|
|
5124
|
+
child.kill("SIGTERM");
|
|
5125
|
+
} catch {
|
|
5126
|
+
}
|
|
5127
|
+
}, this.config.timeoutMs);
|
|
5128
|
+
child.on("close", (code) => {
|
|
5129
|
+
clearTimeout(timer);
|
|
5130
|
+
if (code === 0) {
|
|
5131
|
+
resolve7(Ok16(void 0));
|
|
5132
|
+
} else {
|
|
5133
|
+
resolve7(
|
|
5134
|
+
Err13({
|
|
5135
|
+
category: "agent_not_found",
|
|
5136
|
+
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
5137
|
+
})
|
|
5138
|
+
);
|
|
5139
|
+
}
|
|
5140
|
+
});
|
|
5141
|
+
child.on("error", (err) => {
|
|
5142
|
+
clearTimeout(timer);
|
|
5143
|
+
resolve7(Err13({ category: "agent_not_found", message: err.message }));
|
|
5144
|
+
});
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
5147
|
+
};
|
|
5148
|
+
function errResult(sessionId, message) {
|
|
5149
|
+
return {
|
|
5150
|
+
success: false,
|
|
5151
|
+
sessionId,
|
|
5152
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5153
|
+
error: message
|
|
5154
|
+
};
|
|
5155
|
+
}
|
|
5156
|
+
function parseEvent(line, sessionId) {
|
|
5157
|
+
const trimmed = line.trim();
|
|
5158
|
+
if (trimmed.length === 0) return null;
|
|
5159
|
+
const raw = JSON.parse(trimmed);
|
|
5160
|
+
if (typeof raw.type !== "string") {
|
|
5161
|
+
throw new Error(`ssh event missing 'type': ${trimmed.slice(0, 200)}`);
|
|
5162
|
+
}
|
|
5163
|
+
const ev = {
|
|
5164
|
+
type: raw.type,
|
|
5165
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5166
|
+
sessionId
|
|
5167
|
+
};
|
|
5168
|
+
if (typeof raw.subtype === "string") ev.subtype = raw.subtype;
|
|
5169
|
+
if (raw.content !== void 0) ev.content = raw.content;
|
|
5170
|
+
if (isUsage(raw.usage)) ev.usage = raw.usage;
|
|
5171
|
+
return ev;
|
|
5172
|
+
}
|
|
5173
|
+
function isUsage(u) {
|
|
5174
|
+
if (!u || typeof u !== "object") return false;
|
|
5175
|
+
const o = u;
|
|
5176
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5177
|
+
}
|
|
5178
|
+
async function* readLines(stream) {
|
|
5179
|
+
let buffer = "";
|
|
5180
|
+
for await (const chunk of stream) {
|
|
5181
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5182
|
+
let idx;
|
|
5183
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5184
|
+
yield buffer.slice(0, idx);
|
|
5185
|
+
buffer = buffer.slice(idx + 1);
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5188
|
+
if (buffer.length > 0) yield buffer;
|
|
5189
|
+
}
|
|
5190
|
+
function waitForExit(child) {
|
|
5191
|
+
return new Promise((resolve7) => {
|
|
5192
|
+
if (child.exitCode !== null) {
|
|
5193
|
+
resolve7(child.exitCode);
|
|
5194
|
+
return;
|
|
5195
|
+
}
|
|
5196
|
+
child.once("close", (code) => resolve7(code));
|
|
5197
|
+
child.once("error", () => resolve7(null));
|
|
5198
|
+
});
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
// src/agent/backends/serverless.ts
|
|
5202
|
+
import { spawn as spawn4 } from "child_process";
|
|
5203
|
+
import {
|
|
5204
|
+
Ok as Ok17,
|
|
5205
|
+
Err as Err14
|
|
5206
|
+
} from "@harness-engineering/types";
|
|
5207
|
+
var ServerlessBackend = class {
|
|
5208
|
+
handles = /* @__PURE__ */ new Map();
|
|
5209
|
+
async startSession(params) {
|
|
5210
|
+
const start = await this.coldStart(params);
|
|
5211
|
+
if (!start.ok) return start;
|
|
5212
|
+
const session = {
|
|
5213
|
+
sessionId: `${this.name}-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5214
|
+
workspacePath: params.workspacePath,
|
|
5215
|
+
backendName: this.name,
|
|
5216
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5217
|
+
};
|
|
5218
|
+
this.handles.set(session.sessionId, start.value);
|
|
5219
|
+
return Ok17(session);
|
|
5220
|
+
}
|
|
5221
|
+
async *runTurn(session, params) {
|
|
5222
|
+
const handle = this.handles.get(session.sessionId);
|
|
5223
|
+
if (!handle) {
|
|
5224
|
+
return {
|
|
5225
|
+
success: false,
|
|
5226
|
+
sessionId: session.sessionId,
|
|
5227
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5228
|
+
error: `no serverless handle for session ${session.sessionId}`
|
|
5229
|
+
};
|
|
5230
|
+
}
|
|
5231
|
+
return yield* this.runOnHandle(handle, params, session);
|
|
5232
|
+
}
|
|
5233
|
+
async stopSession(session) {
|
|
5234
|
+
const handle = this.handles.get(session.sessionId);
|
|
5235
|
+
if (!handle) return Ok17(void 0);
|
|
5236
|
+
this.handles.delete(session.sessionId);
|
|
5237
|
+
return this.teardown(handle);
|
|
5238
|
+
}
|
|
5239
|
+
};
|
|
5240
|
+
var FORBIDDEN_IMAGE_CHARS = /[;&|`$()\n\r<>]/;
|
|
5241
|
+
var BLOCKED_DOCKER_FLAGS = [
|
|
5242
|
+
"--privileged",
|
|
5243
|
+
"--cap-add",
|
|
5244
|
+
"--security-opt",
|
|
5245
|
+
"--pid",
|
|
5246
|
+
"--ipc",
|
|
5247
|
+
"--userns"
|
|
5248
|
+
];
|
|
5249
|
+
var DEFAULT_OCI_TIMEOUT_MS = 9e4;
|
|
5250
|
+
var OciServerlessBackend = class extends ServerlessBackend {
|
|
5251
|
+
name = "serverless:oci";
|
|
5252
|
+
config;
|
|
5253
|
+
spawnImpl;
|
|
5254
|
+
envSource;
|
|
5255
|
+
constructor(config) {
|
|
5256
|
+
super();
|
|
5257
|
+
if (!config.image || typeof config.image !== "string") {
|
|
5258
|
+
throw new Error("OciServerlessBackend: `image` is required");
|
|
5259
|
+
}
|
|
5260
|
+
if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
|
|
5261
|
+
throw new Error(
|
|
5262
|
+
`OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
|
|
5263
|
+
);
|
|
5264
|
+
}
|
|
5265
|
+
this.config = {
|
|
5266
|
+
image: config.image,
|
|
5267
|
+
pullPolicy: config.pullPolicy ?? "if-not-present",
|
|
5268
|
+
runtime: config.runtime ?? "docker",
|
|
5269
|
+
envPassthrough: config.envPassthrough ?? [],
|
|
5270
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
|
|
5271
|
+
extraArgs: sanitizeExtraArgs(config.extraArgs),
|
|
5272
|
+
...config.registry !== void 0 ? { registry: config.registry } : {}
|
|
5273
|
+
};
|
|
5274
|
+
this.spawnImpl = config.spawnImpl ?? spawn4;
|
|
5275
|
+
this.envSource = config.envSource ?? process.env;
|
|
5276
|
+
}
|
|
5277
|
+
/** Builds the argv for `docker run -d ...`. Exposed for tests. */
|
|
5278
|
+
buildRunArgs() {
|
|
5279
|
+
const env = this.collectEnv();
|
|
5280
|
+
const args = ["run", "-d", "--rm"];
|
|
5281
|
+
for (const [k, v] of Object.entries(env)) {
|
|
5282
|
+
args.push("-e", `${k}=${v}`);
|
|
5283
|
+
}
|
|
5284
|
+
for (const ea of this.config.extraArgs) {
|
|
5285
|
+
args.push(ea);
|
|
5286
|
+
}
|
|
5287
|
+
args.push("--");
|
|
5288
|
+
args.push(this.config.image);
|
|
5289
|
+
return args;
|
|
5290
|
+
}
|
|
5291
|
+
/** Builds the argv for `docker exec <id> -- agent`. Exposed for tests. */
|
|
5292
|
+
buildExecArgs(handleId) {
|
|
5293
|
+
return ["exec", "-i", handleId, "/agent"];
|
|
5294
|
+
}
|
|
5295
|
+
async coldStart(_params) {
|
|
5296
|
+
if (this.config.pullPolicy === "always") {
|
|
5297
|
+
const pull = await this.runOneShot(this.config.runtime, ["pull", this.config.image]);
|
|
5298
|
+
if (!pull.ok) return pull;
|
|
5299
|
+
}
|
|
5300
|
+
const result = await this.runOneShot(this.config.runtime, this.buildRunArgs());
|
|
5301
|
+
if (!result.ok) return result;
|
|
5302
|
+
const id = result.value.trim().split(/\s+/)[0] ?? "";
|
|
5303
|
+
if (!id) {
|
|
5304
|
+
return Err14({
|
|
5305
|
+
category: "response_error",
|
|
5306
|
+
message: "OciServerlessBackend: empty container id from runtime"
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
return Ok17({ id, adapter: this.name });
|
|
5310
|
+
}
|
|
5311
|
+
async *runOnHandle(handle, params, session) {
|
|
5312
|
+
const child = this.spawnImpl(this.config.runtime, this.buildExecArgs(handle.id), {
|
|
5313
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5314
|
+
});
|
|
5315
|
+
const payload = JSON.stringify({
|
|
5316
|
+
kind: "turn",
|
|
5317
|
+
prompt: params.prompt,
|
|
5318
|
+
isContinuation: params.isContinuation
|
|
5319
|
+
});
|
|
5320
|
+
try {
|
|
5321
|
+
child.stdin.write(payload + "\n");
|
|
5322
|
+
child.stdin.end();
|
|
5323
|
+
} catch (err) {
|
|
5324
|
+
const message = err instanceof Error ? err.message : "failed to write to docker stdin";
|
|
5325
|
+
return turnFailure(session.sessionId, message);
|
|
5326
|
+
}
|
|
5327
|
+
const timeout = setTimeout(() => {
|
|
5328
|
+
try {
|
|
5329
|
+
child.kill("SIGTERM");
|
|
5330
|
+
} catch {
|
|
5331
|
+
}
|
|
5332
|
+
}, this.config.timeoutMs);
|
|
5333
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5334
|
+
let success = true;
|
|
5335
|
+
let lastError;
|
|
5336
|
+
try {
|
|
5337
|
+
for await (const line of readLines2(child.stdout)) {
|
|
5338
|
+
const ev = tryParseEvent(line, session.sessionId);
|
|
5339
|
+
if (!ev) continue;
|
|
5340
|
+
if (ev.usage) finalUsage = ev.usage;
|
|
5341
|
+
if (ev.type === "error" && typeof ev.content === "string") {
|
|
5342
|
+
success = false;
|
|
5343
|
+
lastError = ev.content;
|
|
5344
|
+
}
|
|
5345
|
+
yield ev;
|
|
5346
|
+
}
|
|
5347
|
+
const code = await waitForExit2(child);
|
|
5348
|
+
if (code !== 0 && code !== null) {
|
|
5349
|
+
success = false;
|
|
5350
|
+
lastError = lastError ?? `runtime exec exited with code ${code}`;
|
|
5351
|
+
}
|
|
5352
|
+
} finally {
|
|
5353
|
+
clearTimeout(timeout);
|
|
5354
|
+
}
|
|
5355
|
+
return {
|
|
5356
|
+
success,
|
|
5357
|
+
sessionId: session.sessionId,
|
|
5358
|
+
usage: finalUsage,
|
|
5359
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5360
|
+
};
|
|
5361
|
+
}
|
|
5362
|
+
async teardown(handle) {
|
|
5363
|
+
if (handle.adapter !== this.name) {
|
|
5364
|
+
return Err14({
|
|
5365
|
+
category: "response_error",
|
|
5366
|
+
message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
|
|
5367
|
+
});
|
|
5368
|
+
}
|
|
5369
|
+
const stop = await this.runOneShot(this.config.runtime, ["stop", handle.id]);
|
|
5370
|
+
if (!stop.ok) return stop;
|
|
5371
|
+
return Ok17(void 0);
|
|
5372
|
+
}
|
|
5373
|
+
async healthCheck() {
|
|
5374
|
+
return mapOk(
|
|
5375
|
+
await this.runOneShot(this.config.runtime, ["version", "--format", "{{.Server.Version}}"])
|
|
5376
|
+
);
|
|
5377
|
+
}
|
|
5378
|
+
collectEnv() {
|
|
5379
|
+
const out = {};
|
|
5380
|
+
for (const key of this.config.envPassthrough) {
|
|
5381
|
+
const val = this.envSource[key];
|
|
5382
|
+
if (typeof val === "string") out[key] = val;
|
|
5383
|
+
}
|
|
5384
|
+
return out;
|
|
5385
|
+
}
|
|
5386
|
+
runOneShot(binary, args) {
|
|
5387
|
+
return new Promise((resolve7) => {
|
|
5388
|
+
let child;
|
|
5389
|
+
try {
|
|
5390
|
+
child = this.spawnImpl(binary, args, {
|
|
5391
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5392
|
+
});
|
|
5393
|
+
} catch (err) {
|
|
5394
|
+
resolve7(
|
|
5395
|
+
Err14({
|
|
5396
|
+
category: "agent_not_found",
|
|
5397
|
+
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
5398
|
+
})
|
|
5399
|
+
);
|
|
5400
|
+
return;
|
|
5401
|
+
}
|
|
5402
|
+
let stdout = "";
|
|
5403
|
+
let stderr = "";
|
|
5404
|
+
child.stdout?.on("data", (chunk) => {
|
|
5405
|
+
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5406
|
+
});
|
|
5407
|
+
child.stderr?.on("data", (chunk) => {
|
|
5408
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5409
|
+
});
|
|
5410
|
+
const timer = setTimeout(() => {
|
|
5411
|
+
try {
|
|
5412
|
+
child.kill("SIGTERM");
|
|
5413
|
+
} catch {
|
|
5414
|
+
}
|
|
5415
|
+
}, this.config.timeoutMs);
|
|
5416
|
+
child.on("close", (code) => {
|
|
5417
|
+
clearTimeout(timer);
|
|
5418
|
+
if (code === 0) {
|
|
5419
|
+
resolve7(Ok17(stdout));
|
|
5420
|
+
} else {
|
|
5421
|
+
resolve7(
|
|
5422
|
+
Err14({
|
|
5423
|
+
category: "response_error",
|
|
5424
|
+
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
5425
|
+
})
|
|
5426
|
+
);
|
|
5427
|
+
}
|
|
5428
|
+
});
|
|
5429
|
+
child.on("error", (err) => {
|
|
5430
|
+
clearTimeout(timer);
|
|
5431
|
+
resolve7(Err14({ category: "agent_not_found", message: err.message }));
|
|
5432
|
+
});
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5435
|
+
};
|
|
5436
|
+
function sanitizeExtraArgs(extraArgs) {
|
|
5437
|
+
if (!extraArgs) return [];
|
|
5438
|
+
return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5439
|
+
}
|
|
5440
|
+
function mapOk(r) {
|
|
5441
|
+
return r.ok ? Ok17(void 0) : r;
|
|
5442
|
+
}
|
|
5443
|
+
function turnFailure(sessionId, message) {
|
|
5444
|
+
return {
|
|
5445
|
+
success: false,
|
|
5446
|
+
sessionId,
|
|
5447
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5448
|
+
error: message
|
|
5449
|
+
};
|
|
5450
|
+
}
|
|
5451
|
+
function tryParseEvent(line, sessionId) {
|
|
5452
|
+
const trimmed = line.trim();
|
|
5453
|
+
if (!trimmed) return null;
|
|
5454
|
+
let raw;
|
|
5455
|
+
try {
|
|
5456
|
+
raw = JSON.parse(trimmed);
|
|
5457
|
+
} catch {
|
|
5458
|
+
return null;
|
|
5459
|
+
}
|
|
5460
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5461
|
+
const o = raw;
|
|
5462
|
+
if (typeof o.type !== "string") return null;
|
|
5463
|
+
const ev = {
|
|
5464
|
+
type: o.type,
|
|
5465
|
+
timestamp: typeof o.timestamp === "string" ? o.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5466
|
+
sessionId
|
|
5467
|
+
};
|
|
5468
|
+
if (typeof o.subtype === "string") ev.subtype = o.subtype;
|
|
5469
|
+
if (o.content !== void 0) ev.content = o.content;
|
|
5470
|
+
if (isUsage2(o.usage)) ev.usage = o.usage;
|
|
5471
|
+
return ev;
|
|
5472
|
+
}
|
|
5473
|
+
function isUsage2(u) {
|
|
5474
|
+
if (!u || typeof u !== "object") return false;
|
|
5475
|
+
const o = u;
|
|
5476
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5477
|
+
}
|
|
5478
|
+
async function* readLines2(stream) {
|
|
5479
|
+
let buffer = "";
|
|
5480
|
+
for await (const chunk of stream) {
|
|
5481
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5482
|
+
let idx;
|
|
5483
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5484
|
+
yield buffer.slice(0, idx);
|
|
5485
|
+
buffer = buffer.slice(idx + 1);
|
|
5486
|
+
}
|
|
5487
|
+
}
|
|
5488
|
+
if (buffer.length > 0) yield buffer;
|
|
5489
|
+
}
|
|
5490
|
+
function waitForExit2(child) {
|
|
5491
|
+
return new Promise((resolve7) => {
|
|
5492
|
+
if (child.exitCode !== null) {
|
|
5493
|
+
resolve7(child.exitCode);
|
|
5494
|
+
return;
|
|
5495
|
+
}
|
|
5496
|
+
child.once("close", (code) => resolve7(code));
|
|
5497
|
+
child.once("error", () => resolve7(null));
|
|
5498
|
+
});
|
|
5499
|
+
}
|
|
5500
|
+
|
|
4953
5501
|
// src/agent/backend-factory.ts
|
|
4954
5502
|
function makeGetModel(model) {
|
|
4955
5503
|
if (typeof model === "string") return () => model;
|
|
@@ -4999,6 +5547,35 @@ function createBackend(def, options = {}) {
|
|
|
4999
5547
|
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5000
5548
|
});
|
|
5001
5549
|
}
|
|
5550
|
+
case "ssh": {
|
|
5551
|
+
return new SshBackend({
|
|
5552
|
+
host: def.host,
|
|
5553
|
+
remoteCommand: def.remoteCommand,
|
|
5554
|
+
...def.user !== void 0 ? { user: def.user } : {},
|
|
5555
|
+
...def.port !== void 0 ? { port: def.port } : {},
|
|
5556
|
+
...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
|
|
5557
|
+
...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
|
|
5558
|
+
...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
5561
|
+
case "serverless": {
|
|
5562
|
+
switch (def.adapter) {
|
|
5563
|
+
case "oci":
|
|
5564
|
+
return new OciServerlessBackend({
|
|
5565
|
+
image: def.image,
|
|
5566
|
+
...def.registry !== void 0 ? { registry: def.registry } : {},
|
|
5567
|
+
...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
|
|
5568
|
+
...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
|
|
5569
|
+
...def.runtime !== void 0 ? { runtime: def.runtime } : {}
|
|
5570
|
+
});
|
|
5571
|
+
default: {
|
|
5572
|
+
const exhaustive = def.adapter;
|
|
5573
|
+
throw new Error(
|
|
5574
|
+
`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
|
|
5575
|
+
);
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5002
5579
|
default: {
|
|
5003
5580
|
const exhaustive = def;
|
|
5004
5581
|
throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
|
|
@@ -5008,13 +5585,13 @@ function createBackend(def, options = {}) {
|
|
|
5008
5585
|
|
|
5009
5586
|
// src/agent/backends/container.ts
|
|
5010
5587
|
import {
|
|
5011
|
-
Err as
|
|
5588
|
+
Err as Err15
|
|
5012
5589
|
} from "@harness-engineering/types";
|
|
5013
5590
|
function toAgentError(message, details) {
|
|
5014
5591
|
return { category: "response_error", message, details };
|
|
5015
5592
|
}
|
|
5016
5593
|
var BLOCKED_FLAGS = ["--privileged", "--cap-add", "--security-opt", "--pid", "--ipc", "--userns"];
|
|
5017
|
-
function
|
|
5594
|
+
function sanitizeExtraArgs2(extraArgs) {
|
|
5018
5595
|
if (!extraArgs) return [];
|
|
5019
5596
|
return extraArgs.filter((arg) => !BLOCKED_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5020
5597
|
}
|
|
@@ -5040,7 +5617,7 @@ var ContainerBackend = class {
|
|
|
5040
5617
|
}
|
|
5041
5618
|
const result = await this.secretBackend.resolveSecrets(this.secretKeys);
|
|
5042
5619
|
if (!result.ok) {
|
|
5043
|
-
return
|
|
5620
|
+
return Err15(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
|
|
5044
5621
|
}
|
|
5045
5622
|
return { ok: true, value: result.value };
|
|
5046
5623
|
}
|
|
@@ -5053,7 +5630,7 @@ var ContainerBackend = class {
|
|
|
5053
5630
|
network: this.containerConfig.network ?? "none",
|
|
5054
5631
|
env
|
|
5055
5632
|
};
|
|
5056
|
-
const sanitized =
|
|
5633
|
+
const sanitized = sanitizeExtraArgs2(this.containerConfig.extraArgs);
|
|
5057
5634
|
if (sanitized.length > 0) {
|
|
5058
5635
|
opts.extraArgs = sanitized;
|
|
5059
5636
|
}
|
|
@@ -5065,7 +5642,7 @@ var ContainerBackend = class {
|
|
|
5065
5642
|
const createOpts = this.buildCreateOpts(params, envResult.value);
|
|
5066
5643
|
const containerResult = await this.runtime.createContainer(createOpts);
|
|
5067
5644
|
if (!containerResult.ok) {
|
|
5068
|
-
return
|
|
5645
|
+
return Err15(
|
|
5069
5646
|
toAgentError(
|
|
5070
5647
|
`Container creation failed: ${containerResult.error.message}`,
|
|
5071
5648
|
containerResult.error
|
|
@@ -5090,7 +5667,7 @@ var ContainerBackend = class {
|
|
|
5090
5667
|
this.containerHandles.delete(session.sessionId);
|
|
5091
5668
|
const removeResult = await this.runtime.removeContainer(handle);
|
|
5092
5669
|
if (!removeResult.ok) {
|
|
5093
|
-
return
|
|
5670
|
+
return Err15(
|
|
5094
5671
|
toAgentError(
|
|
5095
5672
|
`Container removal failed: ${removeResult.error.message}`,
|
|
5096
5673
|
removeResult.error
|
|
@@ -5103,7 +5680,7 @@ var ContainerBackend = class {
|
|
|
5103
5680
|
async healthCheck() {
|
|
5104
5681
|
const runtimeResult = await this.runtime.healthCheck();
|
|
5105
5682
|
if (!runtimeResult.ok) {
|
|
5106
|
-
return
|
|
5683
|
+
return Err15({
|
|
5107
5684
|
category: "agent_not_found",
|
|
5108
5685
|
message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
|
|
5109
5686
|
details: runtimeResult.error
|
|
@@ -5114,16 +5691,16 @@ var ContainerBackend = class {
|
|
|
5114
5691
|
};
|
|
5115
5692
|
|
|
5116
5693
|
// src/agent/runtime/docker.ts
|
|
5117
|
-
import { execFile as execFile3, spawn as
|
|
5118
|
-
import { Ok as
|
|
5694
|
+
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
5695
|
+
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5119
5696
|
function dockerExec(args) {
|
|
5120
|
-
return new Promise((
|
|
5697
|
+
return new Promise((resolve7, reject) => {
|
|
5121
5698
|
execFile3("docker", args, (error, stdout) => {
|
|
5122
5699
|
if (error) {
|
|
5123
5700
|
reject(error);
|
|
5124
5701
|
return;
|
|
5125
5702
|
}
|
|
5126
|
-
|
|
5703
|
+
resolve7(stdout.trim());
|
|
5127
5704
|
});
|
|
5128
5705
|
});
|
|
5129
5706
|
}
|
|
@@ -5148,9 +5725,9 @@ var DockerRuntime = class {
|
|
|
5148
5725
|
args.push(opts.image);
|
|
5149
5726
|
args.push("sleep", "infinity");
|
|
5150
5727
|
const containerId = await dockerExec(args);
|
|
5151
|
-
return
|
|
5728
|
+
return Ok18({ containerId, runtime: this.name });
|
|
5152
5729
|
} catch (error) {
|
|
5153
|
-
return
|
|
5730
|
+
return Err16({
|
|
5154
5731
|
category: "container_create_failed",
|
|
5155
5732
|
message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5156
5733
|
details: error
|
|
@@ -5172,7 +5749,7 @@ var DockerRuntime = class {
|
|
|
5172
5749
|
}
|
|
5173
5750
|
}
|
|
5174
5751
|
execArgs.push(handle.containerId, ...cmd);
|
|
5175
|
-
const child =
|
|
5752
|
+
const child = spawn5("docker", execArgs);
|
|
5176
5753
|
const readline3 = await import("readline");
|
|
5177
5754
|
const rl = readline3.createInterface({ input: child.stdout, terminal: false });
|
|
5178
5755
|
try {
|
|
@@ -5182,11 +5759,11 @@ var DockerRuntime = class {
|
|
|
5182
5759
|
} finally {
|
|
5183
5760
|
rl.close();
|
|
5184
5761
|
}
|
|
5185
|
-
const exitCode = await new Promise((
|
|
5762
|
+
const exitCode = await new Promise((resolve7) => {
|
|
5186
5763
|
if (child.exitCode !== null) {
|
|
5187
|
-
|
|
5764
|
+
resolve7(child.exitCode);
|
|
5188
5765
|
} else {
|
|
5189
|
-
child.on("exit", (code) =>
|
|
5766
|
+
child.on("exit", (code) => resolve7(code ?? 1));
|
|
5190
5767
|
}
|
|
5191
5768
|
});
|
|
5192
5769
|
return exitCode;
|
|
@@ -5194,9 +5771,9 @@ var DockerRuntime = class {
|
|
|
5194
5771
|
async removeContainer(handle) {
|
|
5195
5772
|
try {
|
|
5196
5773
|
await dockerExec(["rm", "-f", handle.containerId]);
|
|
5197
|
-
return
|
|
5774
|
+
return Ok18(void 0);
|
|
5198
5775
|
} catch (error) {
|
|
5199
|
-
return
|
|
5776
|
+
return Err16({
|
|
5200
5777
|
category: "container_remove_failed",
|
|
5201
5778
|
message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5202
5779
|
details: error
|
|
@@ -5206,9 +5783,9 @@ var DockerRuntime = class {
|
|
|
5206
5783
|
async healthCheck() {
|
|
5207
5784
|
try {
|
|
5208
5785
|
await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
|
|
5209
|
-
return
|
|
5786
|
+
return Ok18(void 0);
|
|
5210
5787
|
} catch (error) {
|
|
5211
|
-
return
|
|
5788
|
+
return Err16({
|
|
5212
5789
|
category: "runtime_not_found",
|
|
5213
5790
|
message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
5214
5791
|
details: error
|
|
@@ -5218,7 +5795,7 @@ var DockerRuntime = class {
|
|
|
5218
5795
|
};
|
|
5219
5796
|
|
|
5220
5797
|
// src/agent/secrets/env.ts
|
|
5221
|
-
import { Ok as
|
|
5798
|
+
import { Ok as Ok19, Err as Err17 } from "@harness-engineering/types";
|
|
5222
5799
|
var EnvSecretBackend = class {
|
|
5223
5800
|
name = "env";
|
|
5224
5801
|
async resolveSecrets(keys) {
|
|
@@ -5226,7 +5803,7 @@ var EnvSecretBackend = class {
|
|
|
5226
5803
|
for (const key of keys) {
|
|
5227
5804
|
const value = process.env[key];
|
|
5228
5805
|
if (value === void 0) {
|
|
5229
|
-
return
|
|
5806
|
+
return Err17({
|
|
5230
5807
|
category: "secret_not_found",
|
|
5231
5808
|
message: `Environment variable '${key}' is not set`,
|
|
5232
5809
|
key
|
|
@@ -5234,24 +5811,24 @@ var EnvSecretBackend = class {
|
|
|
5234
5811
|
}
|
|
5235
5812
|
secrets[key] = value;
|
|
5236
5813
|
}
|
|
5237
|
-
return
|
|
5814
|
+
return Ok19(secrets);
|
|
5238
5815
|
}
|
|
5239
5816
|
async healthCheck() {
|
|
5240
|
-
return
|
|
5817
|
+
return Ok19(void 0);
|
|
5241
5818
|
}
|
|
5242
5819
|
};
|
|
5243
5820
|
|
|
5244
5821
|
// src/agent/secrets/onepassword.ts
|
|
5245
5822
|
import { execFile as execFile4 } from "child_process";
|
|
5246
|
-
import { Ok as
|
|
5823
|
+
import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
|
|
5247
5824
|
function opExec(args) {
|
|
5248
|
-
return new Promise((
|
|
5825
|
+
return new Promise((resolve7, reject) => {
|
|
5249
5826
|
execFile4("op", args, (error, stdout) => {
|
|
5250
5827
|
if (error) {
|
|
5251
5828
|
reject(error);
|
|
5252
5829
|
return;
|
|
5253
5830
|
}
|
|
5254
|
-
|
|
5831
|
+
resolve7(stdout.trim());
|
|
5255
5832
|
});
|
|
5256
5833
|
});
|
|
5257
5834
|
}
|
|
@@ -5268,21 +5845,21 @@ var OnePasswordSecretBackend = class {
|
|
|
5268
5845
|
const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
|
|
5269
5846
|
secrets[key] = value;
|
|
5270
5847
|
} catch (error) {
|
|
5271
|
-
return
|
|
5848
|
+
return Err18({
|
|
5272
5849
|
category: "access_denied",
|
|
5273
5850
|
message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
|
|
5274
5851
|
key
|
|
5275
5852
|
});
|
|
5276
5853
|
}
|
|
5277
5854
|
}
|
|
5278
|
-
return
|
|
5855
|
+
return Ok20(secrets);
|
|
5279
5856
|
}
|
|
5280
5857
|
async healthCheck() {
|
|
5281
5858
|
try {
|
|
5282
5859
|
await opExec(["--version"]);
|
|
5283
|
-
return
|
|
5860
|
+
return Ok20(void 0);
|
|
5284
5861
|
} catch (error) {
|
|
5285
|
-
return
|
|
5862
|
+
return Err18({
|
|
5286
5863
|
category: "provider_unavailable",
|
|
5287
5864
|
message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5288
5865
|
});
|
|
@@ -5292,15 +5869,15 @@ var OnePasswordSecretBackend = class {
|
|
|
5292
5869
|
|
|
5293
5870
|
// src/agent/secrets/vault.ts
|
|
5294
5871
|
import { execFile as execFile5 } from "child_process";
|
|
5295
|
-
import { Ok as
|
|
5872
|
+
import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
|
|
5296
5873
|
function vaultExec(args, env) {
|
|
5297
|
-
return new Promise((
|
|
5874
|
+
return new Promise((resolve7, reject) => {
|
|
5298
5875
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5299
5876
|
if (error) {
|
|
5300
5877
|
reject(error);
|
|
5301
5878
|
return;
|
|
5302
5879
|
}
|
|
5303
|
-
|
|
5880
|
+
resolve7(stdout.trim());
|
|
5304
5881
|
});
|
|
5305
5882
|
});
|
|
5306
5883
|
}
|
|
@@ -5323,11 +5900,11 @@ var VaultSecretBackend = class {
|
|
|
5323
5900
|
} catch (error) {
|
|
5324
5901
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5325
5902
|
const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
|
|
5326
|
-
return
|
|
5903
|
+
return Err19({ category, message: `Failed to read from Vault: ${msg}` });
|
|
5327
5904
|
}
|
|
5328
5905
|
const missing = keys.find((k) => !(k in data));
|
|
5329
5906
|
if (missing) {
|
|
5330
|
-
return
|
|
5907
|
+
return Err19({
|
|
5331
5908
|
category: "secret_not_found",
|
|
5332
5909
|
message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
|
|
5333
5910
|
key: missing
|
|
@@ -5335,14 +5912,14 @@ var VaultSecretBackend = class {
|
|
|
5335
5912
|
}
|
|
5336
5913
|
const secrets = {};
|
|
5337
5914
|
for (const key of keys) secrets[key] = data[key];
|
|
5338
|
-
return
|
|
5915
|
+
return Ok21(secrets);
|
|
5339
5916
|
}
|
|
5340
5917
|
async healthCheck() {
|
|
5341
5918
|
try {
|
|
5342
5919
|
await vaultExec(["version"]);
|
|
5343
|
-
return
|
|
5920
|
+
return Ok21(void 0);
|
|
5344
5921
|
} catch (error) {
|
|
5345
|
-
return
|
|
5922
|
+
return Err19({
|
|
5346
5923
|
category: "provider_unavailable",
|
|
5347
5924
|
message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5348
5925
|
});
|
|
@@ -5488,6 +6065,8 @@ function buildAnalysisProvider(args) {
|
|
|
5488
6065
|
return buildClaudeCliProvider(def, args, layerModel);
|
|
5489
6066
|
case "mock":
|
|
5490
6067
|
case "gemini":
|
|
6068
|
+
case "ssh":
|
|
6069
|
+
case "serverless":
|
|
5491
6070
|
logger.warn(
|
|
5492
6071
|
`Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
|
|
5493
6072
|
);
|
|
@@ -5670,7 +6249,7 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
5670
6249
|
|
|
5671
6250
|
// src/server/http.ts
|
|
5672
6251
|
import * as http from "http";
|
|
5673
|
-
import * as
|
|
6252
|
+
import * as path15 from "path";
|
|
5674
6253
|
import { assertPortUsable } from "@harness-engineering/core";
|
|
5675
6254
|
|
|
5676
6255
|
// src/server/websocket.ts
|
|
@@ -5733,7 +6312,7 @@ import { z as z3 } from "zod";
|
|
|
5733
6312
|
// src/server/utils.ts
|
|
5734
6313
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
5735
6314
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
5736
|
-
return new Promise((
|
|
6315
|
+
return new Promise((resolve7, reject) => {
|
|
5737
6316
|
let body = "";
|
|
5738
6317
|
let bytes = 0;
|
|
5739
6318
|
req.on("data", (chunk) => {
|
|
@@ -5745,7 +6324,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
5745
6324
|
}
|
|
5746
6325
|
body += String(chunk);
|
|
5747
6326
|
});
|
|
5748
|
-
req.on("end", () =>
|
|
6327
|
+
req.on("end", () => resolve7(body));
|
|
5749
6328
|
req.on("error", reject);
|
|
5750
6329
|
});
|
|
5751
6330
|
}
|
|
@@ -5911,7 +6490,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
5911
6490
|
}
|
|
5912
6491
|
|
|
5913
6492
|
// src/server/routes/chat-proxy.ts
|
|
5914
|
-
import { spawn as
|
|
6493
|
+
import { spawn as spawn6 } from "child_process";
|
|
5915
6494
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
5916
6495
|
import * as readline2 from "readline";
|
|
5917
6496
|
import { z as z6 } from "zod";
|
|
@@ -5997,7 +6576,7 @@ async function handleChatRequest(req, res, command) {
|
|
|
5997
6576
|
});
|
|
5998
6577
|
emit(res, { type: "session", sessionId });
|
|
5999
6578
|
const args = buildArgs(parsed.prompt, sessionId, isFirstTurn, parsed.system);
|
|
6000
|
-
child =
|
|
6579
|
+
child = spawn6(command, args, { env: buildChildEnv(), stdio: "pipe" });
|
|
6001
6580
|
child.stdin?.end();
|
|
6002
6581
|
let clientDisconnected = false;
|
|
6003
6582
|
res.on("close", () => {
|
|
@@ -6881,36 +7460,577 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
6881
7460
|
return false;
|
|
6882
7461
|
}
|
|
6883
7462
|
|
|
6884
|
-
// src/server/routes/
|
|
6885
|
-
import * as fs11 from "fs/promises";
|
|
6886
|
-
import * as path11 from "path";
|
|
7463
|
+
// src/server/routes/v1/proposals.ts
|
|
6887
7464
|
import { z as z13 } from "zod";
|
|
6888
|
-
|
|
6889
|
-
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
7465
|
+
import {
|
|
7466
|
+
getProposal as getProposal3,
|
|
7467
|
+
listProposals,
|
|
7468
|
+
updateProposal as updateProposal3,
|
|
7469
|
+
ProposalNotFoundError as ProposalNotFoundError3
|
|
7470
|
+
} from "@harness-engineering/core";
|
|
7471
|
+
import {
|
|
7472
|
+
EditProposalInputSchema
|
|
7473
|
+
} from "@harness-engineering/types";
|
|
7474
|
+
|
|
7475
|
+
// src/proposals/gate.ts
|
|
7476
|
+
import { parse as parseYaml } from "yaml";
|
|
7477
|
+
import {
|
|
7478
|
+
getProposal,
|
|
7479
|
+
updateProposal,
|
|
7480
|
+
ProposalNotFoundError
|
|
7481
|
+
} from "@harness-engineering/core";
|
|
7482
|
+
var GateRunError = class extends Error {
|
|
7483
|
+
constructor(message) {
|
|
7484
|
+
super(message);
|
|
7485
|
+
this.name = "GateRunError";
|
|
7486
|
+
}
|
|
7487
|
+
};
|
|
7488
|
+
var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
7489
|
+
function checkSkillYaml(yaml) {
|
|
7490
|
+
const findings = [];
|
|
7491
|
+
let doc;
|
|
7492
|
+
try {
|
|
7493
|
+
doc = parseYaml(yaml);
|
|
7494
|
+
} catch (err) {
|
|
7495
|
+
findings.push({
|
|
7496
|
+
severity: "error",
|
|
7497
|
+
title: "skill.yaml does not parse",
|
|
7498
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
7499
|
+
});
|
|
7500
|
+
return findings;
|
|
7501
|
+
}
|
|
7502
|
+
if (!doc || typeof doc !== "object") {
|
|
7503
|
+
findings.push({
|
|
7504
|
+
severity: "error",
|
|
7505
|
+
title: "skill.yaml top-level is not a mapping",
|
|
7506
|
+
detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
|
|
7507
|
+
});
|
|
7508
|
+
return findings;
|
|
7509
|
+
}
|
|
7510
|
+
const obj = doc;
|
|
7511
|
+
if (typeof obj["name"] !== "string") {
|
|
7512
|
+
findings.push({
|
|
7513
|
+
severity: "error",
|
|
7514
|
+
title: "skill.yaml missing `name`",
|
|
7515
|
+
detail: "Every skill must declare its kebab-case name."
|
|
7516
|
+
});
|
|
7517
|
+
}
|
|
7518
|
+
if (typeof obj["version"] !== "string") {
|
|
7519
|
+
findings.push({
|
|
7520
|
+
severity: "error",
|
|
7521
|
+
title: "skill.yaml missing `version`",
|
|
7522
|
+
detail: "Every skill must declare a semver version string."
|
|
7523
|
+
});
|
|
7524
|
+
}
|
|
7525
|
+
if (typeof obj["description"] !== "string") {
|
|
7526
|
+
findings.push({
|
|
7527
|
+
severity: "warning",
|
|
7528
|
+
title: "skill.yaml missing `description`",
|
|
7529
|
+
detail: "Description is strongly recommended for discoverability."
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7532
|
+
return findings;
|
|
7533
|
+
}
|
|
7534
|
+
function checkSkillMd(md) {
|
|
7535
|
+
const findings = [];
|
|
7536
|
+
if (md.trim().length < 40) {
|
|
7537
|
+
findings.push({
|
|
7538
|
+
severity: "error",
|
|
7539
|
+
title: "SKILL.md is too short",
|
|
7540
|
+
detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
|
|
7541
|
+
});
|
|
7542
|
+
}
|
|
7543
|
+
if (!/^#\s+\S/m.test(md)) {
|
|
7544
|
+
findings.push({
|
|
7545
|
+
severity: "warning",
|
|
7546
|
+
title: "SKILL.md has no top-level heading",
|
|
7547
|
+
detail: "Convention: open SKILL.md with `# <Skill Name>`."
|
|
7548
|
+
});
|
|
7549
|
+
}
|
|
7550
|
+
return findings;
|
|
7551
|
+
}
|
|
7552
|
+
function checkName(name) {
|
|
7553
|
+
if (SKILL_NAME_RE.test(name)) return [];
|
|
7554
|
+
return [
|
|
7555
|
+
{
|
|
7556
|
+
severity: "error",
|
|
7557
|
+
title: "skill name violates the kebab-case rule",
|
|
7558
|
+
detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
|
|
7559
|
+
}
|
|
7560
|
+
];
|
|
7561
|
+
}
|
|
7562
|
+
function checkDiff(diff) {
|
|
7563
|
+
const findings = [];
|
|
7564
|
+
if (!diff.includes("---") || !diff.includes("+++")) {
|
|
7565
|
+
findings.push({
|
|
7566
|
+
severity: "error",
|
|
7567
|
+
title: "Refinement diff is not in unified-diff format",
|
|
7568
|
+
detail: "Diffs must include both `---` and `+++` headers."
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
if (!/^@@\s/m.test(diff)) {
|
|
7572
|
+
findings.push({
|
|
7573
|
+
severity: "warning",
|
|
7574
|
+
title: "Refinement diff has no hunk marker",
|
|
7575
|
+
detail: "A unified diff typically contains at least one `@@` line."
|
|
7576
|
+
});
|
|
7577
|
+
}
|
|
7578
|
+
return findings;
|
|
7579
|
+
}
|
|
7580
|
+
function deriveFindings(proposal) {
|
|
7581
|
+
const findings = [];
|
|
7582
|
+
findings.push(...checkName(proposal.content.name));
|
|
7583
|
+
if (proposal.kind === "new-skill") {
|
|
7584
|
+
findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
|
|
7585
|
+
findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
|
|
7586
|
+
} else if (proposal.kind === "refinement") {
|
|
7587
|
+
findings.push(...checkDiff(proposal.content.diff ?? ""));
|
|
7588
|
+
}
|
|
7589
|
+
return findings;
|
|
7590
|
+
}
|
|
7591
|
+
async function runGate(projectPath, proposalId) {
|
|
7592
|
+
const proposal = await getProposal(projectPath, proposalId);
|
|
7593
|
+
if (!proposal) throw new ProposalNotFoundError(proposalId);
|
|
7594
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7595
|
+
throw new GateRunError(
|
|
7596
|
+
`proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
|
|
7597
|
+
);
|
|
7598
|
+
}
|
|
7599
|
+
const findings = deriveFindings(proposal);
|
|
7600
|
+
const runAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7601
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
7602
|
+
const nextStatus = hasError ? "gate-failed" : "gate-running";
|
|
7603
|
+
const updated = await updateProposal(projectPath, proposalId, {
|
|
7604
|
+
status: nextStatus,
|
|
7605
|
+
gate: { lastRunAt: runAt, findings }
|
|
7606
|
+
});
|
|
7607
|
+
return {
|
|
7608
|
+
proposalId: updated.id,
|
|
7609
|
+
status: updated.status,
|
|
7610
|
+
findings,
|
|
7611
|
+
runAt
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7615
|
+
// src/proposals/promote.ts
|
|
7616
|
+
import * as fs11 from "fs";
|
|
7617
|
+
import * as path11 from "path";
|
|
7618
|
+
import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
|
|
7619
|
+
import {
|
|
7620
|
+
getProposal as getProposal2,
|
|
7621
|
+
updateProposal as updateProposal2,
|
|
7622
|
+
ProposalNotFoundError as ProposalNotFoundError2
|
|
7623
|
+
} from "@harness-engineering/core";
|
|
7624
|
+
var GateNotReadyError = class extends Error {
|
|
7625
|
+
constructor(message) {
|
|
7626
|
+
super(message);
|
|
7627
|
+
this.name = "GateNotReadyError";
|
|
7628
|
+
}
|
|
7629
|
+
};
|
|
7630
|
+
var PromotionError = class extends Error {
|
|
7631
|
+
constructor(message) {
|
|
7632
|
+
super(message);
|
|
7633
|
+
this.name = "PromotionError";
|
|
7634
|
+
}
|
|
7635
|
+
};
|
|
7636
|
+
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
7637
|
+
function skillDir(projectPath, name) {
|
|
7638
|
+
return path11.join(projectPath, "agents", "skills", "claude-code", name);
|
|
7639
|
+
}
|
|
7640
|
+
function readIfExists(p) {
|
|
7641
|
+
try {
|
|
7642
|
+
return fs11.readFileSync(p, "utf-8");
|
|
7643
|
+
} catch {
|
|
7644
|
+
return null;
|
|
7645
|
+
}
|
|
7646
|
+
}
|
|
7647
|
+
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
7648
|
+
let doc;
|
|
7649
|
+
try {
|
|
7650
|
+
doc = parseYaml2(yamlText);
|
|
7651
|
+
} catch (err) {
|
|
7652
|
+
throw new PromotionError(
|
|
7653
|
+
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
7654
|
+
);
|
|
7655
|
+
}
|
|
7656
|
+
if (!doc || typeof doc !== "object") {
|
|
7657
|
+
throw new PromotionError("skill.yaml top-level is not a mapping");
|
|
7658
|
+
}
|
|
7659
|
+
const obj = doc;
|
|
7660
|
+
obj["provenance"] = "agent-proposed";
|
|
7661
|
+
obj["originatingProposalId"] = proposalId;
|
|
7662
|
+
return stringifyYaml(obj);
|
|
7663
|
+
}
|
|
7664
|
+
function assertGateReady(proposal) {
|
|
7665
|
+
if (proposal.status !== "gate-running") {
|
|
7666
|
+
throw new GateNotReadyError(
|
|
7667
|
+
`proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
|
|
7668
|
+
);
|
|
7669
|
+
}
|
|
7670
|
+
const findings = proposal.gate?.findings ?? [];
|
|
7671
|
+
if (findings.some((f) => f.severity === "error")) {
|
|
7672
|
+
throw new GateNotReadyError(
|
|
7673
|
+
`proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
|
|
7674
|
+
);
|
|
7675
|
+
}
|
|
7676
|
+
if (!proposal.gate?.lastRunAt) {
|
|
7677
|
+
throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
|
|
7678
|
+
}
|
|
7679
|
+
const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
|
|
7680
|
+
if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
|
|
7681
|
+
throw new GateNotReadyError(
|
|
7682
|
+
`proposal ${proposal.id} gate run is older than 24h; re-run before approving`
|
|
7683
|
+
);
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
async function promoteNewSkill(projectPath, proposal) {
|
|
7687
|
+
const target = skillDir(projectPath, proposal.content.name);
|
|
7688
|
+
if (fs11.existsSync(target)) {
|
|
7689
|
+
throw new PromotionError(
|
|
7690
|
+
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
7691
|
+
);
|
|
7692
|
+
}
|
|
7693
|
+
fs11.mkdirSync(target, { recursive: true });
|
|
7694
|
+
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
7695
|
+
fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
|
|
7696
|
+
fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
7697
|
+
return { skillPath: target };
|
|
7698
|
+
}
|
|
7699
|
+
async function promoteRefinement(projectPath, proposal) {
|
|
7700
|
+
if (!proposal.targetSkill) {
|
|
7701
|
+
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
7702
|
+
}
|
|
7703
|
+
const target = skillDir(projectPath, proposal.targetSkill);
|
|
7704
|
+
if (!fs11.existsSync(target)) {
|
|
7705
|
+
throw new PromotionError(
|
|
7706
|
+
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
7707
|
+
);
|
|
7708
|
+
}
|
|
7709
|
+
const yamlPath = path11.join(target, "skill.yaml");
|
|
7710
|
+
const before = readIfExists(yamlPath) ?? "";
|
|
7711
|
+
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
7712
|
+
if (after === before) {
|
|
7713
|
+
throw new PromotionError(
|
|
7714
|
+
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
7715
|
+
);
|
|
7716
|
+
}
|
|
7717
|
+
fs11.writeFileSync(yamlPath, after);
|
|
7718
|
+
return { skillPath: target };
|
|
7719
|
+
}
|
|
7720
|
+
async function promote(projectPath, proposalId, decidedBy) {
|
|
7721
|
+
const proposal = await getProposal2(projectPath, proposalId);
|
|
7722
|
+
if (!proposal) throw new ProposalNotFoundError2(proposalId);
|
|
7723
|
+
assertGateReady(proposal);
|
|
7724
|
+
const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
|
|
7725
|
+
await updateProposal2(projectPath, proposalId, {
|
|
7726
|
+
status: "approved",
|
|
7727
|
+
decision: {
|
|
7728
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7729
|
+
decidedBy,
|
|
7730
|
+
action: "approved"
|
|
7731
|
+
}
|
|
7732
|
+
});
|
|
7733
|
+
return {
|
|
7734
|
+
proposalId,
|
|
7735
|
+
skillPath: out.skillPath,
|
|
7736
|
+
provenance: "agent-proposed"
|
|
7737
|
+
};
|
|
7738
|
+
}
|
|
7739
|
+
|
|
7740
|
+
// src/proposals/events.ts
|
|
7741
|
+
function emit3(bus, topic, data) {
|
|
7742
|
+
bus.emit(topic, data);
|
|
7743
|
+
}
|
|
7744
|
+
function emitProposalCreated(bus, proposal) {
|
|
7745
|
+
const data = {
|
|
7746
|
+
id: proposal.id,
|
|
7747
|
+
kind: proposal.kind,
|
|
7748
|
+
name: proposal.content.name,
|
|
7749
|
+
proposedBy: proposal.proposedBy,
|
|
7750
|
+
justification: proposal.source.justification
|
|
7751
|
+
};
|
|
7752
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
7753
|
+
emit3(bus, "proposal.created", data);
|
|
7754
|
+
}
|
|
7755
|
+
function emitProposalApproved(bus, proposal) {
|
|
7756
|
+
const data = {
|
|
7757
|
+
id: proposal.id,
|
|
7758
|
+
kind: proposal.kind,
|
|
7759
|
+
name: proposal.content.name,
|
|
7760
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
|
|
7761
|
+
};
|
|
7762
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
7763
|
+
emit3(bus, "proposal.approved", data);
|
|
7764
|
+
}
|
|
7765
|
+
function emitProposalRejected(bus, proposal) {
|
|
7766
|
+
const data = {
|
|
7767
|
+
id: proposal.id,
|
|
7768
|
+
kind: proposal.kind,
|
|
7769
|
+
name: proposal.content.name,
|
|
7770
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
|
|
7771
|
+
reason: proposal.decision?.reason ?? "(no reason given)"
|
|
7772
|
+
};
|
|
7773
|
+
emit3(bus, "proposal.rejected", data);
|
|
7774
|
+
}
|
|
7775
|
+
|
|
7776
|
+
// src/server/routes/v1/proposals.ts
|
|
7777
|
+
var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
|
|
7778
|
+
var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
|
|
7779
|
+
var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
|
|
7780
|
+
var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
|
|
7781
|
+
var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
|
|
7782
|
+
var ProposalStatusValues = [
|
|
7783
|
+
"open",
|
|
7784
|
+
"gate-running",
|
|
7785
|
+
"gate-failed",
|
|
7786
|
+
"approved",
|
|
7787
|
+
"rejected"
|
|
7788
|
+
];
|
|
7789
|
+
var RejectBody = z13.object({
|
|
7790
|
+
reason: z13.string().min(1).max(280)
|
|
7791
|
+
});
|
|
7792
|
+
function sendJSON8(res, status, body) {
|
|
7793
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7794
|
+
res.end(JSON.stringify(body));
|
|
7795
|
+
}
|
|
7796
|
+
function getDecidedBy(req, deps) {
|
|
7797
|
+
if (deps.decidedByResolver) return deps.decidedByResolver(req);
|
|
7798
|
+
const token = req._authToken;
|
|
7799
|
+
return token?.id ?? "unknown";
|
|
7800
|
+
}
|
|
7801
|
+
function parseStatusFromQuery(url) {
|
|
7802
|
+
const queryIdx = url.indexOf("?");
|
|
7803
|
+
if (queryIdx === -1) return void 0;
|
|
7804
|
+
const params = new URLSearchParams(url.slice(queryIdx + 1));
|
|
7805
|
+
const raw = params.get("status");
|
|
7806
|
+
if (!raw) return void 0;
|
|
7807
|
+
if (raw === "all") return "all";
|
|
7808
|
+
if (ProposalStatusValues.includes(raw)) return raw;
|
|
7809
|
+
return void 0;
|
|
7810
|
+
}
|
|
7811
|
+
async function handleList(req, res, deps) {
|
|
7812
|
+
const url = req.url ?? "";
|
|
7813
|
+
const status = parseStatusFromQuery(url);
|
|
7814
|
+
const proposals = await listProposals(deps.projectPath, status ? { status } : {});
|
|
7815
|
+
sendJSON8(res, 200, proposals);
|
|
7816
|
+
}
|
|
7817
|
+
async function handleGet(res, deps, id) {
|
|
7818
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
7819
|
+
if (!proposal) {
|
|
7820
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7821
|
+
return;
|
|
7822
|
+
}
|
|
7823
|
+
sendJSON8(res, 200, proposal);
|
|
7824
|
+
}
|
|
7825
|
+
async function handleRunGate(res, deps, id) {
|
|
7826
|
+
try {
|
|
7827
|
+
const result = await runGate(deps.projectPath, id);
|
|
7828
|
+
sendJSON8(res, 200, result);
|
|
7829
|
+
} catch (err) {
|
|
7830
|
+
if (err instanceof ProposalNotFoundError3) {
|
|
7831
|
+
sendJSON8(res, 404, { error: err.message });
|
|
7832
|
+
return;
|
|
7833
|
+
}
|
|
7834
|
+
if (err instanceof GateRunError) {
|
|
7835
|
+
sendJSON8(res, 409, { error: err.message });
|
|
7836
|
+
return;
|
|
7837
|
+
}
|
|
7838
|
+
sendJSON8(res, 500, {
|
|
7839
|
+
error: "gate run failed",
|
|
7840
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7841
|
+
});
|
|
7842
|
+
}
|
|
7843
|
+
}
|
|
7844
|
+
async function handleApprove(req, res, deps, id) {
|
|
7845
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
7846
|
+
try {
|
|
7847
|
+
const result = await promote(deps.projectPath, id, decidedBy);
|
|
7848
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
7849
|
+
if (proposal) emitProposalApproved(deps.bus, proposal);
|
|
7850
|
+
sendJSON8(res, 200, { promotion: result, proposal });
|
|
7851
|
+
} catch (err) {
|
|
7852
|
+
if (err instanceof ProposalNotFoundError3) {
|
|
7853
|
+
sendJSON8(res, 404, { error: err.message });
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
if (err instanceof GateNotReadyError) {
|
|
7857
|
+
sendJSON8(res, 409, { error: err.message });
|
|
7858
|
+
return;
|
|
7859
|
+
}
|
|
7860
|
+
if (err instanceof PromotionError) {
|
|
7861
|
+
sendJSON8(res, 422, { error: err.message });
|
|
7862
|
+
return;
|
|
7863
|
+
}
|
|
7864
|
+
sendJSON8(res, 500, {
|
|
7865
|
+
error: "approve failed",
|
|
7866
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7867
|
+
});
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
async function handleReject(req, res, deps, id) {
|
|
7871
|
+
let raw;
|
|
7872
|
+
try {
|
|
7873
|
+
raw = await readBody(req);
|
|
7874
|
+
} catch (err) {
|
|
7875
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
7876
|
+
return;
|
|
7877
|
+
}
|
|
7878
|
+
let json;
|
|
7879
|
+
try {
|
|
7880
|
+
json = raw.length > 0 ? JSON.parse(raw) : {};
|
|
7881
|
+
} catch {
|
|
7882
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
7883
|
+
return;
|
|
7884
|
+
}
|
|
7885
|
+
const parsed = RejectBody.safeParse(json);
|
|
7886
|
+
if (!parsed.success) {
|
|
7887
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
7888
|
+
return;
|
|
7889
|
+
}
|
|
7890
|
+
const proposal = await getProposal3(deps.projectPath, id);
|
|
7891
|
+
if (!proposal) {
|
|
7892
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7893
|
+
return;
|
|
7894
|
+
}
|
|
7895
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7896
|
+
sendJSON8(res, 409, {
|
|
7897
|
+
error: `proposal already ${proposal.status}; cannot reject`
|
|
7898
|
+
});
|
|
7899
|
+
return;
|
|
7900
|
+
}
|
|
7901
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
7902
|
+
const updated = await updateProposal3(deps.projectPath, id, {
|
|
7903
|
+
status: "rejected",
|
|
7904
|
+
decision: {
|
|
7905
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7906
|
+
decidedBy,
|
|
7907
|
+
action: "rejected",
|
|
7908
|
+
reason: parsed.data.reason
|
|
7909
|
+
}
|
|
7910
|
+
});
|
|
7911
|
+
emitProposalRejected(deps.bus, updated);
|
|
7912
|
+
sendJSON8(res, 200, updated);
|
|
7913
|
+
}
|
|
7914
|
+
async function handleEdit(req, res, deps, id) {
|
|
7915
|
+
let raw;
|
|
7916
|
+
try {
|
|
7917
|
+
raw = await readBody(req);
|
|
7918
|
+
} catch (err) {
|
|
7919
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
7920
|
+
return;
|
|
7921
|
+
}
|
|
7922
|
+
let json;
|
|
7923
|
+
try {
|
|
7924
|
+
json = JSON.parse(raw);
|
|
7925
|
+
} catch {
|
|
7926
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
7927
|
+
return;
|
|
7928
|
+
}
|
|
7929
|
+
const parsed = EditProposalInputSchema.safeParse(json);
|
|
7930
|
+
if (!parsed.success) {
|
|
7931
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
7932
|
+
return;
|
|
7933
|
+
}
|
|
7934
|
+
const existing = await getProposal3(deps.projectPath, id);
|
|
7935
|
+
if (!existing) {
|
|
7936
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7937
|
+
return;
|
|
7938
|
+
}
|
|
7939
|
+
if (existing.status === "approved" || existing.status === "rejected") {
|
|
7940
|
+
sendJSON8(res, 409, {
|
|
7941
|
+
error: `proposal already ${existing.status}; cannot edit`
|
|
7942
|
+
});
|
|
7943
|
+
return;
|
|
7944
|
+
}
|
|
7945
|
+
const mergedContent = {
|
|
7946
|
+
...existing.content,
|
|
7947
|
+
...parsed.data.content,
|
|
7948
|
+
name: parsed.data.content.name ?? existing.content.name,
|
|
7949
|
+
description: parsed.data.content.description ?? existing.content.description
|
|
7950
|
+
};
|
|
7951
|
+
try {
|
|
7952
|
+
const updated = await updateProposal3(deps.projectPath, id, {
|
|
7953
|
+
content: mergedContent,
|
|
7954
|
+
status: "open",
|
|
7955
|
+
gate: void 0
|
|
7956
|
+
});
|
|
7957
|
+
sendJSON8(res, 200, updated);
|
|
7958
|
+
} catch (err) {
|
|
7959
|
+
sendJSON8(res, 422, {
|
|
7960
|
+
error: "edit failed",
|
|
7961
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7962
|
+
});
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
function handleV1ProposalsRoute(req, res, deps) {
|
|
7966
|
+
const url = req.url ?? "";
|
|
7967
|
+
const method = req.method ?? "GET";
|
|
7968
|
+
if (method === "GET" && LIST_RE.test(url)) {
|
|
7969
|
+
void handleList(req, res, deps);
|
|
7970
|
+
return true;
|
|
7971
|
+
}
|
|
7972
|
+
const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
|
|
7973
|
+
if (runGateMatch) {
|
|
7974
|
+
void handleRunGate(res, deps, runGateMatch[1]);
|
|
7975
|
+
return true;
|
|
7976
|
+
}
|
|
7977
|
+
const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
|
|
7978
|
+
if (approveMatch) {
|
|
7979
|
+
void handleApprove(req, res, deps, approveMatch[1]);
|
|
7980
|
+
return true;
|
|
7981
|
+
}
|
|
7982
|
+
const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
|
|
7983
|
+
if (rejectMatch) {
|
|
7984
|
+
void handleReject(req, res, deps, rejectMatch[1]);
|
|
7985
|
+
return true;
|
|
7986
|
+
}
|
|
7987
|
+
if (method === "PATCH") {
|
|
7988
|
+
const m = SINGLE_RE.exec(url);
|
|
7989
|
+
if (m) {
|
|
7990
|
+
void handleEdit(req, res, deps, m[1]);
|
|
7991
|
+
return true;
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
if (method === "GET") {
|
|
7995
|
+
const m = SINGLE_RE.exec(url);
|
|
7996
|
+
if (m) {
|
|
7997
|
+
void handleGet(res, deps, m[1]);
|
|
7998
|
+
return true;
|
|
7999
|
+
}
|
|
8000
|
+
}
|
|
8001
|
+
return false;
|
|
8002
|
+
}
|
|
8003
|
+
|
|
8004
|
+
// src/server/routes/sessions.ts
|
|
8005
|
+
import * as fs12 from "fs/promises";
|
|
8006
|
+
import * as path12 from "path";
|
|
8007
|
+
import { z as z14 } from "zod";
|
|
8008
|
+
var SessionCreateSchema = z14.object({
|
|
8009
|
+
sessionId: z14.string().min(1)
|
|
8010
|
+
}).passthrough();
|
|
8011
|
+
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8012
|
+
function isSafeId(id) {
|
|
8013
|
+
return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
|
|
8014
|
+
}
|
|
8015
|
+
function jsonResponse(res, status, data) {
|
|
8016
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
8017
|
+
res.end(JSON.stringify(data));
|
|
8018
|
+
}
|
|
8019
|
+
function extractSessionId(url) {
|
|
8020
|
+
const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
|
|
8021
|
+
const id = segments.pop();
|
|
8022
|
+
return id && id !== "sessions" ? id : null;
|
|
8023
|
+
}
|
|
8024
|
+
async function handleList2(res, sessionsDir) {
|
|
8025
|
+
try {
|
|
8026
|
+
const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
|
|
8027
|
+
const sessions = [];
|
|
8028
|
+
for (const entry of entries) {
|
|
8029
|
+
if (!entry.isDirectory()) continue;
|
|
8030
|
+
try {
|
|
8031
|
+
const content = await fs12.readFile(
|
|
8032
|
+
path12.join(sessionsDir, entry.name, "session.json"),
|
|
8033
|
+
"utf-8"
|
|
6914
8034
|
);
|
|
6915
8035
|
sessions.push(JSON.parse(content));
|
|
6916
8036
|
} catch {
|
|
@@ -6928,13 +8048,13 @@ async function handleList(res, sessionsDir) {
|
|
|
6928
8048
|
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
6929
8049
|
}
|
|
6930
8050
|
}
|
|
6931
|
-
async function
|
|
8051
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
6932
8052
|
if (!isSafeId(id)) {
|
|
6933
8053
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6934
8054
|
return;
|
|
6935
8055
|
}
|
|
6936
8056
|
try {
|
|
6937
|
-
const content = await
|
|
8057
|
+
const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
|
|
6938
8058
|
jsonResponse(res, 200, JSON.parse(content));
|
|
6939
8059
|
} catch (err) {
|
|
6940
8060
|
if (err.code === "ENOENT") {
|
|
@@ -6957,9 +8077,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
6957
8077
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6958
8078
|
return;
|
|
6959
8079
|
}
|
|
6960
|
-
const sessionDir =
|
|
6961
|
-
await
|
|
6962
|
-
await
|
|
8080
|
+
const sessionDir = path12.join(sessionsDir, session.sessionId);
|
|
8081
|
+
await fs12.mkdir(sessionDir, { recursive: true });
|
|
8082
|
+
await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
6963
8083
|
jsonResponse(res, 200, { ok: true });
|
|
6964
8084
|
} catch {
|
|
6965
8085
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -6973,10 +8093,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
6973
8093
|
return;
|
|
6974
8094
|
}
|
|
6975
8095
|
const body = await readBody(req);
|
|
6976
|
-
const updates =
|
|
6977
|
-
const sessionFilePath =
|
|
6978
|
-
const current = JSON.parse(await
|
|
6979
|
-
await
|
|
8096
|
+
const updates = z14.record(z14.unknown()).parse(JSON.parse(body));
|
|
8097
|
+
const sessionFilePath = path12.join(sessionsDir, id, "session.json");
|
|
8098
|
+
const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
|
|
8099
|
+
await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
6980
8100
|
jsonResponse(res, 200, { ok: true });
|
|
6981
8101
|
} catch {
|
|
6982
8102
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -6989,7 +8109,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
6989
8109
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
6990
8110
|
return;
|
|
6991
8111
|
}
|
|
6992
|
-
await
|
|
8112
|
+
await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
|
|
6993
8113
|
jsonResponse(res, 200, { ok: true });
|
|
6994
8114
|
} catch {
|
|
6995
8115
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7002,8 +8122,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7002
8122
|
switch (method) {
|
|
7003
8123
|
case "GET": {
|
|
7004
8124
|
const id = extractSessionId(url);
|
|
7005
|
-
if (id) void
|
|
7006
|
-
else void
|
|
8125
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8126
|
+
else void handleList2(res, sessionsDir);
|
|
7007
8127
|
return true;
|
|
7008
8128
|
}
|
|
7009
8129
|
case "POST":
|
|
@@ -7093,20 +8213,20 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7093
8213
|
}
|
|
7094
8214
|
|
|
7095
8215
|
// src/server/routes/auth.ts
|
|
7096
|
-
import { z as
|
|
8216
|
+
import { z as z15 } from "zod";
|
|
7097
8217
|
import {
|
|
7098
8218
|
TokenScopeSchema,
|
|
7099
8219
|
BridgeKindSchema,
|
|
7100
8220
|
AuthTokenPublicSchema
|
|
7101
8221
|
} from "@harness-engineering/types";
|
|
7102
|
-
var CreateBodySchema =
|
|
7103
|
-
name:
|
|
7104
|
-
scopes:
|
|
8222
|
+
var CreateBodySchema = z15.object({
|
|
8223
|
+
name: z15.string().min(1).max(100),
|
|
8224
|
+
scopes: z15.array(TokenScopeSchema).min(1),
|
|
7105
8225
|
bridgeKind: BridgeKindSchema.optional(),
|
|
7106
|
-
tenantId:
|
|
7107
|
-
expiresAt:
|
|
8226
|
+
tenantId: z15.string().optional(),
|
|
8227
|
+
expiresAt: z15.string().datetime().optional()
|
|
7108
8228
|
});
|
|
7109
|
-
function
|
|
8229
|
+
function sendJSON9(res, status, body) {
|
|
7110
8230
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7111
8231
|
res.end(JSON.stringify(body));
|
|
7112
8232
|
}
|
|
@@ -7116,19 +8236,19 @@ async function handlePost(req, res, store) {
|
|
|
7116
8236
|
raw = await readBody(req);
|
|
7117
8237
|
} catch (err) {
|
|
7118
8238
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7119
|
-
|
|
8239
|
+
sendJSON9(res, 413, { error: msg });
|
|
7120
8240
|
return;
|
|
7121
8241
|
}
|
|
7122
8242
|
let json;
|
|
7123
8243
|
try {
|
|
7124
8244
|
json = JSON.parse(raw);
|
|
7125
8245
|
} catch {
|
|
7126
|
-
|
|
8246
|
+
sendJSON9(res, 400, { error: "Invalid JSON body" });
|
|
7127
8247
|
return;
|
|
7128
8248
|
}
|
|
7129
8249
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7130
8250
|
if (!parsed.success) {
|
|
7131
|
-
|
|
8251
|
+
sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7132
8252
|
return;
|
|
7133
8253
|
}
|
|
7134
8254
|
try {
|
|
@@ -7141,37 +8261,37 @@ async function handlePost(req, res, store) {
|
|
|
7141
8261
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7142
8262
|
const result = await store.create(input);
|
|
7143
8263
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
7144
|
-
|
|
8264
|
+
sendJSON9(res, 200, {
|
|
7145
8265
|
...publicRecord,
|
|
7146
8266
|
token: result.token
|
|
7147
8267
|
});
|
|
7148
8268
|
} catch (err) {
|
|
7149
8269
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7150
8270
|
if (msg.includes("already exists")) {
|
|
7151
|
-
|
|
8271
|
+
sendJSON9(res, 409, { error: msg });
|
|
7152
8272
|
return;
|
|
7153
8273
|
}
|
|
7154
|
-
|
|
8274
|
+
sendJSON9(res, 500, { error: "Internal error creating token" });
|
|
7155
8275
|
}
|
|
7156
8276
|
}
|
|
7157
|
-
async function
|
|
8277
|
+
async function handleList3(res, store) {
|
|
7158
8278
|
try {
|
|
7159
8279
|
const list = await store.list();
|
|
7160
|
-
|
|
8280
|
+
sendJSON9(res, 200, list);
|
|
7161
8281
|
} catch {
|
|
7162
|
-
|
|
8282
|
+
sendJSON9(res, 500, { error: "Internal error listing tokens" });
|
|
7163
8283
|
}
|
|
7164
8284
|
}
|
|
7165
8285
|
async function handleDelete2(res, store, id) {
|
|
7166
8286
|
try {
|
|
7167
8287
|
const ok = await store.revoke(id);
|
|
7168
8288
|
if (!ok) {
|
|
7169
|
-
|
|
8289
|
+
sendJSON9(res, 404, { error: "Token not found" });
|
|
7170
8290
|
return;
|
|
7171
8291
|
}
|
|
7172
|
-
|
|
8292
|
+
sendJSON9(res, 200, { deleted: true });
|
|
7173
8293
|
} catch {
|
|
7174
|
-
|
|
8294
|
+
sendJSON9(res, 500, { error: "Internal error revoking token" });
|
|
7175
8295
|
}
|
|
7176
8296
|
}
|
|
7177
8297
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7185,7 +8305,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7185
8305
|
return true;
|
|
7186
8306
|
}
|
|
7187
8307
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7188
|
-
void
|
|
8308
|
+
void handleList3(res, store);
|
|
7189
8309
|
return true;
|
|
7190
8310
|
}
|
|
7191
8311
|
if (method === "DELETE") {
|
|
@@ -7196,12 +8316,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7196
8316
|
return true;
|
|
7197
8317
|
}
|
|
7198
8318
|
}
|
|
7199
|
-
|
|
8319
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
7200
8320
|
return true;
|
|
7201
8321
|
}
|
|
7202
8322
|
|
|
7203
8323
|
// src/server/routes/local-model.ts
|
|
7204
|
-
function
|
|
8324
|
+
function sendJSON10(res, status, body) {
|
|
7205
8325
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7206
8326
|
res.end(JSON.stringify(body));
|
|
7207
8327
|
}
|
|
@@ -7209,36 +8329,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7209
8329
|
const { method, url } = req;
|
|
7210
8330
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7211
8331
|
if (method !== "GET") {
|
|
7212
|
-
|
|
8332
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7213
8333
|
return true;
|
|
7214
8334
|
}
|
|
7215
8335
|
if (!getStatus) {
|
|
7216
|
-
|
|
8336
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7217
8337
|
return true;
|
|
7218
8338
|
}
|
|
7219
8339
|
const status = getStatus();
|
|
7220
8340
|
if (!status) {
|
|
7221
|
-
|
|
8341
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7222
8342
|
return true;
|
|
7223
8343
|
}
|
|
7224
|
-
|
|
8344
|
+
sendJSON10(res, 200, status);
|
|
7225
8345
|
return true;
|
|
7226
8346
|
}
|
|
7227
8347
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7228
8348
|
const { method, url } = req;
|
|
7229
8349
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7230
8350
|
if (method !== "GET") {
|
|
7231
|
-
|
|
8351
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7232
8352
|
return true;
|
|
7233
8353
|
}
|
|
7234
8354
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7235
|
-
|
|
8355
|
+
sendJSON10(res, 200, statuses);
|
|
7236
8356
|
return true;
|
|
7237
8357
|
}
|
|
7238
8358
|
|
|
7239
8359
|
// src/server/static.ts
|
|
7240
|
-
import * as
|
|
7241
|
-
import * as
|
|
8360
|
+
import * as fs13 from "fs";
|
|
8361
|
+
import * as path13 from "path";
|
|
7242
8362
|
var MIME_TYPES = {
|
|
7243
8363
|
".html": "text/html; charset=utf-8",
|
|
7244
8364
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7258,29 +8378,29 @@ var MIME_TYPES = {
|
|
|
7258
8378
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7259
8379
|
const { method, url } = req;
|
|
7260
8380
|
if (method !== "GET") return false;
|
|
7261
|
-
const apiPrefix =
|
|
7262
|
-
const wsPath =
|
|
8381
|
+
const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
|
|
8382
|
+
const wsPath = path13.posix.join(path13.posix.sep, "ws");
|
|
7263
8383
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7264
8384
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7265
|
-
const requestedPath =
|
|
7266
|
-
const resolved =
|
|
7267
|
-
if (!resolved.startsWith(
|
|
7268
|
-
return serveFile(
|
|
8385
|
+
const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8386
|
+
const resolved = path13.resolve(requestedPath);
|
|
8387
|
+
if (!resolved.startsWith(path13.resolve(dashboardDir))) {
|
|
8388
|
+
return serveFile(path13.join(dashboardDir, "index.html"), res);
|
|
7269
8389
|
}
|
|
7270
|
-
if (
|
|
8390
|
+
if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
|
|
7271
8391
|
return serveFile(resolved, res);
|
|
7272
8392
|
}
|
|
7273
|
-
const indexPath =
|
|
7274
|
-
if (
|
|
8393
|
+
const indexPath = path13.join(dashboardDir, "index.html");
|
|
8394
|
+
if (fs13.existsSync(indexPath)) {
|
|
7275
8395
|
return serveFile(indexPath, res);
|
|
7276
8396
|
}
|
|
7277
8397
|
return false;
|
|
7278
8398
|
}
|
|
7279
8399
|
function serveFile(filePath, res) {
|
|
7280
|
-
const ext =
|
|
8400
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
7281
8401
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7282
8402
|
try {
|
|
7283
|
-
const content =
|
|
8403
|
+
const content = fs13.readFileSync(filePath);
|
|
7284
8404
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7285
8405
|
res.end(content);
|
|
7286
8406
|
return true;
|
|
@@ -7290,8 +8410,8 @@ function serveFile(filePath, res) {
|
|
|
7290
8410
|
}
|
|
7291
8411
|
|
|
7292
8412
|
// src/server/plan-watcher.ts
|
|
7293
|
-
import * as
|
|
7294
|
-
import * as
|
|
8413
|
+
import * as fs14 from "fs";
|
|
8414
|
+
import * as path14 from "path";
|
|
7295
8415
|
var PlanWatcher = class {
|
|
7296
8416
|
plansDir;
|
|
7297
8417
|
queue;
|
|
@@ -7305,11 +8425,11 @@ var PlanWatcher = class {
|
|
|
7305
8425
|
* Creates the directory if it does not exist.
|
|
7306
8426
|
*/
|
|
7307
8427
|
start() {
|
|
7308
|
-
|
|
7309
|
-
this.watcher =
|
|
8428
|
+
fs14.mkdirSync(this.plansDir, { recursive: true });
|
|
8429
|
+
this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
|
|
7310
8430
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7311
|
-
const filePath =
|
|
7312
|
-
if (
|
|
8431
|
+
const filePath = path14.join(this.plansDir, filename);
|
|
8432
|
+
if (fs14.existsSync(filePath)) {
|
|
7313
8433
|
void this.handleNewPlan(filename);
|
|
7314
8434
|
}
|
|
7315
8435
|
}
|
|
@@ -7362,8 +8482,8 @@ function parseToken(raw) {
|
|
|
7362
8482
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7363
8483
|
}
|
|
7364
8484
|
var TokenStore = class {
|
|
7365
|
-
constructor(
|
|
7366
|
-
this.path =
|
|
8485
|
+
constructor(path22) {
|
|
8486
|
+
this.path = path22;
|
|
7367
8487
|
}
|
|
7368
8488
|
path;
|
|
7369
8489
|
cache = null;
|
|
@@ -7470,8 +8590,8 @@ import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
|
7470
8590
|
import { dirname as dirname5 } from "path";
|
|
7471
8591
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
7472
8592
|
var AuditLogger = class {
|
|
7473
|
-
constructor(
|
|
7474
|
-
this.path =
|
|
8593
|
+
constructor(path22, opts = {}) {
|
|
8594
|
+
this.path = path22;
|
|
7475
8595
|
this.opts = opts;
|
|
7476
8596
|
}
|
|
7477
8597
|
path;
|
|
@@ -7555,6 +8675,43 @@ var V1_BRIDGE_ROUTES = [
|
|
|
7555
8675
|
scope: "subscribe-webhook",
|
|
7556
8676
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
7557
8677
|
},
|
|
8678
|
+
// Hermes Phase 4 — skill proposal review queue.
|
|
8679
|
+
{
|
|
8680
|
+
method: "GET",
|
|
8681
|
+
pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
|
|
8682
|
+
scope: "read-status",
|
|
8683
|
+
description: "List skill proposals (open + decided)."
|
|
8684
|
+
},
|
|
8685
|
+
{
|
|
8686
|
+
method: "GET",
|
|
8687
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
8688
|
+
scope: "read-status",
|
|
8689
|
+
description: "Get a single skill proposal."
|
|
8690
|
+
},
|
|
8691
|
+
{
|
|
8692
|
+
method: "POST",
|
|
8693
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
|
|
8694
|
+
scope: "manage-proposals",
|
|
8695
|
+
description: "Run the soundness-review gate against a proposal."
|
|
8696
|
+
},
|
|
8697
|
+
{
|
|
8698
|
+
method: "POST",
|
|
8699
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
|
|
8700
|
+
scope: "manage-proposals",
|
|
8701
|
+
description: "Approve a proposal \u2014 promotes the skill into the catalog."
|
|
8702
|
+
},
|
|
8703
|
+
{
|
|
8704
|
+
method: "POST",
|
|
8705
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
|
|
8706
|
+
scope: "manage-proposals",
|
|
8707
|
+
description: "Reject a proposal with a one-line reason."
|
|
8708
|
+
},
|
|
8709
|
+
{
|
|
8710
|
+
method: "PATCH",
|
|
8711
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
8712
|
+
scope: "manage-proposals",
|
|
8713
|
+
description: "Edit proposal content (resets gate to not-run)."
|
|
8714
|
+
},
|
|
7558
8715
|
// ── Phase 5 bridge primitives ──
|
|
7559
8716
|
{
|
|
7560
8717
|
method: "GET",
|
|
@@ -7566,9 +8723,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
7566
8723
|
function isV1Bridge(method, url) {
|
|
7567
8724
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
7568
8725
|
}
|
|
7569
|
-
function requiredBridgeScope(method,
|
|
8726
|
+
function requiredBridgeScope(method, path22) {
|
|
7570
8727
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
7571
|
-
if (r.method === method && r.pattern.test(
|
|
8728
|
+
if (r.method === method && r.pattern.test(path22)) return r.scope;
|
|
7572
8729
|
}
|
|
7573
8730
|
return null;
|
|
7574
8731
|
}
|
|
@@ -7578,24 +8735,24 @@ function hasScope(held, required) {
|
|
|
7578
8735
|
if (held.includes("admin")) return true;
|
|
7579
8736
|
return held.includes(required);
|
|
7580
8737
|
}
|
|
7581
|
-
function requiredScopeForRoute(method,
|
|
7582
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
8738
|
+
function requiredScopeForRoute(method, path22) {
|
|
8739
|
+
const bridgeScope = requiredBridgeScope(method, path22);
|
|
7583
8740
|
if (bridgeScope) return bridgeScope;
|
|
7584
|
-
if (
|
|
7585
|
-
if (
|
|
7586
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
7587
|
-
if ((
|
|
7588
|
-
if (
|
|
7589
|
-
if (
|
|
7590
|
-
if (
|
|
7591
|
-
if (
|
|
7592
|
-
if (
|
|
7593
|
-
if (
|
|
8741
|
+
if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
8742
|
+
if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
8743
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
|
|
8744
|
+
if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
|
|
8745
|
+
if (path22.startsWith("/api/interactions")) return "resolve-interaction";
|
|
8746
|
+
if (path22.startsWith("/api/plans")) return "read-status";
|
|
8747
|
+
if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
|
|
8748
|
+
if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
8749
|
+
if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
8750
|
+
if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
|
|
7594
8751
|
return "read-status";
|
|
7595
|
-
if (
|
|
7596
|
-
if (
|
|
7597
|
-
if (
|
|
7598
|
-
if (
|
|
8752
|
+
if (path22.startsWith("/api/maintenance")) return "trigger-job";
|
|
8753
|
+
if (path22.startsWith("/api/streams")) return "read-status";
|
|
8754
|
+
if (path22.startsWith("/api/sessions")) return "read-status";
|
|
8755
|
+
if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
7599
8756
|
return null;
|
|
7600
8757
|
}
|
|
7601
8758
|
|
|
@@ -7649,6 +8806,11 @@ var OrchestratorServer = class {
|
|
|
7649
8806
|
roadmapPath;
|
|
7650
8807
|
dispatchAdHoc;
|
|
7651
8808
|
sessionsDir;
|
|
8809
|
+
/**
|
|
8810
|
+
* Project root used by file-backed routes (Phase 4 proposals at
|
|
8811
|
+
* `.harness/proposals/`). Defaults to process.cwd().
|
|
8812
|
+
*/
|
|
8813
|
+
projectPath;
|
|
7652
8814
|
maintenanceDeps = null;
|
|
7653
8815
|
getLocalModelStatus = null;
|
|
7654
8816
|
getLocalModelStatuses = null;
|
|
@@ -7666,8 +8828,8 @@ var OrchestratorServer = class {
|
|
|
7666
8828
|
this.orchestrator = orchestrator;
|
|
7667
8829
|
this.port = port;
|
|
7668
8830
|
this.initDependencies(deps);
|
|
7669
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
7670
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
8831
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
|
|
8832
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
|
|
7671
8833
|
this.tokenStore = new TokenStore(tokensPath);
|
|
7672
8834
|
this.auditLogger = new AuditLogger(auditPath);
|
|
7673
8835
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -7680,14 +8842,15 @@ var OrchestratorServer = class {
|
|
|
7680
8842
|
}
|
|
7681
8843
|
initDependencies(deps) {
|
|
7682
8844
|
this.interactionQueue = deps?.interactionQueue;
|
|
7683
|
-
this.plansDir = deps?.plansDir ??
|
|
7684
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
8845
|
+
this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
|
|
8846
|
+
this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
|
|
7685
8847
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
7686
8848
|
this.pipeline = deps?.pipeline ?? null;
|
|
7687
8849
|
this.analysisArchive = deps?.analysisArchive;
|
|
7688
8850
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
7689
8851
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
7690
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
8852
|
+
this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
|
|
8853
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
7691
8854
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
7692
8855
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
7693
8856
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -7858,6 +9021,15 @@ var OrchestratorServer = class {
|
|
|
7858
9021
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
7859
9022
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
7860
9023
|
}),
|
|
9024
|
+
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9025
|
+
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9026
|
+
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
9027
|
+
// business logic. `projectPath` defaults to process.cwd() — that is
|
|
9028
|
+
// where `.harness/proposals/` lives in every deployment we ship.
|
|
9029
|
+
(req, res) => handleV1ProposalsRoute(req, res, {
|
|
9030
|
+
projectPath: this.projectPath,
|
|
9031
|
+
bus: this.orchestrator
|
|
9032
|
+
}),
|
|
7861
9033
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
7862
9034
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
7863
9035
|
];
|
|
@@ -7945,11 +9117,11 @@ var OrchestratorServer = class {
|
|
|
7945
9117
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
7946
9118
|
this.planWatcher.start();
|
|
7947
9119
|
}
|
|
7948
|
-
return new Promise((
|
|
9120
|
+
return new Promise((resolve7) => {
|
|
7949
9121
|
const host = getBindHost();
|
|
7950
9122
|
this.httpServer.listen(this.port, host, () => {
|
|
7951
9123
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
7952
|
-
|
|
9124
|
+
resolve7();
|
|
7953
9125
|
});
|
|
7954
9126
|
});
|
|
7955
9127
|
}
|
|
@@ -7999,8 +9171,8 @@ function genSecret2() {
|
|
|
7999
9171
|
return randomBytes4(32).toString("base64url");
|
|
8000
9172
|
}
|
|
8001
9173
|
var WebhookStore = class {
|
|
8002
|
-
constructor(
|
|
8003
|
-
this.path =
|
|
9174
|
+
constructor(path22) {
|
|
9175
|
+
this.path = path22;
|
|
8004
9176
|
}
|
|
8005
9177
|
path;
|
|
8006
9178
|
cache = null;
|
|
@@ -8391,7 +9563,12 @@ var WEBHOOK_TOPICS = [
|
|
|
8391
9563
|
"maintenance:completed",
|
|
8392
9564
|
"maintenance:error",
|
|
8393
9565
|
"webhook.subscription.created",
|
|
8394
|
-
"webhook.subscription.deleted"
|
|
9566
|
+
"webhook.subscription.deleted",
|
|
9567
|
+
// Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
|
|
9568
|
+
// `proposal.*` glob pattern to receive all three.
|
|
9569
|
+
"proposal.created",
|
|
9570
|
+
"proposal.approved",
|
|
9571
|
+
"proposal.rejected"
|
|
8395
9572
|
];
|
|
8396
9573
|
function newEventId2() {
|
|
8397
9574
|
return `evt_${randomBytes6(8).toString("hex")}`;
|
|
@@ -8586,13 +9763,373 @@ function wireTelemetryFanout(params) {
|
|
|
8586
9763
|
};
|
|
8587
9764
|
}
|
|
8588
9765
|
|
|
8589
|
-
// src/
|
|
8590
|
-
|
|
8591
|
-
|
|
8592
|
-
|
|
8593
|
-
|
|
8594
|
-
|
|
8595
|
-
|
|
9766
|
+
// src/notifications/slack-sink.ts
|
|
9767
|
+
var SEVERITY_PREFIX = {
|
|
9768
|
+
info: ":information_source:",
|
|
9769
|
+
success: ":white_check_mark:",
|
|
9770
|
+
warning: ":warning:",
|
|
9771
|
+
error: ":x:"
|
|
9772
|
+
};
|
|
9773
|
+
var SlackSink = class {
|
|
9774
|
+
kind = "slack";
|
|
9775
|
+
id;
|
|
9776
|
+
webhookUrl;
|
|
9777
|
+
fetchImpl;
|
|
9778
|
+
timeoutMs;
|
|
9779
|
+
constructor(opts) {
|
|
9780
|
+
this.id = opts.id;
|
|
9781
|
+
this.webhookUrl = opts.webhookUrl;
|
|
9782
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
9783
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
9784
|
+
}
|
|
9785
|
+
async deliver(input) {
|
|
9786
|
+
const body = input.wrapped ? this.renderEnvelope(input.payload) : this.renderRawEvent(input.payload);
|
|
9787
|
+
const ctrl = new AbortController();
|
|
9788
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
9789
|
+
try {
|
|
9790
|
+
const res = await this.fetchImpl(this.webhookUrl, {
|
|
9791
|
+
method: "POST",
|
|
9792
|
+
headers: { "Content-Type": "application/json" },
|
|
9793
|
+
body: JSON.stringify(body),
|
|
9794
|
+
signal: ctrl.signal
|
|
9795
|
+
});
|
|
9796
|
+
if (res.ok) {
|
|
9797
|
+
return { ok: true, deliveredAt: Date.now() };
|
|
9798
|
+
}
|
|
9799
|
+
return { ok: false, error: `HTTP ${res.status}`, httpStatus: res.status };
|
|
9800
|
+
} catch (err) {
|
|
9801
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9802
|
+
return { ok: false, error: ctrl.signal.aborted ? "timeout" : msg };
|
|
9803
|
+
} finally {
|
|
9804
|
+
clearTimeout(timer);
|
|
9805
|
+
}
|
|
9806
|
+
}
|
|
9807
|
+
renderEnvelope(env) {
|
|
9808
|
+
const prefix = SEVERITY_PREFIX[env.severity] ?? "";
|
|
9809
|
+
const headline = `${prefix} ${env.title}`.trim();
|
|
9810
|
+
const blocks = [
|
|
9811
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${headline}*
|
|
9812
|
+
${env.summary}` } }
|
|
9813
|
+
];
|
|
9814
|
+
if (env.actions && env.actions.length > 0) {
|
|
9815
|
+
blocks.push({
|
|
9816
|
+
type: "actions",
|
|
9817
|
+
elements: env.actions.map((a) => ({
|
|
9818
|
+
type: "button",
|
|
9819
|
+
text: { type: "plain_text", text: a.label },
|
|
9820
|
+
url: a.url
|
|
9821
|
+
}))
|
|
9822
|
+
});
|
|
9823
|
+
}
|
|
9824
|
+
if (env.permalink) {
|
|
9825
|
+
blocks.push({
|
|
9826
|
+
type: "section",
|
|
9827
|
+
text: { type: "mrkdwn", text: `<${env.permalink}|View details>` }
|
|
9828
|
+
});
|
|
9829
|
+
}
|
|
9830
|
+
return { text: headline, blocks };
|
|
9831
|
+
}
|
|
9832
|
+
renderRawEvent(event) {
|
|
9833
|
+
const dump = (() => {
|
|
9834
|
+
try {
|
|
9835
|
+
return JSON.stringify(event.data, null, 2);
|
|
9836
|
+
} catch {
|
|
9837
|
+
return String(event.data);
|
|
9838
|
+
}
|
|
9839
|
+
})();
|
|
9840
|
+
const text = `harness event: \`${event.type}\``;
|
|
9841
|
+
return {
|
|
9842
|
+
text,
|
|
9843
|
+
blocks: [
|
|
9844
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${text}*
|
|
9845
|
+
\`\`\`
|
|
9846
|
+
${dump}
|
|
9847
|
+
\`\`\`` } }
|
|
9848
|
+
]
|
|
9849
|
+
};
|
|
9850
|
+
}
|
|
9851
|
+
};
|
|
9852
|
+
|
|
9853
|
+
// src/notifications/registry.ts
|
|
9854
|
+
var SinkConfigError = class extends Error {
|
|
9855
|
+
constructor(sinkId, message) {
|
|
9856
|
+
super(`[sink:${sinkId}] ${message}`);
|
|
9857
|
+
this.sinkId = sinkId;
|
|
9858
|
+
this.name = "SinkConfigError";
|
|
9859
|
+
}
|
|
9860
|
+
sinkId;
|
|
9861
|
+
};
|
|
9862
|
+
var SinkRegistry = class _SinkRegistry {
|
|
9863
|
+
entries;
|
|
9864
|
+
constructor(entries) {
|
|
9865
|
+
this.entries = entries;
|
|
9866
|
+
}
|
|
9867
|
+
static fromConfig(config, options) {
|
|
9868
|
+
const entries = [];
|
|
9869
|
+
for (const sinkConfig of config.sinks) {
|
|
9870
|
+
entries.push({
|
|
9871
|
+
config: sinkConfig,
|
|
9872
|
+
adapter: buildSink(sinkConfig, options)
|
|
9873
|
+
});
|
|
9874
|
+
}
|
|
9875
|
+
return new _SinkRegistry(entries);
|
|
9876
|
+
}
|
|
9877
|
+
list() {
|
|
9878
|
+
return this.entries;
|
|
9879
|
+
}
|
|
9880
|
+
get(id) {
|
|
9881
|
+
return this.entries.find((e) => e.config.id === id) ?? null;
|
|
9882
|
+
}
|
|
9883
|
+
ids() {
|
|
9884
|
+
return this.entries.map((e) => e.config.id);
|
|
9885
|
+
}
|
|
9886
|
+
async dispose() {
|
|
9887
|
+
for (const entry of this.entries) {
|
|
9888
|
+
if (entry.adapter.dispose) {
|
|
9889
|
+
await entry.adapter.dispose();
|
|
9890
|
+
}
|
|
9891
|
+
}
|
|
9892
|
+
}
|
|
9893
|
+
};
|
|
9894
|
+
function buildSink(config, options) {
|
|
9895
|
+
const kind = config.kind;
|
|
9896
|
+
switch (kind) {
|
|
9897
|
+
case "slack":
|
|
9898
|
+
return buildSlackSink(config, options);
|
|
9899
|
+
default: {
|
|
9900
|
+
const _exhaustive = kind;
|
|
9901
|
+
throw new SinkConfigError(config.id, `unknown sink kind '${String(_exhaustive)}'`);
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9904
|
+
}
|
|
9905
|
+
function buildSlackSink(config, options) {
|
|
9906
|
+
const rawConfig = config.config;
|
|
9907
|
+
const envKey = typeof rawConfig.webhookUrlEnv === "string" ? rawConfig.webhookUrlEnv : null;
|
|
9908
|
+
const inlineUrl = typeof rawConfig.webhookUrl === "string" ? rawConfig.webhookUrl : null;
|
|
9909
|
+
let url;
|
|
9910
|
+
if (envKey) {
|
|
9911
|
+
const v = options.env[envKey];
|
|
9912
|
+
if (!v) {
|
|
9913
|
+
throw new SinkConfigError(
|
|
9914
|
+
config.id,
|
|
9915
|
+
`Slack webhook env var '${envKey}' is not set in the environment`
|
|
9916
|
+
);
|
|
9917
|
+
}
|
|
9918
|
+
url = v;
|
|
9919
|
+
} else if (inlineUrl) {
|
|
9920
|
+
url = inlineUrl;
|
|
9921
|
+
} else {
|
|
9922
|
+
throw new SinkConfigError(
|
|
9923
|
+
config.id,
|
|
9924
|
+
`Slack sink requires 'config.webhookUrlEnv' (preferred) or 'config.webhookUrl'`
|
|
9925
|
+
);
|
|
9926
|
+
}
|
|
9927
|
+
if (!/^https:\/\/hooks\.slack\.com\//.test(url)) {
|
|
9928
|
+
throw new SinkConfigError(
|
|
9929
|
+
config.id,
|
|
9930
|
+
`Slack webhook URL must be an https://hooks.slack.com/ URL`
|
|
9931
|
+
);
|
|
9932
|
+
}
|
|
9933
|
+
const sinkOpts = {
|
|
9934
|
+
id: config.id,
|
|
9935
|
+
webhookUrl: url
|
|
9936
|
+
};
|
|
9937
|
+
if (options.fetchImpl) sinkOpts.fetchImpl = options.fetchImpl;
|
|
9938
|
+
return new SlackSink(sinkOpts);
|
|
9939
|
+
}
|
|
9940
|
+
|
|
9941
|
+
// src/notifications/events.ts
|
|
9942
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
9943
|
+
|
|
9944
|
+
// src/notifications/envelope.ts
|
|
9945
|
+
function asObj(data) {
|
|
9946
|
+
return typeof data === "object" && data !== null ? data : {};
|
|
9947
|
+
}
|
|
9948
|
+
var ENVELOPE_DERIVERS = {
|
|
9949
|
+
"maintenance.started": (event) => {
|
|
9950
|
+
const data = asObj(event.data);
|
|
9951
|
+
return {
|
|
9952
|
+
title: `Maintenance started: ${data.taskId ?? "(unknown task)"}`,
|
|
9953
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` is running.`,
|
|
9954
|
+
severity: "info"
|
|
9955
|
+
};
|
|
9956
|
+
},
|
|
9957
|
+
"maintenance.completed": (event) => {
|
|
9958
|
+
const data = asObj(event.data);
|
|
9959
|
+
return {
|
|
9960
|
+
title: `Maintenance done: ${data.taskId ?? "(unknown task)"}`,
|
|
9961
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` completed successfully.`,
|
|
9962
|
+
severity: "success"
|
|
9963
|
+
};
|
|
9964
|
+
},
|
|
9965
|
+
"maintenance.error": (event) => {
|
|
9966
|
+
const data = asObj(event.data);
|
|
9967
|
+
return {
|
|
9968
|
+
title: `Maintenance failed: ${data.taskId ?? "(unknown task)"}`,
|
|
9969
|
+
summary: data.error ?? "No error message provided.",
|
|
9970
|
+
severity: "error"
|
|
9971
|
+
};
|
|
9972
|
+
},
|
|
9973
|
+
"interaction.created": (event) => {
|
|
9974
|
+
const data = asObj(event.data);
|
|
9975
|
+
return {
|
|
9976
|
+
title: `Action required: ${truncate(data.question ?? "pending interaction", 80)}`,
|
|
9977
|
+
summary: data.question ?? "(no question text)",
|
|
9978
|
+
severity: "warning"
|
|
9979
|
+
};
|
|
9980
|
+
},
|
|
9981
|
+
"interaction.resolved": (event) => {
|
|
9982
|
+
const data = asObj(event.data);
|
|
9983
|
+
return {
|
|
9984
|
+
title: `Interaction resolved`,
|
|
9985
|
+
summary: data.resolution ?? "(no resolution text)",
|
|
9986
|
+
severity: "info"
|
|
9987
|
+
};
|
|
9988
|
+
},
|
|
9989
|
+
"notification.test": (event) => {
|
|
9990
|
+
const data = asObj(event.data);
|
|
9991
|
+
return {
|
|
9992
|
+
title: "Test notification from harness",
|
|
9993
|
+
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9994
|
+
severity: "info"
|
|
9995
|
+
};
|
|
9996
|
+
},
|
|
9997
|
+
// Hermes Phase 4 — skill proposal lifecycle events.
|
|
9998
|
+
"proposal.created": (event) => {
|
|
9999
|
+
const data = asObj(event.data);
|
|
10000
|
+
const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
|
|
10001
|
+
return {
|
|
10002
|
+
title: `New skill proposal: ${label}`,
|
|
10003
|
+
summary: truncate(data.justification ?? "No justification provided.", 240),
|
|
10004
|
+
severity: "info"
|
|
10005
|
+
};
|
|
10006
|
+
},
|
|
10007
|
+
"proposal.approved": (event) => {
|
|
10008
|
+
const data = asObj(event.data);
|
|
10009
|
+
const label = data.name ?? data.targetSkill ?? "(unknown skill)";
|
|
10010
|
+
return {
|
|
10011
|
+
title: `Skill proposal approved: ${label}`,
|
|
10012
|
+
summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
|
|
10013
|
+
severity: "success"
|
|
10014
|
+
};
|
|
10015
|
+
},
|
|
10016
|
+
"proposal.rejected": (event) => {
|
|
10017
|
+
const data = asObj(event.data);
|
|
10018
|
+
return {
|
|
10019
|
+
title: "Skill proposal rejected",
|
|
10020
|
+
summary: truncate(data.reason ?? "No reason provided.", 240),
|
|
10021
|
+
severity: "warning"
|
|
10022
|
+
};
|
|
10023
|
+
}
|
|
10024
|
+
};
|
|
10025
|
+
function truncate(s, max) {
|
|
10026
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
10027
|
+
}
|
|
10028
|
+
function fallbackTitle(event) {
|
|
10029
|
+
return event.type;
|
|
10030
|
+
}
|
|
10031
|
+
function fallbackSummary(event) {
|
|
10032
|
+
try {
|
|
10033
|
+
return "```\n" + JSON.stringify(event.data, null, 2) + "\n```";
|
|
10034
|
+
} catch {
|
|
10035
|
+
return String(event.data);
|
|
10036
|
+
}
|
|
10037
|
+
}
|
|
10038
|
+
function severityFromType(type) {
|
|
10039
|
+
if (type.endsWith(".error") || type.endsWith(".failed")) return "error";
|
|
10040
|
+
if (type.endsWith(".completed") || type.endsWith(".resolved")) return "success";
|
|
10041
|
+
if (type.endsWith(".created") || type.startsWith("interaction.")) return "warning";
|
|
10042
|
+
return "info";
|
|
10043
|
+
}
|
|
10044
|
+
function backfillEnvelope(event, partial) {
|
|
10045
|
+
return {
|
|
10046
|
+
title: truncate(partial.title ?? fallbackTitle(event), 280),
|
|
10047
|
+
summary: partial.summary ?? fallbackSummary(event),
|
|
10048
|
+
severity: partial.severity ?? severityFromType(event.type)
|
|
10049
|
+
};
|
|
10050
|
+
}
|
|
10051
|
+
function wrapAsEnvelope(event) {
|
|
10052
|
+
const deriver = ENVELOPE_DERIVERS[event.type];
|
|
10053
|
+
const partial = deriver ? deriver(event) : {};
|
|
10054
|
+
const envelope = backfillEnvelope(event, partial);
|
|
10055
|
+
if (partial.actions) envelope.actions = partial.actions;
|
|
10056
|
+
if (partial.permalink) envelope.permalink = partial.permalink;
|
|
10057
|
+
if (event.correlationId) envelope.correlationId = event.correlationId;
|
|
10058
|
+
return envelope;
|
|
10059
|
+
}
|
|
10060
|
+
|
|
10061
|
+
// src/notifications/events.ts
|
|
10062
|
+
var NOTIFICATION_TOPICS = [
|
|
10063
|
+
"interaction.created",
|
|
10064
|
+
"interaction.resolved",
|
|
10065
|
+
"maintenance:started",
|
|
10066
|
+
"maintenance:completed",
|
|
10067
|
+
"maintenance:error",
|
|
10068
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10069
|
+
"proposal.created",
|
|
10070
|
+
"proposal.approved",
|
|
10071
|
+
"proposal.rejected"
|
|
10072
|
+
];
|
|
10073
|
+
function newEventId4() {
|
|
10074
|
+
return `evt_${randomBytes8(8).toString("hex")}`;
|
|
10075
|
+
}
|
|
10076
|
+
function dispatchToEntry(bus, entry, event) {
|
|
10077
|
+
const eventType = event.type;
|
|
10078
|
+
const matches = entry.config.events.some((p) => eventMatches(p, eventType));
|
|
10079
|
+
if (!matches) return;
|
|
10080
|
+
const payload = entry.config.wrap_response ? wrapAsEnvelope(event) : event;
|
|
10081
|
+
const summaryBase = {
|
|
10082
|
+
sinkId: entry.adapter.id,
|
|
10083
|
+
kind: entry.adapter.kind,
|
|
10084
|
+
eventType,
|
|
10085
|
+
eventId: event.id
|
|
10086
|
+
};
|
|
10087
|
+
void entry.adapter.deliver({ payload, wrapped: entry.config.wrap_response }).then((result) => {
|
|
10088
|
+
bus.emit("notification.delivery.attempted", { ...summaryBase, ok: result.ok });
|
|
10089
|
+
if (!result.ok) {
|
|
10090
|
+
bus.emit("notification.delivery.failed", {
|
|
10091
|
+
...summaryBase,
|
|
10092
|
+
ok: false,
|
|
10093
|
+
error: result.error
|
|
10094
|
+
});
|
|
10095
|
+
}
|
|
10096
|
+
}).catch((err) => {
|
|
10097
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10098
|
+
bus.emit("notification.delivery.failed", { ...summaryBase, ok: false, error: msg });
|
|
10099
|
+
});
|
|
10100
|
+
}
|
|
10101
|
+
function wireNotificationSinks({ bus, registry }) {
|
|
10102
|
+
const handlers = [];
|
|
10103
|
+
for (const topic of NOTIFICATION_TOPICS) {
|
|
10104
|
+
const eventType = topic.replace(":", ".");
|
|
10105
|
+
const fn = (data) => {
|
|
10106
|
+
const entries = registry.list();
|
|
10107
|
+
if (entries.length === 0) return;
|
|
10108
|
+
const event = {
|
|
10109
|
+
id: newEventId4(),
|
|
10110
|
+
type: eventType,
|
|
10111
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10112
|
+
data
|
|
10113
|
+
};
|
|
10114
|
+
for (const entry of entries) {
|
|
10115
|
+
dispatchToEntry(bus, entry, event);
|
|
10116
|
+
}
|
|
10117
|
+
};
|
|
10118
|
+
bus.on(topic, fn);
|
|
10119
|
+
handlers.push({ topic, fn });
|
|
10120
|
+
}
|
|
10121
|
+
return () => {
|
|
10122
|
+
for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
|
|
10123
|
+
};
|
|
10124
|
+
}
|
|
10125
|
+
|
|
10126
|
+
// src/orchestrator.ts
|
|
10127
|
+
import { CacheMetricsRecorder, OTLPExporter } from "@harness-engineering/core";
|
|
10128
|
+
|
|
10129
|
+
// src/logging/logger.ts
|
|
10130
|
+
var StructuredLogger = class {
|
|
10131
|
+
debug(message, context) {
|
|
10132
|
+
this.log("debug", message, context);
|
|
8596
10133
|
}
|
|
8597
10134
|
info(message, context) {
|
|
8598
10135
|
this.log("info", message, context);
|
|
@@ -8627,8 +10164,8 @@ var StructuredLogger = class {
|
|
|
8627
10164
|
};
|
|
8628
10165
|
|
|
8629
10166
|
// src/workspace/config-scanner.ts
|
|
8630
|
-
import { existsSync as
|
|
8631
|
-
import { join as
|
|
10167
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
10168
|
+
import { join as join14, relative } from "path";
|
|
8632
10169
|
import {
|
|
8633
10170
|
scanForInjection,
|
|
8634
10171
|
SecurityScanner,
|
|
@@ -8652,10 +10189,10 @@ function adjustFindingSeverity(findings) {
|
|
|
8652
10189
|
});
|
|
8653
10190
|
}
|
|
8654
10191
|
async function scanSingleFile(filePath, targetDir, scanner) {
|
|
8655
|
-
if (!
|
|
10192
|
+
if (!existsSync5(filePath)) return null;
|
|
8656
10193
|
let content;
|
|
8657
10194
|
try {
|
|
8658
|
-
content =
|
|
10195
|
+
content = readFileSync5(filePath, "utf8");
|
|
8659
10196
|
} catch {
|
|
8660
10197
|
return null;
|
|
8661
10198
|
}
|
|
@@ -8674,7 +10211,7 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
8674
10211
|
const scanner = new SecurityScanner(parseSecurityConfig({}));
|
|
8675
10212
|
const results = [];
|
|
8676
10213
|
for (const configFile of CONFIG_FILES) {
|
|
8677
|
-
const result = await scanSingleFile(
|
|
10214
|
+
const result = await scanSingleFile(join14(workspacePath, configFile), workspacePath, scanner);
|
|
8678
10215
|
if (result) results.push(result);
|
|
8679
10216
|
}
|
|
8680
10217
|
return { exitCode: computeScanExitCode(results), results };
|
|
@@ -8860,6 +10397,19 @@ var BUILT_IN_TASKS = [
|
|
|
8860
10397
|
schedule: "*/15 * * * *",
|
|
8861
10398
|
branch: null,
|
|
8862
10399
|
checkCommand: ["harness", "sync-main", "--json"]
|
|
10400
|
+
},
|
|
10401
|
+
// Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
|
|
10402
|
+
// on every existing catalog skill. Schedule is Feb 31 (a date that never
|
|
10403
|
+
// exists) so the cron loop never fires it automatically; operators trigger
|
|
10404
|
+
// it once via the dashboard "Run now" button or `harness backfill-skill-
|
|
10405
|
+
// provenance` after upgrading to Phase 4.
|
|
10406
|
+
{
|
|
10407
|
+
id: "proposal-provenance-backfill",
|
|
10408
|
+
type: "housekeeping",
|
|
10409
|
+
description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
|
|
10410
|
+
schedule: "0 0 31 2 *",
|
|
10411
|
+
branch: null,
|
|
10412
|
+
checkCommand: ["backfill-skill-provenance"]
|
|
8863
10413
|
}
|
|
8864
10414
|
];
|
|
8865
10415
|
|
|
@@ -8952,24 +10502,49 @@ var MaintenanceScheduler = class {
|
|
|
8952
10502
|
this.resolvedTasks = this.resolveTasks();
|
|
8953
10503
|
}
|
|
8954
10504
|
/**
|
|
8955
|
-
* Merge built-in task definitions with config overrides
|
|
8956
|
-
*
|
|
8957
|
-
*
|
|
10505
|
+
* Merge built-in task definitions with config overrides, then append
|
|
10506
|
+
* Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
|
|
10507
|
+
* overrides). Tasks with `enabled: false` are filtered out. Schedule
|
|
10508
|
+
* overrides replace the default cron expression.
|
|
8958
10509
|
*/
|
|
8959
10510
|
resolveTasks() {
|
|
8960
10511
|
const overrides = this.config.tasks ?? {};
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
return true;
|
|
8965
|
-
}).map((task) => {
|
|
10512
|
+
const customs = this.config.customTasks ?? {};
|
|
10513
|
+
const merged = [];
|
|
10514
|
+
for (const task of BUILT_IN_TASKS) {
|
|
8966
10515
|
const override = overrides[task.id];
|
|
8967
|
-
if (
|
|
8968
|
-
|
|
10516
|
+
if (override?.enabled === false) continue;
|
|
10517
|
+
merged.push({
|
|
8969
10518
|
...task,
|
|
8970
|
-
...override
|
|
8971
|
-
};
|
|
8972
|
-
}
|
|
10519
|
+
...override?.schedule !== void 0 && { schedule: override.schedule }
|
|
10520
|
+
});
|
|
10521
|
+
}
|
|
10522
|
+
for (const [id, def] of Object.entries(customs)) {
|
|
10523
|
+
const override = overrides[id];
|
|
10524
|
+
if (override?.enabled === false) continue;
|
|
10525
|
+
merged.push({
|
|
10526
|
+
id,
|
|
10527
|
+
type: def.type,
|
|
10528
|
+
description: def.description,
|
|
10529
|
+
schedule: override?.schedule ?? def.schedule,
|
|
10530
|
+
branch: def.branch,
|
|
10531
|
+
...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
|
|
10532
|
+
...def.checkScript !== void 0 && { checkScript: def.checkScript },
|
|
10533
|
+
...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
|
|
10534
|
+
...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
|
|
10535
|
+
...def.inlineSkillsBudgetTokens !== void 0 && {
|
|
10536
|
+
inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
|
|
10537
|
+
},
|
|
10538
|
+
...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
|
|
10539
|
+
...def.contextFromMaxAgeMinutes !== void 0 && {
|
|
10540
|
+
contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
|
|
10541
|
+
},
|
|
10542
|
+
...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
|
|
10543
|
+
...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
|
|
10544
|
+
isCustom: true
|
|
10545
|
+
});
|
|
10546
|
+
}
|
|
10547
|
+
return merged;
|
|
8973
10548
|
}
|
|
8974
10549
|
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
8975
10550
|
getResolvedTasks() {
|
|
@@ -9142,27 +10717,27 @@ var MaintenanceScheduler = class {
|
|
|
9142
10717
|
};
|
|
9143
10718
|
|
|
9144
10719
|
// src/maintenance/leader-elector.ts
|
|
9145
|
-
import { Ok as
|
|
10720
|
+
import { Ok as Ok22 } from "@harness-engineering/types";
|
|
9146
10721
|
var SingleProcessLeaderElector = class {
|
|
9147
10722
|
async electLeader() {
|
|
9148
|
-
return
|
|
10723
|
+
return Ok22("claimed");
|
|
9149
10724
|
}
|
|
9150
10725
|
};
|
|
9151
10726
|
|
|
9152
10727
|
// src/maintenance/reporter.ts
|
|
9153
|
-
import * as
|
|
9154
|
-
import * as
|
|
9155
|
-
import { z as
|
|
9156
|
-
var RunResultSchema =
|
|
9157
|
-
taskId:
|
|
9158
|
-
startedAt:
|
|
9159
|
-
completedAt:
|
|
9160
|
-
status:
|
|
9161
|
-
findings:
|
|
9162
|
-
fixed:
|
|
9163
|
-
prUrl:
|
|
9164
|
-
prUpdated:
|
|
9165
|
-
error:
|
|
10728
|
+
import * as fs15 from "fs";
|
|
10729
|
+
import * as path16 from "path";
|
|
10730
|
+
import { z as z16 } from "zod";
|
|
10731
|
+
var RunResultSchema = z16.object({
|
|
10732
|
+
taskId: z16.string(),
|
|
10733
|
+
startedAt: z16.string(),
|
|
10734
|
+
completedAt: z16.string(),
|
|
10735
|
+
status: z16.enum(["success", "failure", "skipped", "no-issues"]),
|
|
10736
|
+
findings: z16.number(),
|
|
10737
|
+
fixed: z16.number(),
|
|
10738
|
+
prUrl: z16.string().nullable(),
|
|
10739
|
+
prUpdated: z16.boolean(),
|
|
10740
|
+
error: z16.string().optional()
|
|
9166
10741
|
});
|
|
9167
10742
|
var MAX_HISTORY = 500;
|
|
9168
10743
|
var fallbackLogger = {
|
|
@@ -9186,10 +10761,10 @@ var MaintenanceReporter = class {
|
|
|
9186
10761
|
*/
|
|
9187
10762
|
async load() {
|
|
9188
10763
|
try {
|
|
9189
|
-
await
|
|
9190
|
-
const filePath =
|
|
9191
|
-
const data = await
|
|
9192
|
-
const parsed =
|
|
10764
|
+
await fs15.promises.mkdir(this.persistDir, { recursive: true });
|
|
10765
|
+
const filePath = path16.join(this.persistDir, "history.json");
|
|
10766
|
+
const data = await fs15.promises.readFile(filePath, "utf-8");
|
|
10767
|
+
const parsed = z16.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
9193
10768
|
if (parsed.success) {
|
|
9194
10769
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
9195
10770
|
}
|
|
@@ -9222,9 +10797,9 @@ var MaintenanceReporter = class {
|
|
|
9222
10797
|
*/
|
|
9223
10798
|
async persist() {
|
|
9224
10799
|
try {
|
|
9225
|
-
await
|
|
9226
|
-
const filePath =
|
|
9227
|
-
await
|
|
10800
|
+
await fs15.promises.mkdir(this.persistDir, { recursive: true });
|
|
10801
|
+
const filePath = path16.join(this.persistDir, "history.json");
|
|
10802
|
+
await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
9228
10803
|
} catch (err) {
|
|
9229
10804
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
9230
10805
|
}
|
|
@@ -9240,6 +10815,9 @@ var TaskRunner = class {
|
|
|
9240
10815
|
cwd;
|
|
9241
10816
|
prManager;
|
|
9242
10817
|
baseBranch;
|
|
10818
|
+
checkScriptRunner;
|
|
10819
|
+
contextResolver;
|
|
10820
|
+
outputStore;
|
|
9243
10821
|
constructor(options) {
|
|
9244
10822
|
this.config = options.config;
|
|
9245
10823
|
this.checkRunner = options.checkRunner;
|
|
@@ -9248,27 +10826,49 @@ var TaskRunner = class {
|
|
|
9248
10826
|
this.cwd = options.cwd;
|
|
9249
10827
|
this.prManager = options.prManager ?? null;
|
|
9250
10828
|
this.baseBranch = options.baseBranch ?? "main";
|
|
10829
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
10830
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
10831
|
+
this.outputStore = options.outputStore ?? null;
|
|
9251
10832
|
}
|
|
9252
10833
|
/**
|
|
9253
10834
|
* Run a maintenance task and return the result.
|
|
9254
10835
|
* Dispatches to the appropriate execution path based on task type.
|
|
9255
10836
|
* Never throws -- errors are captured in the RunResult.
|
|
10837
|
+
*
|
|
10838
|
+
* @param task - Resolved task definition.
|
|
10839
|
+
* @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
|
|
10840
|
+
* when called from the scheduler path.
|
|
9256
10841
|
*/
|
|
9257
|
-
async run(task) {
|
|
10842
|
+
async run(task, origin = "cron") {
|
|
9258
10843
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10844
|
+
let result;
|
|
10845
|
+
let captured;
|
|
9259
10846
|
try {
|
|
9260
10847
|
switch (task.type) {
|
|
9261
|
-
case "mechanical-ai":
|
|
9262
|
-
|
|
10848
|
+
case "mechanical-ai": {
|
|
10849
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
10850
|
+
result = out.result;
|
|
10851
|
+
captured = out.captured;
|
|
10852
|
+
break;
|
|
10853
|
+
}
|
|
9263
10854
|
case "pure-ai":
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
10855
|
+
result = await this.runPureAI(task, startedAt);
|
|
10856
|
+
break;
|
|
10857
|
+
case "report-only": {
|
|
10858
|
+
const out = await this.runReportOnly(task, startedAt);
|
|
10859
|
+
result = out.result;
|
|
10860
|
+
captured = out.captured;
|
|
10861
|
+
break;
|
|
10862
|
+
}
|
|
10863
|
+
case "housekeeping": {
|
|
10864
|
+
const out = await this.runHousekeeping(task, startedAt);
|
|
10865
|
+
result = out.result;
|
|
10866
|
+
captured = out.captured;
|
|
10867
|
+
break;
|
|
10868
|
+
}
|
|
9269
10869
|
default: {
|
|
9270
10870
|
const _exhaustive = task.type;
|
|
9271
|
-
|
|
10871
|
+
result = this.failureResult(
|
|
9272
10872
|
task.id,
|
|
9273
10873
|
startedAt,
|
|
9274
10874
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -9276,69 +10876,174 @@ var TaskRunner = class {
|
|
|
9276
10876
|
}
|
|
9277
10877
|
}
|
|
9278
10878
|
} catch (err) {
|
|
9279
|
-
|
|
10879
|
+
result = this.failureResult(task.id, startedAt, String(err));
|
|
10880
|
+
}
|
|
10881
|
+
result.origin = origin;
|
|
10882
|
+
await this.persistOutput(task, result, captured, origin);
|
|
10883
|
+
return result;
|
|
10884
|
+
}
|
|
10885
|
+
async persistOutput(task, result, captured, origin) {
|
|
10886
|
+
if (!this.outputStore) return;
|
|
10887
|
+
const entry = {
|
|
10888
|
+
taskId: result.taskId,
|
|
10889
|
+
startedAt: result.startedAt,
|
|
10890
|
+
completedAt: result.completedAt,
|
|
10891
|
+
status: result.status,
|
|
10892
|
+
findings: result.findings,
|
|
10893
|
+
fixed: result.fixed,
|
|
10894
|
+
prUrl: result.prUrl,
|
|
10895
|
+
prUpdated: result.prUpdated,
|
|
10896
|
+
origin,
|
|
10897
|
+
...result.error !== void 0 && { error: result.error },
|
|
10898
|
+
...result.costUsd !== void 0 && { costUsd: result.costUsd },
|
|
10899
|
+
...captured?.stdout !== void 0 && { stdout: captured.stdout },
|
|
10900
|
+
...captured?.stderr !== void 0 && { stderr: captured.stderr },
|
|
10901
|
+
...captured?.structured !== void 0 && { structured: captured.structured },
|
|
10902
|
+
...captured?.context !== void 0 && { context: captured.context }
|
|
10903
|
+
};
|
|
10904
|
+
try {
|
|
10905
|
+
await this.outputStore.write(task.id, entry, task.outputRetention);
|
|
10906
|
+
} catch {
|
|
9280
10907
|
}
|
|
9281
10908
|
}
|
|
9282
10909
|
/**
|
|
9283
|
-
*
|
|
10910
|
+
* Run the check step using whichever runner the task asks for. Custom
|
|
10911
|
+
* tasks that declare `checkScript` go through the Hermes Phase 2
|
|
10912
|
+
* `CheckScriptRunner`; built-ins (and customs that use the legacy
|
|
10913
|
+
* `checkCommand` shape) go through the original heuristic runner.
|
|
9284
10914
|
*/
|
|
9285
|
-
async
|
|
10915
|
+
async runCheckStep(task) {
|
|
10916
|
+
if (task.checkScript) {
|
|
10917
|
+
if (!this.checkScriptRunner) {
|
|
10918
|
+
throw new Error(
|
|
10919
|
+
`task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
|
|
10920
|
+
);
|
|
10921
|
+
}
|
|
10922
|
+
const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
|
|
10923
|
+
return {
|
|
10924
|
+
passed: r2.passed,
|
|
10925
|
+
findings: r2.findings,
|
|
10926
|
+
stdout: r2.output,
|
|
10927
|
+
stderr: r2.stderr,
|
|
10928
|
+
structured: r2.structured ? r2.structured : null
|
|
10929
|
+
};
|
|
10930
|
+
}
|
|
9286
10931
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
9287
|
-
|
|
10932
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
9288
10933
|
}
|
|
10934
|
+
const r = await this.checkRunner.run(task.checkCommand, this.cwd);
|
|
10935
|
+
return {
|
|
10936
|
+
passed: r.passed,
|
|
10937
|
+
findings: r.findings,
|
|
10938
|
+
stdout: r.output,
|
|
10939
|
+
stderr: "",
|
|
10940
|
+
structured: null
|
|
10941
|
+
};
|
|
10942
|
+
}
|
|
10943
|
+
/**
|
|
10944
|
+
* Hermes Phase 2 — Compose the agent prompt-context block from inlined
|
|
10945
|
+
* skills + upstream task outputs. Returns an empty string when nothing
|
|
10946
|
+
* is configured (or when the resolver is absent), which is the safe
|
|
10947
|
+
* no-op default.
|
|
10948
|
+
*/
|
|
10949
|
+
async composePromptContext(task) {
|
|
10950
|
+
if (!this.contextResolver) return "";
|
|
10951
|
+
const skills = await this.contextResolver.resolveInlineSkills(
|
|
10952
|
+
task.inlineSkills,
|
|
10953
|
+
task.inlineSkillsBudgetTokens ?? 8e3
|
|
10954
|
+
);
|
|
10955
|
+
const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
|
|
10956
|
+
maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
|
|
10957
|
+
});
|
|
10958
|
+
return [skills, upstream].filter(Boolean).join("\n");
|
|
10959
|
+
}
|
|
10960
|
+
/**
|
|
10961
|
+
* Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
|
|
10962
|
+
* only if fixable findings exist; persist captured stdout/stderr/context
|
|
10963
|
+
* via the output store on the way out.
|
|
10964
|
+
*/
|
|
10965
|
+
async runMechanicalAI(task, startedAt) {
|
|
9289
10966
|
if (!task.fixSkill) {
|
|
9290
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
10967
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
9291
10968
|
}
|
|
9292
10969
|
if (!task.branch) {
|
|
9293
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
10970
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
9294
10971
|
}
|
|
9295
|
-
|
|
9296
|
-
|
|
10972
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
10973
|
+
return wrap(
|
|
10974
|
+
this.failureResult(
|
|
10975
|
+
task.id,
|
|
10976
|
+
startedAt,
|
|
10977
|
+
"mechanical-ai task missing checkCommand or checkScript"
|
|
10978
|
+
)
|
|
10979
|
+
);
|
|
10980
|
+
}
|
|
10981
|
+
let check;
|
|
10982
|
+
try {
|
|
10983
|
+
check = await this.runCheckStep(task);
|
|
10984
|
+
} catch (err) {
|
|
10985
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
10986
|
+
}
|
|
10987
|
+
const promptContext = await this.composePromptContext(task);
|
|
10988
|
+
const baseCaptured = {
|
|
10989
|
+
stdout: check.stdout,
|
|
10990
|
+
stderr: check.stderr,
|
|
10991
|
+
structured: check.structured,
|
|
10992
|
+
...promptContext ? { context: promptContext } : {}
|
|
10993
|
+
};
|
|
10994
|
+
const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
|
|
10995
|
+
if (check.findings === 0 || wakeAgentExplicitlyFalse) {
|
|
9297
10996
|
return {
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
9304
|
-
|
|
9305
|
-
|
|
10997
|
+
result: {
|
|
10998
|
+
taskId: task.id,
|
|
10999
|
+
startedAt,
|
|
11000
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11001
|
+
status: "no-issues",
|
|
11002
|
+
findings: check.findings,
|
|
11003
|
+
fixed: 0,
|
|
11004
|
+
prUrl: null,
|
|
11005
|
+
prUpdated: false
|
|
11006
|
+
},
|
|
11007
|
+
captured: baseCaptured
|
|
9306
11008
|
};
|
|
9307
11009
|
}
|
|
9308
11010
|
if (this.prManager) {
|
|
9309
11011
|
try {
|
|
9310
11012
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
9311
11013
|
} catch (err) {
|
|
9312
|
-
return
|
|
11014
|
+
return wrap(
|
|
11015
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11016
|
+
baseCaptured
|
|
11017
|
+
);
|
|
9313
11018
|
}
|
|
9314
11019
|
}
|
|
9315
11020
|
const backendName = this.resolveBackend(task.id);
|
|
9316
11021
|
let agentResult;
|
|
9317
11022
|
try {
|
|
9318
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
backendName,
|
|
9322
|
-
this.cwd
|
|
9323
|
-
);
|
|
11023
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11024
|
+
promptContext
|
|
11025
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
9324
11026
|
} catch (err) {
|
|
9325
11027
|
return {
|
|
9326
|
-
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
|
|
9333
|
-
|
|
9334
|
-
|
|
11028
|
+
result: {
|
|
11029
|
+
taskId: task.id,
|
|
11030
|
+
startedAt,
|
|
11031
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11032
|
+
status: "failure",
|
|
11033
|
+
findings: check.findings,
|
|
11034
|
+
fixed: 0,
|
|
11035
|
+
prUrl: null,
|
|
11036
|
+
prUpdated: false,
|
|
11037
|
+
error: `Agent dispatch failed: ${String(err)}`
|
|
11038
|
+
},
|
|
11039
|
+
captured: baseCaptured
|
|
9335
11040
|
};
|
|
9336
11041
|
}
|
|
9337
11042
|
let prUrl = null;
|
|
9338
11043
|
let prUpdated = false;
|
|
9339
11044
|
if (this.prManager && agentResult.producedCommits) {
|
|
9340
11045
|
try {
|
|
9341
|
-
const summary = `Findings: ${
|
|
11046
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
9342
11047
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
9343
11048
|
prUrl = prResult.prUrl;
|
|
9344
11049
|
prUpdated = prResult.prUpdated;
|
|
@@ -9347,14 +11052,17 @@ var TaskRunner = class {
|
|
|
9347
11052
|
}
|
|
9348
11053
|
}
|
|
9349
11054
|
return {
|
|
9350
|
-
|
|
9351
|
-
|
|
9352
|
-
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
11055
|
+
result: {
|
|
11056
|
+
taskId: task.id,
|
|
11057
|
+
startedAt,
|
|
11058
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11059
|
+
status: "success",
|
|
11060
|
+
findings: check.findings,
|
|
11061
|
+
fixed: agentResult.fixed,
|
|
11062
|
+
prUrl,
|
|
11063
|
+
prUpdated
|
|
11064
|
+
},
|
|
11065
|
+
captured: baseCaptured
|
|
9358
11066
|
};
|
|
9359
11067
|
}
|
|
9360
11068
|
/**
|
|
@@ -9374,15 +11082,13 @@ var TaskRunner = class {
|
|
|
9374
11082
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
9375
11083
|
}
|
|
9376
11084
|
}
|
|
11085
|
+
const promptContext = await this.composePromptContext(task);
|
|
9377
11086
|
const backendName = this.resolveBackend(task.id);
|
|
9378
11087
|
let agentResult;
|
|
9379
11088
|
try {
|
|
9380
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
backendName,
|
|
9384
|
-
this.cwd
|
|
9385
|
-
);
|
|
11089
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11090
|
+
promptContext
|
|
11091
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
9386
11092
|
} catch (err) {
|
|
9387
11093
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
9388
11094
|
}
|
|
@@ -9410,7 +11116,7 @@ var TaskRunner = class {
|
|
|
9410
11116
|
};
|
|
9411
11117
|
}
|
|
9412
11118
|
/**
|
|
9413
|
-
* Report-only: run check
|
|
11119
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
9414
11120
|
*
|
|
9415
11121
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
9416
11122
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -9420,122 +11126,715 @@ var TaskRunner = class {
|
|
|
9420
11126
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
9421
11127
|
*/
|
|
9422
11128
|
async runReportOnly(task, startedAt) {
|
|
9423
|
-
if (!task.checkCommand
|
|
9424
|
-
return
|
|
11129
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11130
|
+
return wrap(
|
|
11131
|
+
this.failureResult(
|
|
11132
|
+
task.id,
|
|
11133
|
+
startedAt,
|
|
11134
|
+
"report-only task missing checkCommand or checkScript"
|
|
11135
|
+
)
|
|
11136
|
+
);
|
|
11137
|
+
}
|
|
11138
|
+
let check;
|
|
11139
|
+
try {
|
|
11140
|
+
check = await this.runCheckStep(task);
|
|
11141
|
+
} catch (err) {
|
|
11142
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11143
|
+
}
|
|
11144
|
+
const parsed = parseStatusLine(check.stdout);
|
|
11145
|
+
const status = parsed?.status ?? "success";
|
|
11146
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
11147
|
+
const result = {
|
|
11148
|
+
taskId: task.id,
|
|
11149
|
+
startedAt,
|
|
11150
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11151
|
+
status,
|
|
11152
|
+
findings,
|
|
11153
|
+
fixed: 0,
|
|
11154
|
+
prUrl: null,
|
|
11155
|
+
prUpdated: false
|
|
11156
|
+
};
|
|
11157
|
+
if (parsed?.error) {
|
|
11158
|
+
result.error = parsed.error;
|
|
11159
|
+
}
|
|
11160
|
+
return {
|
|
11161
|
+
result,
|
|
11162
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11163
|
+
};
|
|
11164
|
+
}
|
|
11165
|
+
/**
|
|
11166
|
+
* Housekeeping: run command directly, no AI, no PR.
|
|
11167
|
+
*
|
|
11168
|
+
* Captures stdout and parses a trailing JSON status line if present.
|
|
11169
|
+
* Recognized contracts:
|
|
11170
|
+
* - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
|
|
11171
|
+
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
11172
|
+
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
11173
|
+
* status: 'success', findings: 0.
|
|
11174
|
+
*
|
|
11175
|
+
* Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
|
|
11176
|
+
* tasks; the runner falls through to the same JSON-status parsing path.
|
|
11177
|
+
*/
|
|
11178
|
+
async runHousekeeping(task, startedAt) {
|
|
11179
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11180
|
+
return wrap(
|
|
11181
|
+
this.failureResult(
|
|
11182
|
+
task.id,
|
|
11183
|
+
startedAt,
|
|
11184
|
+
"housekeeping task missing checkCommand or checkScript"
|
|
11185
|
+
)
|
|
11186
|
+
);
|
|
11187
|
+
}
|
|
11188
|
+
let stdout;
|
|
11189
|
+
let stderr = "";
|
|
11190
|
+
let structured = null;
|
|
11191
|
+
if (task.checkScript) {
|
|
11192
|
+
try {
|
|
11193
|
+
const r = await this.runCheckStep(task);
|
|
11194
|
+
stdout = r.stdout;
|
|
11195
|
+
stderr = r.stderr;
|
|
11196
|
+
structured = r.structured;
|
|
11197
|
+
} catch (err) {
|
|
11198
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11199
|
+
}
|
|
11200
|
+
} else {
|
|
11201
|
+
try {
|
|
11202
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
11203
|
+
stdout = out.stdout ?? "";
|
|
11204
|
+
} catch (err) {
|
|
11205
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11206
|
+
}
|
|
11207
|
+
}
|
|
11208
|
+
const parsed = parseStatusLine(stdout);
|
|
11209
|
+
const status = parsed?.status ?? "success";
|
|
11210
|
+
const result = {
|
|
11211
|
+
taskId: task.id,
|
|
11212
|
+
startedAt,
|
|
11213
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11214
|
+
status,
|
|
11215
|
+
findings: 0,
|
|
11216
|
+
fixed: 0,
|
|
11217
|
+
prUrl: null,
|
|
11218
|
+
prUpdated: false
|
|
11219
|
+
};
|
|
11220
|
+
if (parsed?.error) result.error = parsed.error;
|
|
11221
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
11222
|
+
}
|
|
11223
|
+
/**
|
|
11224
|
+
* Resolve which AI backend name to use for a given task.
|
|
11225
|
+
* Priority: per-task override > global config > 'local' default.
|
|
11226
|
+
*/
|
|
11227
|
+
resolveBackend(taskId) {
|
|
11228
|
+
const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
|
|
11229
|
+
if (taskOverride) return taskOverride;
|
|
11230
|
+
return this.config.aiBackend ?? "local";
|
|
11231
|
+
}
|
|
11232
|
+
failureResult(taskId, startedAt, error) {
|
|
11233
|
+
return {
|
|
11234
|
+
taskId,
|
|
11235
|
+
startedAt,
|
|
11236
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11237
|
+
status: "failure",
|
|
11238
|
+
findings: 0,
|
|
11239
|
+
fixed: 0,
|
|
11240
|
+
prUrl: null,
|
|
11241
|
+
prUpdated: false,
|
|
11242
|
+
error
|
|
11243
|
+
};
|
|
11244
|
+
}
|
|
11245
|
+
};
|
|
11246
|
+
function wrap(result, captured) {
|
|
11247
|
+
return captured ? { result, captured } : { result };
|
|
11248
|
+
}
|
|
11249
|
+
function parseStatusLine(output) {
|
|
11250
|
+
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11251
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11252
|
+
const line = lines[i];
|
|
11253
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
|
|
11254
|
+
try {
|
|
11255
|
+
const obj = JSON.parse(line);
|
|
11256
|
+
const s = obj.status;
|
|
11257
|
+
if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
|
|
11258
|
+
const parsed = { status: s, rawStatus: s };
|
|
11259
|
+
if (typeof obj.candidatesFound === "number") {
|
|
11260
|
+
parsed.candidatesFound = obj.candidatesFound;
|
|
11261
|
+
}
|
|
11262
|
+
if (typeof obj.error === "string") {
|
|
11263
|
+
parsed.error = obj.error;
|
|
11264
|
+
}
|
|
11265
|
+
if (typeof obj.reason === "string") {
|
|
11266
|
+
parsed.reason = obj.reason;
|
|
11267
|
+
}
|
|
11268
|
+
if (typeof obj.detail === "string" && !parsed.error) {
|
|
11269
|
+
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
11270
|
+
}
|
|
11271
|
+
return parsed;
|
|
11272
|
+
}
|
|
11273
|
+
if (s === "updated" || s === "no-op") {
|
|
11274
|
+
return { status: "success", rawStatus: s };
|
|
11275
|
+
}
|
|
11276
|
+
if (s === "error") {
|
|
11277
|
+
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
11278
|
+
return { status: "failure", error: message, rawStatus: "error" };
|
|
11279
|
+
}
|
|
11280
|
+
} catch {
|
|
11281
|
+
}
|
|
11282
|
+
}
|
|
11283
|
+
return null;
|
|
11284
|
+
}
|
|
11285
|
+
|
|
11286
|
+
// src/maintenance/check-script-runner.ts
|
|
11287
|
+
import { execFile as execFile6 } from "child_process";
|
|
11288
|
+
import { promisify as promisify3 } from "util";
|
|
11289
|
+
import * as path17 from "path";
|
|
11290
|
+
var execFileAsync = promisify3(execFile6);
|
|
11291
|
+
var CheckScriptRunner = class {
|
|
11292
|
+
constructor(cwd) {
|
|
11293
|
+
this.cwd = cwd;
|
|
11294
|
+
}
|
|
11295
|
+
cwd;
|
|
11296
|
+
async run(spec, cwd) {
|
|
11297
|
+
const projectRoot = cwd ?? this.cwd;
|
|
11298
|
+
const captured = await captureScript(spec, projectRoot);
|
|
11299
|
+
const parseJson = spec.parseStdoutJson !== false;
|
|
11300
|
+
const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
|
|
11301
|
+
if (structured) {
|
|
11302
|
+
return mapStructured(structured, captured.stdout, captured.stderr);
|
|
11303
|
+
}
|
|
11304
|
+
return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
|
|
11305
|
+
}
|
|
11306
|
+
};
|
|
11307
|
+
async function captureScript(spec, projectRoot) {
|
|
11308
|
+
const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
|
|
11309
|
+
const args = spec.args ?? [];
|
|
11310
|
+
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11311
|
+
try {
|
|
11312
|
+
const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
|
|
11313
|
+
return {
|
|
11314
|
+
stdout: String(result.stdout ?? ""),
|
|
11315
|
+
stderr: String(result.stderr ?? ""),
|
|
11316
|
+
exitedAbnormally: false
|
|
11317
|
+
};
|
|
11318
|
+
} catch (err) {
|
|
11319
|
+
const e = err;
|
|
11320
|
+
return {
|
|
11321
|
+
stdout: String(e.stdout ?? ""),
|
|
11322
|
+
stderr: String(e.stderr ?? ""),
|
|
11323
|
+
exitedAbnormally: true
|
|
11324
|
+
};
|
|
11325
|
+
}
|
|
11326
|
+
}
|
|
11327
|
+
function parseStatusEnvelope(stdout) {
|
|
11328
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11329
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11330
|
+
const env = classifyLine2(lines[i]);
|
|
11331
|
+
if (env) return env;
|
|
11332
|
+
}
|
|
11333
|
+
return null;
|
|
11334
|
+
}
|
|
11335
|
+
var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
|
|
11336
|
+
function classifyLine2(line) {
|
|
11337
|
+
const obj = tryParseJsonObject(line);
|
|
11338
|
+
if (!obj) return null;
|
|
11339
|
+
const s = obj.status;
|
|
11340
|
+
if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
|
|
11341
|
+
return buildEnvelope(s, obj);
|
|
11342
|
+
}
|
|
11343
|
+
function tryParseJsonObject(line) {
|
|
11344
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
|
|
11345
|
+
try {
|
|
11346
|
+
return JSON.parse(line);
|
|
11347
|
+
} catch {
|
|
11348
|
+
return null;
|
|
11349
|
+
}
|
|
11350
|
+
}
|
|
11351
|
+
function buildEnvelope(status, obj) {
|
|
11352
|
+
const env = { status };
|
|
11353
|
+
if (typeof obj.findings === "number") env.findings = obj.findings;
|
|
11354
|
+
if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
|
|
11355
|
+
if (typeof obj.message === "string") env.message = obj.message;
|
|
11356
|
+
if (obj.outputs && typeof obj.outputs === "object") {
|
|
11357
|
+
env.outputs = obj.outputs;
|
|
11358
|
+
}
|
|
11359
|
+
return env;
|
|
11360
|
+
}
|
|
11361
|
+
function mapStructured(env, stdout, stderr) {
|
|
11362
|
+
const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
|
|
11363
|
+
switch (env.status) {
|
|
11364
|
+
case "ok":
|
|
11365
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11366
|
+
case "findings": {
|
|
11367
|
+
const wake = env.wakeAgent ?? findings > 0;
|
|
11368
|
+
return { passed: !wake, findings, output: stdout, stderr, structured: env };
|
|
11369
|
+
}
|
|
11370
|
+
case "skip":
|
|
11371
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11372
|
+
case "error":
|
|
11373
|
+
return {
|
|
11374
|
+
passed: false,
|
|
11375
|
+
findings: Math.max(findings, 1),
|
|
11376
|
+
output: stdout,
|
|
11377
|
+
stderr,
|
|
11378
|
+
structured: env
|
|
11379
|
+
};
|
|
11380
|
+
default:
|
|
11381
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11382
|
+
}
|
|
11383
|
+
}
|
|
11384
|
+
function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
11385
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
11386
|
+
const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
11387
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
|
|
11388
|
+
return {
|
|
11389
|
+
passed: findings === 0 && !exitedAbnormally,
|
|
11390
|
+
findings,
|
|
11391
|
+
output: stdout,
|
|
11392
|
+
stderr,
|
|
11393
|
+
structured: null
|
|
11394
|
+
};
|
|
11395
|
+
}
|
|
11396
|
+
|
|
11397
|
+
// src/maintenance/output-store.ts
|
|
11398
|
+
import * as fs16 from "fs";
|
|
11399
|
+
import * as path18 from "path";
|
|
11400
|
+
var DEFAULT_RETENTION = {
|
|
11401
|
+
runs: 50,
|
|
11402
|
+
maxAgeDays: 30
|
|
11403
|
+
};
|
|
11404
|
+
var fallbackLogger2 = {
|
|
11405
|
+
info: () => {
|
|
11406
|
+
},
|
|
11407
|
+
warn: (m, c) => console.warn(m, c),
|
|
11408
|
+
error: (m, c) => console.error(m, c)
|
|
11409
|
+
};
|
|
11410
|
+
var TaskOutputStore = class {
|
|
11411
|
+
rootDir;
|
|
11412
|
+
retentionDefaults;
|
|
11413
|
+
logger;
|
|
11414
|
+
constructor(options) {
|
|
11415
|
+
this.rootDir = options.rootDir;
|
|
11416
|
+
this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
|
|
11417
|
+
this.logger = options.logger ?? fallbackLogger2;
|
|
11418
|
+
}
|
|
11419
|
+
/**
|
|
11420
|
+
* Reject task IDs that don't match the validator's kebab-case pattern —
|
|
11421
|
+
* defends `dirFor()` against caller-supplied path-traversal segments
|
|
11422
|
+
* (`'../foo'`) when the store is invoked from CLI surfaces that don't
|
|
11423
|
+
* round-trip through `validateCustomTasks`.
|
|
11424
|
+
*/
|
|
11425
|
+
ensureSafeTaskId(taskId) {
|
|
11426
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
|
|
11427
|
+
throw new Error(
|
|
11428
|
+
`TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
|
|
11429
|
+
);
|
|
11430
|
+
}
|
|
11431
|
+
}
|
|
11432
|
+
/**
|
|
11433
|
+
* Persist a single run entry. Retention is applied after the write so
|
|
11434
|
+
* the latest record is durable even if pruning fails.
|
|
11435
|
+
*/
|
|
11436
|
+
async write(taskId, entry, retention) {
|
|
11437
|
+
this.ensureSafeTaskId(taskId);
|
|
11438
|
+
const dir = this.dirFor(taskId);
|
|
11439
|
+
await fs16.promises.mkdir(dir, { recursive: true });
|
|
11440
|
+
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11441
|
+
const filePath = path18.join(dir, fileName);
|
|
11442
|
+
const tmpPath = `${filePath}.tmp`;
|
|
11443
|
+
const payload = JSON.stringify(entry, null, 2);
|
|
11444
|
+
await fs16.promises.writeFile(tmpPath, payload, "utf-8");
|
|
11445
|
+
await fs16.promises.rename(tmpPath, filePath);
|
|
11446
|
+
try {
|
|
11447
|
+
await this.applyRetention(taskId, retention);
|
|
11448
|
+
} catch (err) {
|
|
11449
|
+
this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11452
|
+
/**
|
|
11453
|
+
* Return the most recent persisted entry for the task, or null if none.
|
|
11454
|
+
*/
|
|
11455
|
+
async latest(taskId) {
|
|
11456
|
+
const entries = await this.list(taskId, 1, 0);
|
|
11457
|
+
return entries[0] ?? null;
|
|
11458
|
+
}
|
|
11459
|
+
/**
|
|
11460
|
+
* List entries newest-first with offset+limit pagination.
|
|
11461
|
+
*/
|
|
11462
|
+
async list(taskId, limit, offset) {
|
|
11463
|
+
this.ensureSafeTaskId(taskId);
|
|
11464
|
+
const dir = this.dirFor(taskId);
|
|
11465
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
11466
|
+
const slice = fileNames.slice(offset, offset + limit);
|
|
11467
|
+
const out = [];
|
|
11468
|
+
for (const name of slice) {
|
|
11469
|
+
const entry = await this.readEntry(path18.join(dir, name));
|
|
11470
|
+
if (entry) out.push(entry);
|
|
11471
|
+
}
|
|
11472
|
+
return out;
|
|
11473
|
+
}
|
|
11474
|
+
/**
|
|
11475
|
+
* Lookup a specific run by its file name (without the `.json` suffix) or
|
|
11476
|
+
* by its raw completion timestamp.
|
|
11477
|
+
*/
|
|
11478
|
+
async get(taskId, runId) {
|
|
11479
|
+
this.ensureSafeTaskId(taskId);
|
|
11480
|
+
if (/[\\/]|\.\./.test(runId)) {
|
|
11481
|
+
throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
|
|
11482
|
+
}
|
|
11483
|
+
const dir = this.dirFor(taskId);
|
|
11484
|
+
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
11485
|
+
return this.readEntry(path18.join(dir, fileName));
|
|
11486
|
+
}
|
|
11487
|
+
/**
|
|
11488
|
+
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
11489
|
+
* outputs from outside the store API.
|
|
11490
|
+
*/
|
|
11491
|
+
dirFor(taskId) {
|
|
11492
|
+
return path18.join(this.rootDir, taskId, "outputs");
|
|
11493
|
+
}
|
|
11494
|
+
async readEntry(filePath) {
|
|
11495
|
+
try {
|
|
11496
|
+
const buf = await fs16.promises.readFile(filePath, "utf-8");
|
|
11497
|
+
const parsed = JSON.parse(buf);
|
|
11498
|
+
return parsed;
|
|
11499
|
+
} catch {
|
|
11500
|
+
return null;
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
async applyRetention(taskId, retention) {
|
|
11504
|
+
const runs = retention?.runs ?? this.retentionDefaults.runs;
|
|
11505
|
+
const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
|
|
11506
|
+
const dir = this.dirFor(taskId);
|
|
11507
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
11508
|
+
const overflow = fileNames.slice(runs);
|
|
11509
|
+
const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
11510
|
+
const aged = [];
|
|
11511
|
+
for (const name of fileNames) {
|
|
11512
|
+
const ts = parseIsoFromFileName(name);
|
|
11513
|
+
if (ts !== null && ts < cutoffMs) aged.push(name);
|
|
11514
|
+
}
|
|
11515
|
+
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
11516
|
+
for (const name of toRemove) {
|
|
11517
|
+
try {
|
|
11518
|
+
await fs16.promises.unlink(path18.join(dir, name));
|
|
11519
|
+
} catch {
|
|
11520
|
+
}
|
|
11521
|
+
}
|
|
11522
|
+
}
|
|
11523
|
+
};
|
|
11524
|
+
async function listJsonFilesDescending(dir) {
|
|
11525
|
+
let names;
|
|
11526
|
+
try {
|
|
11527
|
+
names = await fs16.promises.readdir(dir);
|
|
11528
|
+
} catch {
|
|
11529
|
+
return [];
|
|
11530
|
+
}
|
|
11531
|
+
return names.filter((n) => n.endsWith(".json")).sort().reverse();
|
|
11532
|
+
}
|
|
11533
|
+
function sanitizeIso(iso) {
|
|
11534
|
+
return iso.replace(/:/g, "-");
|
|
11535
|
+
}
|
|
11536
|
+
function parseIsoFromFileName(fileName) {
|
|
11537
|
+
const stem = fileName.replace(/\.json$/, "");
|
|
11538
|
+
const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
|
|
11539
|
+
const ms = Date.parse(restored);
|
|
11540
|
+
return Number.isFinite(ms) ? ms : null;
|
|
11541
|
+
}
|
|
11542
|
+
|
|
11543
|
+
// src/maintenance/context-resolver.ts
|
|
11544
|
+
var ContextResolver = class {
|
|
11545
|
+
outputStore;
|
|
11546
|
+
skillReader;
|
|
11547
|
+
logger;
|
|
11548
|
+
perUpstreamMaxChars;
|
|
11549
|
+
constructor(options) {
|
|
11550
|
+
this.outputStore = options.outputStore;
|
|
11551
|
+
this.skillReader = options.skillReader ?? null;
|
|
11552
|
+
this.logger = options.logger ?? fallbackLogger3;
|
|
11553
|
+
this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
|
|
11554
|
+
}
|
|
11555
|
+
async resolveContextFrom(upstreamTaskIds, options = {}) {
|
|
11556
|
+
if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
|
|
11557
|
+
const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
|
|
11558
|
+
const now = Date.now();
|
|
11559
|
+
const sections = [];
|
|
11560
|
+
for (const id of upstreamTaskIds) {
|
|
11561
|
+
const entry = await this.outputStore.latest(id);
|
|
11562
|
+
sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
|
|
11563
|
+
}
|
|
11564
|
+
return `## Upstream context
|
|
11565
|
+
|
|
11566
|
+
${sections.join("\n\n")}
|
|
11567
|
+
`;
|
|
11568
|
+
}
|
|
11569
|
+
async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
|
|
11570
|
+
if (!skillNames || skillNames.length === 0) return "";
|
|
11571
|
+
if (!this.skillReader) return "";
|
|
11572
|
+
const charBudget = budgetTokens * 4;
|
|
11573
|
+
let used = 0;
|
|
11574
|
+
const sections = [];
|
|
11575
|
+
let truncatedAt = -1;
|
|
11576
|
+
for (let i = 0; i < skillNames.length; i++) {
|
|
11577
|
+
const name = skillNames[i];
|
|
11578
|
+
const body = await this.skillReader.read(name);
|
|
11579
|
+
if (body === null) {
|
|
11580
|
+
this.logger.warn("inlineSkills: skill not found in registry", { name });
|
|
11581
|
+
continue;
|
|
11582
|
+
}
|
|
11583
|
+
const block = `### ${name}
|
|
11584
|
+
|
|
11585
|
+
${body}`;
|
|
11586
|
+
if (used + block.length > charBudget) {
|
|
11587
|
+
truncatedAt = i;
|
|
11588
|
+
break;
|
|
11589
|
+
}
|
|
11590
|
+
used += block.length;
|
|
11591
|
+
sections.push(block);
|
|
11592
|
+
}
|
|
11593
|
+
if (truncatedAt >= 0) {
|
|
11594
|
+
this.logger.warn(
|
|
11595
|
+
`inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
|
|
11596
|
+
);
|
|
11597
|
+
}
|
|
11598
|
+
if (sections.length === 0) return "";
|
|
11599
|
+
return `## Reference skills
|
|
11600
|
+
|
|
11601
|
+
${sections.join("\n\n")}
|
|
11602
|
+
`;
|
|
11603
|
+
}
|
|
11604
|
+
formatUpstream(id, entry, now, maxAgeMs) {
|
|
11605
|
+
if (!entry) {
|
|
11606
|
+
return `### ${id}
|
|
11607
|
+
|
|
11608
|
+
_[no prior run]_`;
|
|
11609
|
+
}
|
|
11610
|
+
const completedMs = Date.parse(entry.completedAt);
|
|
11611
|
+
if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
|
|
11612
|
+
return `### ${id} (last run ${entry.completedAt}, stale)
|
|
11613
|
+
|
|
11614
|
+
_[stale: omitted]_`;
|
|
11615
|
+
}
|
|
11616
|
+
const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
|
|
11617
|
+
const body = (entry.stdout ?? "").trim();
|
|
11618
|
+
const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
|
|
11619
|
+
|
|
11620
|
+
_[truncated]_` : body;
|
|
11621
|
+
return `${head}
|
|
11622
|
+
|
|
11623
|
+
${truncated || "_[no stdout captured]_"}`;
|
|
11624
|
+
}
|
|
11625
|
+
};
|
|
11626
|
+
var fallbackLogger3 = {
|
|
11627
|
+
info: () => {
|
|
11628
|
+
},
|
|
11629
|
+
warn: () => {
|
|
11630
|
+
},
|
|
11631
|
+
error: () => {
|
|
11632
|
+
}
|
|
11633
|
+
};
|
|
11634
|
+
|
|
11635
|
+
// src/maintenance/custom-task-validator.ts
|
|
11636
|
+
import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
|
|
11637
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
11638
|
+
var REQUIRED_FIELDS_BY_TYPE = {
|
|
11639
|
+
"mechanical-ai": ["branch", "fixSkill"],
|
|
11640
|
+
"pure-ai": ["branch", "fixSkill"],
|
|
11641
|
+
"report-only": [],
|
|
11642
|
+
housekeeping: []
|
|
11643
|
+
};
|
|
11644
|
+
function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
11645
|
+
const errors = [];
|
|
11646
|
+
if (!customTasks) return Ok23(void 0);
|
|
11647
|
+
const builtInIds = new Set(builtIns.map((t) => t.id));
|
|
11648
|
+
const customIds = Object.keys(customTasks);
|
|
11649
|
+
const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
|
|
11650
|
+
for (const id of customIds) {
|
|
11651
|
+
const task = customTasks[id];
|
|
11652
|
+
if (!task) continue;
|
|
11653
|
+
validateOne(id, task, builtInIds, allIds, deps, errors);
|
|
11654
|
+
}
|
|
11655
|
+
detectCycles(customTasks, builtIns, errors);
|
|
11656
|
+
return errors.length === 0 ? Ok23(void 0) : Err20(errors);
|
|
11657
|
+
}
|
|
11658
|
+
function validateOne(id, task, builtInIds, allIds, deps, errors) {
|
|
11659
|
+
const prefix = `customTasks.${id}`;
|
|
11660
|
+
if (!ID_PATTERN.test(id)) {
|
|
11661
|
+
errors.push({
|
|
11662
|
+
path: prefix,
|
|
11663
|
+
message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
|
|
11664
|
+
});
|
|
11665
|
+
}
|
|
11666
|
+
if (builtInIds.has(id)) {
|
|
11667
|
+
errors.push({
|
|
11668
|
+
path: prefix,
|
|
11669
|
+
message: `task ID '${id}' collides with a built-in task; choose a different name`
|
|
11670
|
+
});
|
|
11671
|
+
}
|
|
11672
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
11673
|
+
errors.push({ path: `${prefix}.description`, message: "description is required" });
|
|
11674
|
+
}
|
|
11675
|
+
if (!task.schedule || task.schedule.trim().length === 0) {
|
|
11676
|
+
errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
|
|
11677
|
+
}
|
|
11678
|
+
validateCheckShape(prefix, task, errors);
|
|
11679
|
+
validateRequiredByType(prefix, task, errors);
|
|
11680
|
+
validateContextFrom(prefix, id, task, allIds, errors);
|
|
11681
|
+
validateInlineSkills(prefix, task, deps, errors);
|
|
11682
|
+
validateScriptPath(prefix, task, deps, errors);
|
|
11683
|
+
}
|
|
11684
|
+
function validateCheckShape(prefix, task, errors) {
|
|
11685
|
+
const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
|
|
11686
|
+
const hasScript = task.checkScript !== void 0;
|
|
11687
|
+
if (hasCommand && hasScript) {
|
|
11688
|
+
errors.push({
|
|
11689
|
+
path: prefix,
|
|
11690
|
+
message: "a task may declare checkCommand OR checkScript, not both"
|
|
11691
|
+
});
|
|
11692
|
+
}
|
|
11693
|
+
const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
|
|
11694
|
+
if (needsCheck && !hasCommand && !hasScript) {
|
|
11695
|
+
errors.push({
|
|
11696
|
+
path: prefix,
|
|
11697
|
+
message: `${task.type} task must declare either checkCommand or checkScript`
|
|
11698
|
+
});
|
|
11699
|
+
}
|
|
11700
|
+
if (hasScript) {
|
|
11701
|
+
const path22 = task.checkScript?.path;
|
|
11702
|
+
if (!path22 || path22.trim().length === 0) {
|
|
11703
|
+
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
9425
11704
|
}
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
taskId: task.id,
|
|
9432
|
-
startedAt,
|
|
9433
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9434
|
-
status,
|
|
9435
|
-
findings,
|
|
9436
|
-
fixed: 0,
|
|
9437
|
-
prUrl: null,
|
|
9438
|
-
prUpdated: false
|
|
9439
|
-
};
|
|
9440
|
-
if (parsed?.error) {
|
|
9441
|
-
result.error = parsed.error;
|
|
11705
|
+
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
11706
|
+
errors.push({
|
|
11707
|
+
path: `${prefix}.checkScript.timeoutMs`,
|
|
11708
|
+
message: "timeoutMs must be a positive integer"
|
|
11709
|
+
});
|
|
9442
11710
|
}
|
|
9443
|
-
return result;
|
|
9444
11711
|
}
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9457
|
-
|
|
9458
|
-
|
|
9459
|
-
let stdout;
|
|
9460
|
-
try {
|
|
9461
|
-
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
9462
|
-
stdout = out.stdout ?? "";
|
|
9463
|
-
} catch (err) {
|
|
9464
|
-
return this.failureResult(task.id, startedAt, String(err));
|
|
11712
|
+
}
|
|
11713
|
+
function validateRequiredByType(prefix, task, errors) {
|
|
11714
|
+
const required = REQUIRED_FIELDS_BY_TYPE[task.type];
|
|
11715
|
+
if (!required) {
|
|
11716
|
+
errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
|
|
11717
|
+
return;
|
|
11718
|
+
}
|
|
11719
|
+
for (const field of required) {
|
|
11720
|
+
const value = task[field];
|
|
11721
|
+
if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
|
|
11722
|
+
errors.push({
|
|
11723
|
+
path: `${prefix}.${String(field)}`,
|
|
11724
|
+
message: `${task.type} task requires ${String(field)}`
|
|
11725
|
+
});
|
|
9465
11726
|
}
|
|
9466
|
-
const parsed = parseStatusLine(stdout);
|
|
9467
|
-
const status = parsed?.status ?? "success";
|
|
9468
|
-
const result = {
|
|
9469
|
-
taskId: task.id,
|
|
9470
|
-
startedAt,
|
|
9471
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9472
|
-
status,
|
|
9473
|
-
findings: 0,
|
|
9474
|
-
fixed: 0,
|
|
9475
|
-
prUrl: null,
|
|
9476
|
-
prUpdated: false
|
|
9477
|
-
};
|
|
9478
|
-
if (parsed?.error) result.error = parsed.error;
|
|
9479
|
-
return result;
|
|
9480
11727
|
}
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
|
|
9485
|
-
|
|
9486
|
-
const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
|
|
9487
|
-
if (taskOverride) return taskOverride;
|
|
9488
|
-
return this.config.aiBackend ?? "local";
|
|
11728
|
+
if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
|
|
11729
|
+
errors.push({
|
|
11730
|
+
path: `${prefix}.branch`,
|
|
11731
|
+
message: `${task.type} task requires a non-null branch`
|
|
11732
|
+
});
|
|
9489
11733
|
}
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
fixed: 0,
|
|
9498
|
-
prUrl: null,
|
|
9499
|
-
prUpdated: false,
|
|
9500
|
-
error
|
|
9501
|
-
};
|
|
11734
|
+
}
|
|
11735
|
+
function validateContextFrom(prefix, selfId, task, allIds, errors) {
|
|
11736
|
+
if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
|
|
11737
|
+
errors.push({
|
|
11738
|
+
path: `${prefix}.contextFromMaxAgeMinutes`,
|
|
11739
|
+
message: "contextFromMaxAgeMinutes must be a positive integer"
|
|
11740
|
+
});
|
|
9502
11741
|
}
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
}
|
|
9517
|
-
|
|
9518
|
-
parsed.error = obj.error;
|
|
9519
|
-
}
|
|
9520
|
-
if (typeof obj.reason === "string") {
|
|
9521
|
-
parsed.reason = obj.reason;
|
|
9522
|
-
}
|
|
9523
|
-
if (typeof obj.detail === "string" && !parsed.error) {
|
|
9524
|
-
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
9525
|
-
}
|
|
9526
|
-
return parsed;
|
|
9527
|
-
}
|
|
9528
|
-
if (s === "updated" || s === "no-op") {
|
|
9529
|
-
return { status: "success", rawStatus: s };
|
|
9530
|
-
}
|
|
9531
|
-
if (s === "error") {
|
|
9532
|
-
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
9533
|
-
return { status: "failure", error: message, rawStatus: "error" };
|
|
9534
|
-
}
|
|
9535
|
-
} catch {
|
|
11742
|
+
if (!task.contextFrom) return;
|
|
11743
|
+
for (let i = 0; i < task.contextFrom.length; i++) {
|
|
11744
|
+
const upstreamId = task.contextFrom[i];
|
|
11745
|
+
if (!upstreamId) continue;
|
|
11746
|
+
if (upstreamId === selfId) {
|
|
11747
|
+
errors.push({
|
|
11748
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
11749
|
+
message: `task '${selfId}' cannot reference itself in contextFrom`
|
|
11750
|
+
});
|
|
11751
|
+
}
|
|
11752
|
+
if (!allIds.has(upstreamId)) {
|
|
11753
|
+
errors.push({
|
|
11754
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
11755
|
+
message: `references unknown task '${upstreamId}'`
|
|
11756
|
+
});
|
|
9536
11757
|
}
|
|
9537
11758
|
}
|
|
9538
|
-
|
|
11759
|
+
}
|
|
11760
|
+
function validateInlineSkills(prefix, task, deps, errors) {
|
|
11761
|
+
if (!task.inlineSkills) return;
|
|
11762
|
+
if (!deps.skillExists) return;
|
|
11763
|
+
for (let i = 0; i < task.inlineSkills.length; i++) {
|
|
11764
|
+
const name = task.inlineSkills[i];
|
|
11765
|
+
if (!name) continue;
|
|
11766
|
+
if (!deps.skillExists(name)) {
|
|
11767
|
+
errors.push({
|
|
11768
|
+
path: `${prefix}.inlineSkills[${i}]`,
|
|
11769
|
+
message: `skill '${name}' not found in the registry`
|
|
11770
|
+
});
|
|
11771
|
+
}
|
|
11772
|
+
}
|
|
11773
|
+
if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
|
|
11774
|
+
errors.push({
|
|
11775
|
+
path: `${prefix}.inlineSkillsBudgetTokens`,
|
|
11776
|
+
message: "inlineSkillsBudgetTokens must be a positive integer"
|
|
11777
|
+
});
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
11780
|
+
function validateScriptPath(prefix, task, deps, errors) {
|
|
11781
|
+
if (!task.checkScript?.path) return;
|
|
11782
|
+
if (!deps.scriptExists) return;
|
|
11783
|
+
if (!deps.scriptExists(task.checkScript.path)) {
|
|
11784
|
+
errors.push({
|
|
11785
|
+
path: `${prefix}.checkScript.path`,
|
|
11786
|
+
message: `executable not found: ${task.checkScript.path}`
|
|
11787
|
+
});
|
|
11788
|
+
}
|
|
11789
|
+
}
|
|
11790
|
+
function detectCycles(customTasks, builtIns, errors) {
|
|
11791
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
11792
|
+
for (const t of builtIns) adjacency.set(t.id, []);
|
|
11793
|
+
for (const [id, task] of Object.entries(customTasks)) {
|
|
11794
|
+
adjacency.set(id, (task.contextFrom ?? []).slice());
|
|
11795
|
+
}
|
|
11796
|
+
const color = /* @__PURE__ */ new Map();
|
|
11797
|
+
for (const id of adjacency.keys()) color.set(id, "white");
|
|
11798
|
+
const reported = /* @__PURE__ */ new Set();
|
|
11799
|
+
for (const id of Object.keys(customTasks)) {
|
|
11800
|
+
if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11803
|
+
function visitFromRoot(start, adjacency, color, errors, reported) {
|
|
11804
|
+
const stack = [{ id: start, nextIdx: 0, path: [start] }];
|
|
11805
|
+
color.set(start, "grey");
|
|
11806
|
+
while (stack.length) {
|
|
11807
|
+
const top = stack[stack.length - 1];
|
|
11808
|
+
const neighbors = adjacency.get(top.id) ?? [];
|
|
11809
|
+
if (top.nextIdx >= neighbors.length) {
|
|
11810
|
+
color.set(top.id, "black");
|
|
11811
|
+
stack.pop();
|
|
11812
|
+
continue;
|
|
11813
|
+
}
|
|
11814
|
+
const next = neighbors[top.nextIdx++];
|
|
11815
|
+
if (!next || !adjacency.has(next)) continue;
|
|
11816
|
+
handleEdge(top, next, color, stack, errors, reported);
|
|
11817
|
+
}
|
|
11818
|
+
}
|
|
11819
|
+
function handleEdge(top, next, color, stack, errors, reported) {
|
|
11820
|
+
const nextColor = color.get(next);
|
|
11821
|
+
if (nextColor === "grey") {
|
|
11822
|
+
reportCycle(top.path, next, errors, reported);
|
|
11823
|
+
} else if (nextColor === "white") {
|
|
11824
|
+
color.set(next, "grey");
|
|
11825
|
+
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
11826
|
+
}
|
|
11827
|
+
}
|
|
11828
|
+
function reportCycle(path22, next, errors, reported) {
|
|
11829
|
+
const cycleStart = path22.indexOf(next);
|
|
11830
|
+
const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
|
|
11831
|
+
const key = cyclePath.join("\u2192");
|
|
11832
|
+
if (reported.has(key)) return;
|
|
11833
|
+
reported.add(key);
|
|
11834
|
+
errors.push({
|
|
11835
|
+
path: `customTasks.${cyclePath[0]}.contextFrom`,
|
|
11836
|
+
message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
|
|
11837
|
+
});
|
|
9539
11838
|
}
|
|
9540
11839
|
|
|
9541
11840
|
// src/orchestrator.ts
|
|
@@ -9625,13 +11924,20 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9625
11924
|
cacheMetrics;
|
|
9626
11925
|
otlpExporter;
|
|
9627
11926
|
telemetryFanoutOff;
|
|
11927
|
+
// Hermes Phase 3: in-process notification sinks subscribe to the same
|
|
11928
|
+
// event bus (`this`) that webhook fanout uses, applying envelope
|
|
11929
|
+
// formatting before delivering to Slack/etc. The registry + unwire
|
|
11930
|
+
// handle are kept on the instance so stop() can detach listeners and
|
|
11931
|
+
// call adapter dispose() in deterministic order.
|
|
11932
|
+
notificationsRegistry;
|
|
11933
|
+
notificationFanoutOff;
|
|
9628
11934
|
orchestratorIdPromise;
|
|
9629
11935
|
recorder;
|
|
9630
11936
|
intelligenceRunner;
|
|
9631
11937
|
completionHandler;
|
|
9632
11938
|
/** Project root directory, derived from workspace root. */
|
|
9633
11939
|
get projectRoot() {
|
|
9634
|
-
return
|
|
11940
|
+
return path19.resolve(this.config.workspace.root, "..", "..");
|
|
9635
11941
|
}
|
|
9636
11942
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
9637
11943
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -9686,10 +11992,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9686
11992
|
this.renderer = new PromptRenderer();
|
|
9687
11993
|
this.overrideBackend = overrides?.backend ?? null;
|
|
9688
11994
|
this.interactionQueue = new InteractionQueue(
|
|
9689
|
-
|
|
11995
|
+
path19.join(config.workspace.root, "..", "interactions"),
|
|
9690
11996
|
this
|
|
9691
11997
|
);
|
|
9692
|
-
this.analysisArchive = new AnalysisArchive(
|
|
11998
|
+
this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
|
|
9693
11999
|
const backendsMap = this.config.agent.backends ?? {};
|
|
9694
12000
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
9695
12001
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -9733,7 +12039,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9733
12039
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
9734
12040
|
});
|
|
9735
12041
|
this.recorder = new StreamRecorder(
|
|
9736
|
-
|
|
12042
|
+
path19.resolve(config.workspace.root, "..", "streams"),
|
|
9737
12043
|
this.logger
|
|
9738
12044
|
);
|
|
9739
12045
|
const self = this;
|
|
@@ -9764,10 +12070,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9764
12070
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
9765
12071
|
if (config.server?.port) {
|
|
9766
12072
|
const webhookStore = new WebhookStore(
|
|
9767
|
-
|
|
12073
|
+
path19.join(this.projectRoot, ".harness", "webhooks.json")
|
|
9768
12074
|
);
|
|
9769
12075
|
this.webhookQueue = new WebhookQueue(
|
|
9770
|
-
|
|
12076
|
+
path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
9771
12077
|
);
|
|
9772
12078
|
const webhookDelivery = new WebhookDelivery({
|
|
9773
12079
|
queue: this.webhookQueue,
|
|
@@ -9780,6 +12086,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9780
12086
|
delivery: webhookDelivery
|
|
9781
12087
|
});
|
|
9782
12088
|
webhookDelivery.start();
|
|
12089
|
+
this.setupNotifications(config.notifications);
|
|
9783
12090
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
9784
12091
|
if (otlpCfg) {
|
|
9785
12092
|
this.otlpExporter = new OTLPExporter({
|
|
@@ -9804,7 +12111,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9804
12111
|
queue: this.webhookQueue
|
|
9805
12112
|
},
|
|
9806
12113
|
cacheMetrics: this.cacheMetrics,
|
|
9807
|
-
plansDir:
|
|
12114
|
+
plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
9808
12115
|
pipeline: this.pipeline,
|
|
9809
12116
|
analysisArchive: this.analysisArchive,
|
|
9810
12117
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -9860,13 +12167,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9860
12167
|
const logger = this.logger;
|
|
9861
12168
|
const checkRunner = {
|
|
9862
12169
|
run: async (command, cwd) => {
|
|
9863
|
-
const { execFile:
|
|
9864
|
-
const { promisify:
|
|
9865
|
-
const
|
|
12170
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12171
|
+
const { promisify: promisify5 } = await import("util");
|
|
12172
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
9866
12173
|
const [cmd, ...args] = command;
|
|
9867
12174
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
9868
12175
|
try {
|
|
9869
|
-
const { stdout } = await
|
|
12176
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
9870
12177
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
9871
12178
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
9872
12179
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -9895,13 +12202,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9895
12202
|
};
|
|
9896
12203
|
const commandExecutor = {
|
|
9897
12204
|
exec: async (command, cwd) => {
|
|
9898
|
-
const { execFile:
|
|
9899
|
-
const { promisify:
|
|
9900
|
-
const
|
|
12205
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12206
|
+
const { promisify: promisify5 } = await import("util");
|
|
12207
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
9901
12208
|
const [cmd, ...args] = command;
|
|
9902
12209
|
if (!cmd) return { stdout: "" };
|
|
9903
12210
|
try {
|
|
9904
|
-
const { stdout } = await
|
|
12211
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
9905
12212
|
return { stdout: String(stdout) };
|
|
9906
12213
|
} catch (err) {
|
|
9907
12214
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -9913,12 +12220,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9913
12220
|
}
|
|
9914
12221
|
}
|
|
9915
12222
|
};
|
|
12223
|
+
const outputStore = new TaskOutputStore({
|
|
12224
|
+
rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
12225
|
+
logger: this.logger
|
|
12226
|
+
});
|
|
12227
|
+
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
12228
|
+
const skillReader = {
|
|
12229
|
+
// The orchestrator does not own the skill registry; CLI-side skill
|
|
12230
|
+
// resolution wires this in via direct injection. Default: skill not
|
|
12231
|
+
// resolvable from the orchestrator boundary.
|
|
12232
|
+
read: async () => null
|
|
12233
|
+
};
|
|
12234
|
+
const contextResolver = new ContextResolver({
|
|
12235
|
+
outputStore,
|
|
12236
|
+
skillReader,
|
|
12237
|
+
logger: this.logger
|
|
12238
|
+
});
|
|
9916
12239
|
return new TaskRunner({
|
|
9917
12240
|
config: maintenanceConfig,
|
|
9918
12241
|
checkRunner,
|
|
9919
12242
|
agentDispatcher,
|
|
9920
12243
|
commandExecutor,
|
|
9921
|
-
cwd: this.projectRoot
|
|
12244
|
+
cwd: this.projectRoot,
|
|
12245
|
+
checkScriptRunner,
|
|
12246
|
+
contextResolver,
|
|
12247
|
+
outputStore
|
|
9922
12248
|
});
|
|
9923
12249
|
}
|
|
9924
12250
|
/**
|
|
@@ -9926,8 +12252,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
9926
12252
|
* Extracted from start() to keep function length under threshold.
|
|
9927
12253
|
*/
|
|
9928
12254
|
async initMaintenance(maintenanceConfig) {
|
|
12255
|
+
const validation = validateCustomTasks(
|
|
12256
|
+
maintenanceConfig.customTasks,
|
|
12257
|
+
BUILT_IN_TASKS
|
|
12258
|
+
);
|
|
12259
|
+
if (!validation.ok) {
|
|
12260
|
+
const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
12261
|
+
throw new Error(`Invalid maintenance.customTasks configuration:
|
|
12262
|
+
${messages}`);
|
|
12263
|
+
}
|
|
9929
12264
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
9930
|
-
persistDir:
|
|
12265
|
+
persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
9931
12266
|
logger: this.logger
|
|
9932
12267
|
});
|
|
9933
12268
|
await this.maintenanceReporter.load();
|
|
@@ -10615,6 +12950,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10615
12950
|
);
|
|
10616
12951
|
this.emit("state_change", this.getSnapshot());
|
|
10617
12952
|
}
|
|
12953
|
+
/**
|
|
12954
|
+
* Hermes Phase 3: wire in-process notification sinks against the
|
|
12955
|
+
* orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
|
|
12956
|
+
* missing env var) logs + skips rather than breaking startup — the
|
|
12957
|
+
* hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
|
|
12958
|
+
* to the same topics as `wireWebhookFanout`; a slow Slack call cannot
|
|
12959
|
+
* block webhook delivery because the two paths fan out independently.
|
|
12960
|
+
*/
|
|
12961
|
+
setupNotifications(notifConfig) {
|
|
12962
|
+
if (!notifConfig || !notifConfig.sinks || notifConfig.sinks.length === 0) return;
|
|
12963
|
+
try {
|
|
12964
|
+
this.notificationsRegistry = SinkRegistry.fromConfig(notifConfig, {
|
|
12965
|
+
env: process.env
|
|
12966
|
+
});
|
|
12967
|
+
this.notificationFanoutOff = wireNotificationSinks({
|
|
12968
|
+
bus: this,
|
|
12969
|
+
registry: this.notificationsRegistry
|
|
12970
|
+
});
|
|
12971
|
+
} catch (err) {
|
|
12972
|
+
this.logger.warn(
|
|
12973
|
+
`notifications sink registry failed: ${err instanceof Error ? err.message : String(err)}; sinks disabled`
|
|
12974
|
+
);
|
|
12975
|
+
delete this.notificationsRegistry;
|
|
12976
|
+
}
|
|
12977
|
+
}
|
|
10618
12978
|
/**
|
|
10619
12979
|
* Stops execution for a specific issue.
|
|
10620
12980
|
*
|
|
@@ -10782,6 +13142,14 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10782
13142
|
this.webhookFanoutOff();
|
|
10783
13143
|
delete this.webhookFanoutOff;
|
|
10784
13144
|
}
|
|
13145
|
+
if (this.notificationFanoutOff) {
|
|
13146
|
+
this.notificationFanoutOff();
|
|
13147
|
+
delete this.notificationFanoutOff;
|
|
13148
|
+
}
|
|
13149
|
+
if (this.notificationsRegistry) {
|
|
13150
|
+
await this.notificationsRegistry.dispose();
|
|
13151
|
+
delete this.notificationsRegistry;
|
|
13152
|
+
}
|
|
10785
13153
|
if (this.telemetryFanoutOff) {
|
|
10786
13154
|
this.telemetryFanoutOff();
|
|
10787
13155
|
delete this.telemetryFanoutOff;
|
|
@@ -11071,10 +13439,10 @@ function launchTUI(orchestrator) {
|
|
|
11071
13439
|
|
|
11072
13440
|
// src/maintenance/sync-main.ts
|
|
11073
13441
|
import { execFile as nodeExecFile } from "child_process";
|
|
11074
|
-
import { promisify as
|
|
11075
|
-
var
|
|
13442
|
+
import { promisify as promisify4 } from "util";
|
|
13443
|
+
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
11076
13444
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
11077
|
-
const exec =
|
|
13445
|
+
const exec = promisify4(execFileFn);
|
|
11078
13446
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
11079
13447
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
11080
13448
|
}
|
|
@@ -11137,7 +13505,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
11137
13505
|
}
|
|
11138
13506
|
async function syncMain(repoRoot, opts = {}) {
|
|
11139
13507
|
const execFileFn = opts.execFileFn ?? nodeExecFile;
|
|
11140
|
-
const timeoutMs = opts.timeoutMs ??
|
|
13508
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
11141
13509
|
try {
|
|
11142
13510
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
11143
13511
|
if (!originRef) {
|
|
@@ -11212,10 +13580,472 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11212
13580
|
};
|
|
11213
13581
|
}
|
|
11214
13582
|
}
|
|
13583
|
+
|
|
13584
|
+
// src/sessions/search-index.ts
|
|
13585
|
+
import * as fs17 from "fs";
|
|
13586
|
+
import * as path20 from "path";
|
|
13587
|
+
import Database2 from "better-sqlite3";
|
|
13588
|
+
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
13589
|
+
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
13590
|
+
var SCHEMA_SQL2 = `
|
|
13591
|
+
CREATE TABLE IF NOT EXISTS session_docs (
|
|
13592
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13593
|
+
session_id TEXT NOT NULL,
|
|
13594
|
+
archived INTEGER NOT NULL,
|
|
13595
|
+
file_kind TEXT NOT NULL,
|
|
13596
|
+
path TEXT NOT NULL,
|
|
13597
|
+
mtime_ms INTEGER NOT NULL,
|
|
13598
|
+
body TEXT NOT NULL,
|
|
13599
|
+
UNIQUE (session_id, archived, file_kind)
|
|
13600
|
+
);
|
|
13601
|
+
|
|
13602
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_docs_fts USING fts5 (
|
|
13603
|
+
body,
|
|
13604
|
+
content='session_docs',
|
|
13605
|
+
content_rowid='id',
|
|
13606
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
13607
|
+
);
|
|
13608
|
+
|
|
13609
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ai
|
|
13610
|
+
AFTER INSERT ON session_docs
|
|
13611
|
+
BEGIN INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body); END;
|
|
13612
|
+
|
|
13613
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ad
|
|
13614
|
+
AFTER DELETE ON session_docs
|
|
13615
|
+
BEGIN INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body); END;
|
|
13616
|
+
|
|
13617
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_au
|
|
13618
|
+
AFTER UPDATE ON session_docs
|
|
13619
|
+
BEGIN
|
|
13620
|
+
INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body);
|
|
13621
|
+
INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body);
|
|
13622
|
+
END;
|
|
13623
|
+
`;
|
|
13624
|
+
var DEFAULT_LIMIT = 20;
|
|
13625
|
+
function normalizeFts5Query(query) {
|
|
13626
|
+
const advancedSyntax = /["()*^+]|\bAND\b|\bOR\b|\bNOT\b|[A-Za-z_]+:/;
|
|
13627
|
+
if (advancedSyntax.test(query)) return query;
|
|
13628
|
+
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
13629
|
+
}
|
|
13630
|
+
function searchIndexPath(projectPath) {
|
|
13631
|
+
return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
13632
|
+
}
|
|
13633
|
+
var FILE_KIND_TO_FILENAME = {
|
|
13634
|
+
summary: "summary.md",
|
|
13635
|
+
learnings: "learnings.md",
|
|
13636
|
+
failures: "failures.md",
|
|
13637
|
+
sections: "session-sections.md",
|
|
13638
|
+
llm_summary: "llm-summary.md"
|
|
13639
|
+
};
|
|
13640
|
+
var SqliteSearchIndex = class {
|
|
13641
|
+
db;
|
|
13642
|
+
upsertStmt;
|
|
13643
|
+
removeSessionStmt;
|
|
13644
|
+
totalStmt;
|
|
13645
|
+
constructor(dbPath) {
|
|
13646
|
+
fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
|
|
13647
|
+
this.db = new Database2(dbPath);
|
|
13648
|
+
this.db.pragma("journal_mode = WAL");
|
|
13649
|
+
this.db.pragma("synchronous = NORMAL");
|
|
13650
|
+
this.db.exec(SCHEMA_SQL2);
|
|
13651
|
+
this.upsertStmt = this.db.prepare(
|
|
13652
|
+
`INSERT INTO session_docs (session_id, archived, file_kind, path, mtime_ms, body)
|
|
13653
|
+
VALUES (@sessionId, @archived, @fileKind, @path, @mtimeMs, @body)
|
|
13654
|
+
ON CONFLICT(session_id, archived, file_kind) DO UPDATE SET
|
|
13655
|
+
path = excluded.path,
|
|
13656
|
+
mtime_ms = excluded.mtime_ms,
|
|
13657
|
+
body = excluded.body`
|
|
13658
|
+
);
|
|
13659
|
+
this.removeSessionStmt = this.db.prepare(`DELETE FROM session_docs WHERE session_id = ?`);
|
|
13660
|
+
this.totalStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM session_docs`);
|
|
13661
|
+
}
|
|
13662
|
+
upsertSessionDoc(doc) {
|
|
13663
|
+
this.upsertStmt.run({
|
|
13664
|
+
sessionId: doc.sessionId,
|
|
13665
|
+
archived: doc.archived ? 1 : 0,
|
|
13666
|
+
fileKind: doc.fileKind,
|
|
13667
|
+
path: doc.path,
|
|
13668
|
+
mtimeMs: Math.floor(doc.mtimeMs),
|
|
13669
|
+
body: doc.body
|
|
13670
|
+
});
|
|
13671
|
+
}
|
|
13672
|
+
removeSession(sessionId) {
|
|
13673
|
+
const info = this.removeSessionStmt.run(sessionId);
|
|
13674
|
+
return info.changes;
|
|
13675
|
+
}
|
|
13676
|
+
/**
|
|
13677
|
+
* Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
|
|
13678
|
+
* re-walk. Live (archived=0) rows are preserved.
|
|
13679
|
+
*/
|
|
13680
|
+
resetArchived() {
|
|
13681
|
+
this.db.prepare(`DELETE FROM session_docs WHERE archived = 1`).run();
|
|
13682
|
+
}
|
|
13683
|
+
/** Total rows currently indexed (across both live and archived). */
|
|
13684
|
+
totalIndexed() {
|
|
13685
|
+
const row = this.totalStmt.get();
|
|
13686
|
+
return row.n;
|
|
13687
|
+
}
|
|
13688
|
+
/**
|
|
13689
|
+
* Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
|
|
13690
|
+
* FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
|
|
13691
|
+
* is therefore the user-facing language. Errors from malformed queries
|
|
13692
|
+
* surface as thrown `SqliteError` so the CLI can catch + render them.
|
|
13693
|
+
*/
|
|
13694
|
+
search(query, opts = {}) {
|
|
13695
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
13696
|
+
const filters = [];
|
|
13697
|
+
const params = { q: normalizeFts5Query(query), limit };
|
|
13698
|
+
if (opts.archivedOnly) {
|
|
13699
|
+
filters.push("d.archived = 1");
|
|
13700
|
+
}
|
|
13701
|
+
const fileKinds = opts.fileKinds && opts.fileKinds.length > 0 ? opts.fileKinds : null;
|
|
13702
|
+
if (fileKinds) {
|
|
13703
|
+
const placeholders = fileKinds.map((_, i) => `@fk${i}`).join(", ");
|
|
13704
|
+
filters.push(`d.file_kind IN (${placeholders})`);
|
|
13705
|
+
fileKinds.forEach((k, i) => {
|
|
13706
|
+
params[`fk${i}`] = k;
|
|
13707
|
+
});
|
|
13708
|
+
}
|
|
13709
|
+
const whereClause = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
|
|
13710
|
+
const sql = `
|
|
13711
|
+
SELECT
|
|
13712
|
+
d.session_id AS sessionId,
|
|
13713
|
+
d.archived AS archived,
|
|
13714
|
+
d.file_kind AS fileKind,
|
|
13715
|
+
d.path AS path,
|
|
13716
|
+
bm25(session_docs_fts) AS bm25,
|
|
13717
|
+
snippet(session_docs_fts, 0, '\u2026', '\u2026', '\u2026', 16) AS snippet
|
|
13718
|
+
FROM session_docs_fts
|
|
13719
|
+
JOIN session_docs d ON d.id = session_docs_fts.rowid
|
|
13720
|
+
WHERE session_docs_fts MATCH @q
|
|
13721
|
+
${whereClause}
|
|
13722
|
+
ORDER BY bm25 ASC
|
|
13723
|
+
LIMIT @limit
|
|
13724
|
+
`;
|
|
13725
|
+
const start = Date.now();
|
|
13726
|
+
const rows = this.db.prepare(sql).all(params);
|
|
13727
|
+
const durationMs = Date.now() - start;
|
|
13728
|
+
const matches = rows.map((r) => ({
|
|
13729
|
+
sessionId: r.sessionId,
|
|
13730
|
+
archived: r.archived === 1,
|
|
13731
|
+
fileKind: r.fileKind,
|
|
13732
|
+
path: r.path,
|
|
13733
|
+
bm25: r.bm25,
|
|
13734
|
+
snippet: r.snippet
|
|
13735
|
+
}));
|
|
13736
|
+
return { matches, durationMs, totalIndexed: this.totalIndexed() };
|
|
13737
|
+
}
|
|
13738
|
+
close() {
|
|
13739
|
+
this.db.close();
|
|
13740
|
+
}
|
|
13741
|
+
};
|
|
13742
|
+
function openSearchIndex(projectPath) {
|
|
13743
|
+
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
13744
|
+
}
|
|
13745
|
+
function indexSessionDirectory(idx, args) {
|
|
13746
|
+
const kinds = args.fileKinds ?? [...INDEXED_FILE_KINDS];
|
|
13747
|
+
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
13748
|
+
let docsWritten = 0;
|
|
13749
|
+
for (const kind of kinds) {
|
|
13750
|
+
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
13751
|
+
const filePath = path20.join(args.sessionDir, fileName);
|
|
13752
|
+
if (!fs17.existsSync(filePath)) continue;
|
|
13753
|
+
let body = fs17.readFileSync(filePath, "utf8");
|
|
13754
|
+
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
13755
|
+
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
13756
|
+
}
|
|
13757
|
+
const stat = fs17.statSync(filePath);
|
|
13758
|
+
const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
13759
|
+
idx.upsertSessionDoc({
|
|
13760
|
+
sessionId: args.sessionId,
|
|
13761
|
+
archived: args.archived,
|
|
13762
|
+
fileKind: kind,
|
|
13763
|
+
path: relPath,
|
|
13764
|
+
mtimeMs: stat.mtimeMs,
|
|
13765
|
+
body
|
|
13766
|
+
});
|
|
13767
|
+
docsWritten++;
|
|
13768
|
+
}
|
|
13769
|
+
return { docsWritten };
|
|
13770
|
+
}
|
|
13771
|
+
function reindexFromArchive(projectPath, opts = {}) {
|
|
13772
|
+
const start = Date.now();
|
|
13773
|
+
const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
|
|
13774
|
+
const idx = openSearchIndex(projectPath);
|
|
13775
|
+
try {
|
|
13776
|
+
idx.resetArchived();
|
|
13777
|
+
let sessionsIndexed = 0;
|
|
13778
|
+
let docsWritten = 0;
|
|
13779
|
+
if (fs17.existsSync(archiveBase)) {
|
|
13780
|
+
const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
|
|
13781
|
+
for (const entry of entries) {
|
|
13782
|
+
if (!entry.isDirectory()) continue;
|
|
13783
|
+
const sessionDir = path20.join(archiveBase, entry.name);
|
|
13784
|
+
const result = indexSessionDirectory(idx, {
|
|
13785
|
+
sessionId: entry.name,
|
|
13786
|
+
sessionDir,
|
|
13787
|
+
archived: true,
|
|
13788
|
+
projectPath,
|
|
13789
|
+
...opts.fileKinds && { fileKinds: opts.fileKinds },
|
|
13790
|
+
...opts.maxBytesPerBody !== void 0 && { maxBytesPerBody: opts.maxBytesPerBody }
|
|
13791
|
+
});
|
|
13792
|
+
if (result.docsWritten > 0) sessionsIndexed++;
|
|
13793
|
+
docsWritten += result.docsWritten;
|
|
13794
|
+
}
|
|
13795
|
+
}
|
|
13796
|
+
return { sessionsIndexed, docsWritten, durationMs: Date.now() - start };
|
|
13797
|
+
} finally {
|
|
13798
|
+
idx.close();
|
|
13799
|
+
}
|
|
13800
|
+
}
|
|
13801
|
+
|
|
13802
|
+
// src/sessions/summarize.ts
|
|
13803
|
+
import * as fs18 from "fs";
|
|
13804
|
+
import * as path21 from "path";
|
|
13805
|
+
import {
|
|
13806
|
+
SessionSummarySchema
|
|
13807
|
+
} from "@harness-engineering/types";
|
|
13808
|
+
import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
|
|
13809
|
+
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
13810
|
+
var SUMMARY_INPUT_FILES = [
|
|
13811
|
+
{ filename: "summary.md", kind: "summary" },
|
|
13812
|
+
{ filename: "learnings.md", kind: "learnings" },
|
|
13813
|
+
{ filename: "failures.md", kind: "failures" },
|
|
13814
|
+
{ filename: "session-sections.md", kind: "sections" }
|
|
13815
|
+
];
|
|
13816
|
+
var DEFAULT_INPUT_BUDGET_TOKENS = 16e3;
|
|
13817
|
+
var DEFAULT_TIMEOUT_MS4 = 6e4;
|
|
13818
|
+
var CHARS_PER_TOKEN = 4;
|
|
13819
|
+
var SYSTEM_PROMPT = `You produce concise, structured retrospectives of completed harness-engineering sessions.
|
|
13820
|
+
|
|
13821
|
+
Read the session's archived markdown files and emit a JSON object that conforms exactly to the provided schema. Be specific and grounded \u2014 quote artefacts (file names, skill names, error messages) verbatim when relevant. Do not invent. If a field has no content, return an empty array.`;
|
|
13822
|
+
var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-engineering session. Produce a structured summary capturing:
|
|
13823
|
+
- headline: one-sentence retrospective (\u2264 120 chars)
|
|
13824
|
+
- keyOutcomes: concrete things that shipped / decisions made (\u2264 20 strings)
|
|
13825
|
+
- openQuestions: items still open (\u2264 20 strings)
|
|
13826
|
+
- relatedSessions: other session slugs referenced (may be empty)
|
|
13827
|
+
|
|
13828
|
+
---
|
|
13829
|
+
|
|
13830
|
+
`;
|
|
13831
|
+
function readInputCorpus(archiveDir) {
|
|
13832
|
+
const parts = [];
|
|
13833
|
+
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
13834
|
+
const p = path21.join(archiveDir, filename);
|
|
13835
|
+
if (!fs18.existsSync(p)) continue;
|
|
13836
|
+
try {
|
|
13837
|
+
const content = fs18.readFileSync(p, "utf8");
|
|
13838
|
+
if (content.trim().length === 0) continue;
|
|
13839
|
+
parts.push(`## FILE: ${kind}
|
|
13840
|
+
|
|
13841
|
+
${content.trim()}`);
|
|
13842
|
+
} catch {
|
|
13843
|
+
}
|
|
13844
|
+
}
|
|
13845
|
+
return parts.join("\n\n");
|
|
13846
|
+
}
|
|
13847
|
+
function truncateForBudget(text, inputBudgetTokens) {
|
|
13848
|
+
const cap = Math.max(0, inputBudgetTokens * CHARS_PER_TOKEN);
|
|
13849
|
+
if (text.length <= cap) return text;
|
|
13850
|
+
return text.slice(0, cap) + "\n\n[TRUNCATED \u2014 input exceeded token budget]";
|
|
13851
|
+
}
|
|
13852
|
+
function renderLlmSummaryMarkdown(summary, meta) {
|
|
13853
|
+
const lines = [
|
|
13854
|
+
"---",
|
|
13855
|
+
`generatedAt: ${meta.generatedAt}`,
|
|
13856
|
+
`model: ${meta.model}`,
|
|
13857
|
+
`inputTokens: ${meta.inputTokens}`,
|
|
13858
|
+
`outputTokens: ${meta.outputTokens}`,
|
|
13859
|
+
`schemaVersion: ${meta.schemaVersion}`,
|
|
13860
|
+
"---",
|
|
13861
|
+
"",
|
|
13862
|
+
"## Headline",
|
|
13863
|
+
summary.headline,
|
|
13864
|
+
"",
|
|
13865
|
+
"## Key outcomes"
|
|
13866
|
+
];
|
|
13867
|
+
if (summary.keyOutcomes.length === 0) {
|
|
13868
|
+
lines.push("_(none)_");
|
|
13869
|
+
} else {
|
|
13870
|
+
for (const item of summary.keyOutcomes) lines.push(`- ${item}`);
|
|
13871
|
+
}
|
|
13872
|
+
lines.push("", "## Open questions");
|
|
13873
|
+
if (summary.openQuestions.length === 0) {
|
|
13874
|
+
lines.push("_(none)_");
|
|
13875
|
+
} else {
|
|
13876
|
+
for (const item of summary.openQuestions) lines.push(`- ${item}`);
|
|
13877
|
+
}
|
|
13878
|
+
lines.push("", "## Related sessions");
|
|
13879
|
+
if (summary.relatedSessions.length === 0) {
|
|
13880
|
+
lines.push("_(none)_");
|
|
13881
|
+
} else {
|
|
13882
|
+
for (const item of summary.relatedSessions) lines.push(`- ${item}`);
|
|
13883
|
+
}
|
|
13884
|
+
lines.push("");
|
|
13885
|
+
return lines.join("\n");
|
|
13886
|
+
}
|
|
13887
|
+
function writeStubMarkdown(archiveDir, reason) {
|
|
13888
|
+
const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
|
|
13889
|
+
const body = `---
|
|
13890
|
+
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13891
|
+
schemaVersion: 1
|
|
13892
|
+
status: failed
|
|
13893
|
+
---
|
|
13894
|
+
|
|
13895
|
+
## Summary unavailable
|
|
13896
|
+
|
|
13897
|
+
- reason: ${reason}
|
|
13898
|
+
`;
|
|
13899
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13900
|
+
return filePath;
|
|
13901
|
+
}
|
|
13902
|
+
async function summarizeArchivedSession(ctx) {
|
|
13903
|
+
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
13904
|
+
if (!fs18.existsSync(ctx.archiveDir)) {
|
|
13905
|
+
return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
13906
|
+
}
|
|
13907
|
+
const corpus = readInputCorpus(ctx.archiveDir);
|
|
13908
|
+
if (corpus.trim().length === 0) {
|
|
13909
|
+
return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
13910
|
+
}
|
|
13911
|
+
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
13912
|
+
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
13913
|
+
const prompt = USER_PROMPT_PREAMBLE + truncated;
|
|
13914
|
+
const timeoutMs = ctx.config?.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
13915
|
+
const analyzeOpts = {
|
|
13916
|
+
prompt,
|
|
13917
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
13918
|
+
responseSchema: SessionSummarySchema,
|
|
13919
|
+
...ctx.config?.model && { model: ctx.config.model }
|
|
13920
|
+
};
|
|
13921
|
+
let response;
|
|
13922
|
+
try {
|
|
13923
|
+
response = await Promise.race([
|
|
13924
|
+
ctx.provider.analyze(analyzeOpts),
|
|
13925
|
+
new Promise(
|
|
13926
|
+
(_, reject) => setTimeout(
|
|
13927
|
+
() => reject(new Error(`provider call timed out after ${timeoutMs}ms`)),
|
|
13928
|
+
timeoutMs
|
|
13929
|
+
)
|
|
13930
|
+
)
|
|
13931
|
+
]);
|
|
13932
|
+
} catch (e) {
|
|
13933
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
13934
|
+
ctx.logger?.warn?.("session summary: provider call failed", { reason });
|
|
13935
|
+
let stubPath;
|
|
13936
|
+
if (writeStubOnError) {
|
|
13937
|
+
try {
|
|
13938
|
+
stubPath = writeStubMarkdown(ctx.archiveDir, reason);
|
|
13939
|
+
} catch {
|
|
13940
|
+
}
|
|
13941
|
+
}
|
|
13942
|
+
return Err21(
|
|
13943
|
+
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
13944
|
+
);
|
|
13945
|
+
}
|
|
13946
|
+
const parsed = SessionSummarySchema.safeParse(response.result);
|
|
13947
|
+
if (!parsed.success) {
|
|
13948
|
+
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
13949
|
+
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
13950
|
+
if (writeStubOnError) {
|
|
13951
|
+
try {
|
|
13952
|
+
writeStubMarkdown(ctx.archiveDir, reason);
|
|
13953
|
+
} catch {
|
|
13954
|
+
}
|
|
13955
|
+
}
|
|
13956
|
+
return Err21(new Error(reason));
|
|
13957
|
+
}
|
|
13958
|
+
const meta = {
|
|
13959
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13960
|
+
model: response.model,
|
|
13961
|
+
inputTokens: response.tokenUsage.inputTokens,
|
|
13962
|
+
outputTokens: response.tokenUsage.outputTokens,
|
|
13963
|
+
schemaVersion: 1
|
|
13964
|
+
};
|
|
13965
|
+
const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
13966
|
+
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
13967
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13968
|
+
return Ok24({ summary: parsed.data, meta, filePath });
|
|
13969
|
+
}
|
|
13970
|
+
function isSummaryEnabled(config) {
|
|
13971
|
+
if (!config) return false;
|
|
13972
|
+
if (config.enabled === false) return false;
|
|
13973
|
+
return true;
|
|
13974
|
+
}
|
|
13975
|
+
|
|
13976
|
+
// src/sessions/archive-hooks.ts
|
|
13977
|
+
var defaultLogger = {
|
|
13978
|
+
warn: (msg, meta) => console.warn(`[sessions] ${msg}`, meta)
|
|
13979
|
+
};
|
|
13980
|
+
async function runSummaryStep(opts, logger, sessionId, archiveDir) {
|
|
13981
|
+
const enabled = isSummaryEnabled(opts.config?.summary) && opts.provider != null;
|
|
13982
|
+
if (!enabled || !opts.provider) return;
|
|
13983
|
+
const ctx = {
|
|
13984
|
+
archiveDir,
|
|
13985
|
+
provider: opts.provider,
|
|
13986
|
+
...opts.config?.summary && { config: opts.config.summary },
|
|
13987
|
+
...logger && { logger }
|
|
13988
|
+
};
|
|
13989
|
+
try {
|
|
13990
|
+
const result = await summarizeArchivedSession(ctx);
|
|
13991
|
+
if (!result.ok) {
|
|
13992
|
+
logger.warn?.("session summary: failed", {
|
|
13993
|
+
sessionId,
|
|
13994
|
+
error: result.error.message
|
|
13995
|
+
});
|
|
13996
|
+
}
|
|
13997
|
+
} catch (e) {
|
|
13998
|
+
logger.warn?.("session summary: threw", {
|
|
13999
|
+
sessionId,
|
|
14000
|
+
error: e instanceof Error ? e.message : String(e)
|
|
14001
|
+
});
|
|
14002
|
+
}
|
|
14003
|
+
}
|
|
14004
|
+
function runIndexStep(opts, logger, sessionId, archiveDir) {
|
|
14005
|
+
try {
|
|
14006
|
+
const idx = openSearchIndex(opts.projectPath);
|
|
14007
|
+
try {
|
|
14008
|
+
const result = indexSessionDirectory(idx, {
|
|
14009
|
+
sessionId,
|
|
14010
|
+
sessionDir: archiveDir,
|
|
14011
|
+
archived: true,
|
|
14012
|
+
projectPath: opts.projectPath,
|
|
14013
|
+
...opts.config?.search?.indexedFileKinds && {
|
|
14014
|
+
fileKinds: opts.config.search.indexedFileKinds
|
|
14015
|
+
},
|
|
14016
|
+
...opts.config?.search?.maxIndexBytesPerFile !== void 0 && {
|
|
14017
|
+
maxBytesPerBody: opts.config.search.maxIndexBytesPerFile
|
|
14018
|
+
}
|
|
14019
|
+
});
|
|
14020
|
+
if (result.docsWritten === 0) {
|
|
14021
|
+
logger.warn?.("session index: no docs written", { sessionId, archiveDir });
|
|
14022
|
+
}
|
|
14023
|
+
} finally {
|
|
14024
|
+
idx.close();
|
|
14025
|
+
}
|
|
14026
|
+
} catch (e) {
|
|
14027
|
+
logger.warn?.("session index: failed", {
|
|
14028
|
+
sessionId,
|
|
14029
|
+
error: e instanceof Error ? e.message : String(e)
|
|
14030
|
+
});
|
|
14031
|
+
}
|
|
14032
|
+
}
|
|
14033
|
+
function buildArchiveHooks(opts) {
|
|
14034
|
+
const logger = opts.logger ?? defaultLogger;
|
|
14035
|
+
return {
|
|
14036
|
+
async onArchived({ sessionId, archiveDir }) {
|
|
14037
|
+
await runSummaryStep(opts, logger, sessionId, archiveDir);
|
|
14038
|
+
runIndexStep(opts, logger, sessionId, archiveDir);
|
|
14039
|
+
}
|
|
14040
|
+
};
|
|
14041
|
+
}
|
|
11215
14042
|
export {
|
|
11216
14043
|
AnalysisArchive,
|
|
14044
|
+
BUILT_IN_TASKS,
|
|
11217
14045
|
BackendRouter,
|
|
11218
14046
|
ClaimManager,
|
|
14047
|
+
GateNotReadyError,
|
|
14048
|
+
GateRunError,
|
|
11219
14049
|
InteractionQueue,
|
|
11220
14050
|
LinearGraphQLStub,
|
|
11221
14051
|
MAX_ATTEMPTS,
|
|
@@ -11224,10 +14054,16 @@ export {
|
|
|
11224
14054
|
Orchestrator,
|
|
11225
14055
|
OrchestratorBackendFactory,
|
|
11226
14056
|
PRDetector,
|
|
14057
|
+
PromotionError,
|
|
11227
14058
|
PromptRenderer,
|
|
11228
14059
|
RETRY_DELAYS_MS,
|
|
11229
14060
|
RoadmapTrackerAdapter,
|
|
14061
|
+
SinkConfigError,
|
|
14062
|
+
SinkRegistry,
|
|
14063
|
+
SlackSink,
|
|
14064
|
+
SqliteSearchIndex,
|
|
11230
14065
|
StreamRecorder,
|
|
14066
|
+
TaskOutputStore,
|
|
11231
14067
|
TokenStore,
|
|
11232
14068
|
WebhookQueue,
|
|
11233
14069
|
WorkflowLoader,
|
|
@@ -11235,31 +14071,49 @@ export {
|
|
|
11235
14071
|
WorkspaceManager,
|
|
11236
14072
|
applyEvent,
|
|
11237
14073
|
artifactPresenceFromIssue,
|
|
14074
|
+
buildArchiveHooks,
|
|
11238
14075
|
calculateRetryDelay,
|
|
11239
14076
|
canDispatch,
|
|
11240
14077
|
computeRateLimitDelay,
|
|
11241
14078
|
createBackend,
|
|
11242
14079
|
createEmptyState,
|
|
11243
14080
|
detectScopeTier,
|
|
14081
|
+
emitProposalApproved,
|
|
14082
|
+
emitProposalCreated,
|
|
14083
|
+
emitProposalRejected,
|
|
11244
14084
|
extractHighlights,
|
|
11245
14085
|
extractTitlePrefix,
|
|
11246
14086
|
getAvailableSlots,
|
|
11247
14087
|
getDefaultConfig,
|
|
11248
14088
|
getPerStateCount,
|
|
14089
|
+
indexSessionDirectory,
|
|
11249
14090
|
isEligible,
|
|
14091
|
+
isSummaryEnabled,
|
|
11250
14092
|
launchTUI,
|
|
11251
14093
|
loadPublishedIndex,
|
|
11252
14094
|
migrateAgentConfig,
|
|
14095
|
+
normalizeFts5Query,
|
|
14096
|
+
openSearchIndex,
|
|
14097
|
+
promote,
|
|
11253
14098
|
reconcile,
|
|
14099
|
+
reindexFromArchive,
|
|
11254
14100
|
renderAnalysisComment,
|
|
14101
|
+
renderLlmSummaryMarkdown,
|
|
11255
14102
|
renderPRComment,
|
|
11256
14103
|
resolveEscalationConfig,
|
|
11257
14104
|
resolveOrchestratorId,
|
|
11258
14105
|
routeIssue,
|
|
14106
|
+
runGate,
|
|
11259
14107
|
savePublishedIndex,
|
|
14108
|
+
searchIndexPath,
|
|
11260
14109
|
selectCandidates,
|
|
11261
14110
|
sortCandidates,
|
|
14111
|
+
summarizeArchivedSession,
|
|
11262
14112
|
syncMain,
|
|
11263
14113
|
triageIssue,
|
|
11264
|
-
|
|
14114
|
+
truncateForBudget,
|
|
14115
|
+
validateCustomTasks,
|
|
14116
|
+
validateWorkflowConfig,
|
|
14117
|
+
wireNotificationSinks,
|
|
14118
|
+
wrapAsEnvelope
|
|
11265
14119
|
};
|