@harness-engineering/orchestrator 0.5.0 → 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 +315 -4
- package/dist/index.d.ts +315 -4
- package/dist/index.js +1781 -355
- package/dist/index.mjs +1753 -323
- 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 },
|
|
@@ -3783,8 +3783,8 @@ var BackendRouter = class {
|
|
|
3783
3783
|
validateReferences() {
|
|
3784
3784
|
const known = new Set(Object.keys(this.backends));
|
|
3785
3785
|
const missing = [];
|
|
3786
|
-
const check = (
|
|
3787
|
-
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 });
|
|
3788
3788
|
};
|
|
3789
3789
|
check("default", this.routing.default);
|
|
3790
3790
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3797,7 +3797,7 @@ var BackendRouter = class {
|
|
|
3797
3797
|
check("isolation.container", this.routing.isolation?.container);
|
|
3798
3798
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
3799
3799
|
if (missing.length > 0) {
|
|
3800
|
-
const detail = missing.map(({ path:
|
|
3800
|
+
const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
|
|
3801
3801
|
const known_ = [...known].join(", ") || "(none)";
|
|
3802
3802
|
throw new Error(
|
|
3803
3803
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3814,11 +3814,11 @@ import {
|
|
|
3814
3814
|
Ok as Ok10,
|
|
3815
3815
|
Err as Err7
|
|
3816
3816
|
} from "@harness-engineering/types";
|
|
3817
|
-
function resolveExitCode(code, command,
|
|
3817
|
+
function resolveExitCode(code, command, resolve7) {
|
|
3818
3818
|
if (code === 0) {
|
|
3819
|
-
|
|
3819
|
+
resolve7(Ok10(void 0));
|
|
3820
3820
|
} else {
|
|
3821
|
-
|
|
3821
|
+
resolve7(
|
|
3822
3822
|
Err7({
|
|
3823
3823
|
category: "agent_not_found",
|
|
3824
3824
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3826,8 +3826,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3826
3826
|
);
|
|
3827
3827
|
}
|
|
3828
3828
|
}
|
|
3829
|
-
function resolveSpawnError(command,
|
|
3830
|
-
|
|
3829
|
+
function resolveSpawnError(command, resolve7) {
|
|
3830
|
+
resolve7(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3831
3831
|
}
|
|
3832
3832
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3833
3833
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4140,10 +4140,10 @@ var ClaudeBackend = class {
|
|
|
4140
4140
|
errRl.close();
|
|
4141
4141
|
}
|
|
4142
4142
|
if (exitCode === null) {
|
|
4143
|
-
await new Promise((
|
|
4143
|
+
await new Promise((resolve7) => {
|
|
4144
4144
|
child.on("exit", (code) => {
|
|
4145
4145
|
exitCode = code;
|
|
4146
|
-
|
|
4146
|
+
resolve7(null);
|
|
4147
4147
|
});
|
|
4148
4148
|
});
|
|
4149
4149
|
}
|
|
@@ -4165,10 +4165,10 @@ var ClaudeBackend = class {
|
|
|
4165
4165
|
return Ok10(void 0);
|
|
4166
4166
|
}
|
|
4167
4167
|
async healthCheck() {
|
|
4168
|
-
return new Promise((
|
|
4168
|
+
return new Promise((resolve7) => {
|
|
4169
4169
|
const child = spawn2(this.command, ["--version"]);
|
|
4170
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4171
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4170
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
|
|
4171
|
+
child.on("error", () => resolveSpawnError(this.command, resolve7));
|
|
4172
4172
|
});
|
|
4173
4173
|
}
|
|
4174
4174
|
};
|
|
@@ -4791,7 +4791,7 @@ var PiBackend = class {
|
|
|
4791
4791
|
} else {
|
|
4792
4792
|
resolvedModelName = this.config.model;
|
|
4793
4793
|
}
|
|
4794
|
-
const piSdk = await import("@
|
|
4794
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4795
4795
|
const model = buildLocalModel({
|
|
4796
4796
|
model: resolvedModelName,
|
|
4797
4797
|
endpoint: this.config.endpoint,
|
|
@@ -4946,7 +4946,7 @@ var PiBackend = class {
|
|
|
4946
4946
|
}
|
|
4947
4947
|
async healthCheck() {
|
|
4948
4948
|
try {
|
|
4949
|
-
await import("@
|
|
4949
|
+
await import("@earendil-works/pi-coding-agent");
|
|
4950
4950
|
return Ok15(void 0);
|
|
4951
4951
|
} catch (err) {
|
|
4952
4952
|
return Err12({
|
|
@@ -5100,14 +5100,14 @@ var SshBackend = class {
|
|
|
5100
5100
|
async healthCheck() {
|
|
5101
5101
|
const args = [...this.buildSshArgs()];
|
|
5102
5102
|
args[args.length - 1] = "true";
|
|
5103
|
-
return new Promise((
|
|
5103
|
+
return new Promise((resolve7) => {
|
|
5104
5104
|
let child;
|
|
5105
5105
|
try {
|
|
5106
5106
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5107
5107
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5108
5108
|
});
|
|
5109
5109
|
} catch (err) {
|
|
5110
|
-
|
|
5110
|
+
resolve7(
|
|
5111
5111
|
Err13({
|
|
5112
5112
|
category: "agent_not_found",
|
|
5113
5113
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5128,9 +5128,9 @@ var SshBackend = class {
|
|
|
5128
5128
|
child.on("close", (code) => {
|
|
5129
5129
|
clearTimeout(timer);
|
|
5130
5130
|
if (code === 0) {
|
|
5131
|
-
|
|
5131
|
+
resolve7(Ok16(void 0));
|
|
5132
5132
|
} else {
|
|
5133
|
-
|
|
5133
|
+
resolve7(
|
|
5134
5134
|
Err13({
|
|
5135
5135
|
category: "agent_not_found",
|
|
5136
5136
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5140,7 +5140,7 @@ var SshBackend = class {
|
|
|
5140
5140
|
});
|
|
5141
5141
|
child.on("error", (err) => {
|
|
5142
5142
|
clearTimeout(timer);
|
|
5143
|
-
|
|
5143
|
+
resolve7(Err13({ category: "agent_not_found", message: err.message }));
|
|
5144
5144
|
});
|
|
5145
5145
|
});
|
|
5146
5146
|
}
|
|
@@ -5188,13 +5188,13 @@ async function* readLines(stream) {
|
|
|
5188
5188
|
if (buffer.length > 0) yield buffer;
|
|
5189
5189
|
}
|
|
5190
5190
|
function waitForExit(child) {
|
|
5191
|
-
return new Promise((
|
|
5191
|
+
return new Promise((resolve7) => {
|
|
5192
5192
|
if (child.exitCode !== null) {
|
|
5193
|
-
|
|
5193
|
+
resolve7(child.exitCode);
|
|
5194
5194
|
return;
|
|
5195
5195
|
}
|
|
5196
|
-
child.once("close", (code) =>
|
|
5197
|
-
child.once("error", () =>
|
|
5196
|
+
child.once("close", (code) => resolve7(code));
|
|
5197
|
+
child.once("error", () => resolve7(null));
|
|
5198
5198
|
});
|
|
5199
5199
|
}
|
|
5200
5200
|
|
|
@@ -5384,14 +5384,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5384
5384
|
return out;
|
|
5385
5385
|
}
|
|
5386
5386
|
runOneShot(binary, args) {
|
|
5387
|
-
return new Promise((
|
|
5387
|
+
return new Promise((resolve7) => {
|
|
5388
5388
|
let child;
|
|
5389
5389
|
try {
|
|
5390
5390
|
child = this.spawnImpl(binary, args, {
|
|
5391
5391
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5392
5392
|
});
|
|
5393
5393
|
} catch (err) {
|
|
5394
|
-
|
|
5394
|
+
resolve7(
|
|
5395
5395
|
Err14({
|
|
5396
5396
|
category: "agent_not_found",
|
|
5397
5397
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5416,9 +5416,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5416
5416
|
child.on("close", (code) => {
|
|
5417
5417
|
clearTimeout(timer);
|
|
5418
5418
|
if (code === 0) {
|
|
5419
|
-
|
|
5419
|
+
resolve7(Ok17(stdout));
|
|
5420
5420
|
} else {
|
|
5421
|
-
|
|
5421
|
+
resolve7(
|
|
5422
5422
|
Err14({
|
|
5423
5423
|
category: "response_error",
|
|
5424
5424
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5428,7 +5428,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5428
5428
|
});
|
|
5429
5429
|
child.on("error", (err) => {
|
|
5430
5430
|
clearTimeout(timer);
|
|
5431
|
-
|
|
5431
|
+
resolve7(Err14({ category: "agent_not_found", message: err.message }));
|
|
5432
5432
|
});
|
|
5433
5433
|
});
|
|
5434
5434
|
}
|
|
@@ -5488,13 +5488,13 @@ async function* readLines2(stream) {
|
|
|
5488
5488
|
if (buffer.length > 0) yield buffer;
|
|
5489
5489
|
}
|
|
5490
5490
|
function waitForExit2(child) {
|
|
5491
|
-
return new Promise((
|
|
5491
|
+
return new Promise((resolve7) => {
|
|
5492
5492
|
if (child.exitCode !== null) {
|
|
5493
|
-
|
|
5493
|
+
resolve7(child.exitCode);
|
|
5494
5494
|
return;
|
|
5495
5495
|
}
|
|
5496
|
-
child.once("close", (code) =>
|
|
5497
|
-
child.once("error", () =>
|
|
5496
|
+
child.once("close", (code) => resolve7(code));
|
|
5497
|
+
child.once("error", () => resolve7(null));
|
|
5498
5498
|
});
|
|
5499
5499
|
}
|
|
5500
5500
|
|
|
@@ -5694,13 +5694,13 @@ var ContainerBackend = class {
|
|
|
5694
5694
|
import { execFile as execFile3, spawn as spawn5 } from "child_process";
|
|
5695
5695
|
import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
|
|
5696
5696
|
function dockerExec(args) {
|
|
5697
|
-
return new Promise((
|
|
5697
|
+
return new Promise((resolve7, reject) => {
|
|
5698
5698
|
execFile3("docker", args, (error, stdout) => {
|
|
5699
5699
|
if (error) {
|
|
5700
5700
|
reject(error);
|
|
5701
5701
|
return;
|
|
5702
5702
|
}
|
|
5703
|
-
|
|
5703
|
+
resolve7(stdout.trim());
|
|
5704
5704
|
});
|
|
5705
5705
|
});
|
|
5706
5706
|
}
|
|
@@ -5759,11 +5759,11 @@ var DockerRuntime = class {
|
|
|
5759
5759
|
} finally {
|
|
5760
5760
|
rl.close();
|
|
5761
5761
|
}
|
|
5762
|
-
const exitCode = await new Promise((
|
|
5762
|
+
const exitCode = await new Promise((resolve7) => {
|
|
5763
5763
|
if (child.exitCode !== null) {
|
|
5764
|
-
|
|
5764
|
+
resolve7(child.exitCode);
|
|
5765
5765
|
} else {
|
|
5766
|
-
child.on("exit", (code) =>
|
|
5766
|
+
child.on("exit", (code) => resolve7(code ?? 1));
|
|
5767
5767
|
}
|
|
5768
5768
|
});
|
|
5769
5769
|
return exitCode;
|
|
@@ -5822,13 +5822,13 @@ var EnvSecretBackend = class {
|
|
|
5822
5822
|
import { execFile as execFile4 } from "child_process";
|
|
5823
5823
|
import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
|
|
5824
5824
|
function opExec(args) {
|
|
5825
|
-
return new Promise((
|
|
5825
|
+
return new Promise((resolve7, reject) => {
|
|
5826
5826
|
execFile4("op", args, (error, stdout) => {
|
|
5827
5827
|
if (error) {
|
|
5828
5828
|
reject(error);
|
|
5829
5829
|
return;
|
|
5830
5830
|
}
|
|
5831
|
-
|
|
5831
|
+
resolve7(stdout.trim());
|
|
5832
5832
|
});
|
|
5833
5833
|
});
|
|
5834
5834
|
}
|
|
@@ -5871,13 +5871,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5871
5871
|
import { execFile as execFile5 } from "child_process";
|
|
5872
5872
|
import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
|
|
5873
5873
|
function vaultExec(args, env) {
|
|
5874
|
-
return new Promise((
|
|
5874
|
+
return new Promise((resolve7, reject) => {
|
|
5875
5875
|
execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5876
5876
|
if (error) {
|
|
5877
5877
|
reject(error);
|
|
5878
5878
|
return;
|
|
5879
5879
|
}
|
|
5880
|
-
|
|
5880
|
+
resolve7(stdout.trim());
|
|
5881
5881
|
});
|
|
5882
5882
|
});
|
|
5883
5883
|
}
|
|
@@ -6249,7 +6249,7 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6249
6249
|
|
|
6250
6250
|
// src/server/http.ts
|
|
6251
6251
|
import * as http from "http";
|
|
6252
|
-
import * as
|
|
6252
|
+
import * as path15 from "path";
|
|
6253
6253
|
import { assertPortUsable } from "@harness-engineering/core";
|
|
6254
6254
|
|
|
6255
6255
|
// src/server/websocket.ts
|
|
@@ -6312,7 +6312,7 @@ import { z as z3 } from "zod";
|
|
|
6312
6312
|
// src/server/utils.ts
|
|
6313
6313
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6314
6314
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6315
|
-
return new Promise((
|
|
6315
|
+
return new Promise((resolve7, reject) => {
|
|
6316
6316
|
let body = "";
|
|
6317
6317
|
let bytes = 0;
|
|
6318
6318
|
req.on("data", (chunk) => {
|
|
@@ -6324,7 +6324,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6324
6324
|
}
|
|
6325
6325
|
body += String(chunk);
|
|
6326
6326
|
});
|
|
6327
|
-
req.on("end", () =>
|
|
6327
|
+
req.on("end", () => resolve7(body));
|
|
6328
6328
|
req.on("error", reject);
|
|
6329
6329
|
});
|
|
6330
6330
|
}
|
|
@@ -7460,35 +7460,576 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
7460
7460
|
return false;
|
|
7461
7461
|
}
|
|
7462
7462
|
|
|
7463
|
-
// src/server/routes/
|
|
7464
|
-
import * as fs11 from "fs/promises";
|
|
7465
|
-
import * as path11 from "path";
|
|
7463
|
+
// src/server/routes/v1/proposals.ts
|
|
7466
7464
|
import { z as z13 } from "zod";
|
|
7467
|
-
|
|
7468
|
-
|
|
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)
|
|
7469
8010
|
}).passthrough();
|
|
7470
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;
|
|
7471
8012
|
function isSafeId(id) {
|
|
7472
|
-
return UUID_RE2.test(id) ||
|
|
8013
|
+
return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
|
|
7473
8014
|
}
|
|
7474
8015
|
function jsonResponse(res, status, data) {
|
|
7475
8016
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7476
8017
|
res.end(JSON.stringify(data));
|
|
7477
8018
|
}
|
|
7478
8019
|
function extractSessionId(url) {
|
|
7479
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
8020
|
+
const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
|
|
7480
8021
|
const id = segments.pop();
|
|
7481
8022
|
return id && id !== "sessions" ? id : null;
|
|
7482
8023
|
}
|
|
7483
|
-
async function
|
|
8024
|
+
async function handleList2(res, sessionsDir) {
|
|
7484
8025
|
try {
|
|
7485
|
-
const entries = await
|
|
8026
|
+
const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
|
|
7486
8027
|
const sessions = [];
|
|
7487
8028
|
for (const entry of entries) {
|
|
7488
8029
|
if (!entry.isDirectory()) continue;
|
|
7489
8030
|
try {
|
|
7490
|
-
const content = await
|
|
7491
|
-
|
|
8031
|
+
const content = await fs12.readFile(
|
|
8032
|
+
path12.join(sessionsDir, entry.name, "session.json"),
|
|
7492
8033
|
"utf-8"
|
|
7493
8034
|
);
|
|
7494
8035
|
sessions.push(JSON.parse(content));
|
|
@@ -7507,13 +8048,13 @@ async function handleList(res, sessionsDir) {
|
|
|
7507
8048
|
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
7508
8049
|
}
|
|
7509
8050
|
}
|
|
7510
|
-
async function
|
|
8051
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
7511
8052
|
if (!isSafeId(id)) {
|
|
7512
8053
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7513
8054
|
return;
|
|
7514
8055
|
}
|
|
7515
8056
|
try {
|
|
7516
|
-
const content = await
|
|
8057
|
+
const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
|
|
7517
8058
|
jsonResponse(res, 200, JSON.parse(content));
|
|
7518
8059
|
} catch (err) {
|
|
7519
8060
|
if (err.code === "ENOENT") {
|
|
@@ -7536,9 +8077,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
7536
8077
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7537
8078
|
return;
|
|
7538
8079
|
}
|
|
7539
|
-
const sessionDir =
|
|
7540
|
-
await
|
|
7541
|
-
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));
|
|
7542
8083
|
jsonResponse(res, 200, { ok: true });
|
|
7543
8084
|
} catch {
|
|
7544
8085
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -7552,10 +8093,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
7552
8093
|
return;
|
|
7553
8094
|
}
|
|
7554
8095
|
const body = await readBody(req);
|
|
7555
|
-
const updates =
|
|
7556
|
-
const sessionFilePath =
|
|
7557
|
-
const current = JSON.parse(await
|
|
7558
|
-
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));
|
|
7559
8100
|
jsonResponse(res, 200, { ok: true });
|
|
7560
8101
|
} catch {
|
|
7561
8102
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -7568,7 +8109,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
7568
8109
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
7569
8110
|
return;
|
|
7570
8111
|
}
|
|
7571
|
-
await
|
|
8112
|
+
await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
|
|
7572
8113
|
jsonResponse(res, 200, { ok: true });
|
|
7573
8114
|
} catch {
|
|
7574
8115
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7581,8 +8122,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7581
8122
|
switch (method) {
|
|
7582
8123
|
case "GET": {
|
|
7583
8124
|
const id = extractSessionId(url);
|
|
7584
|
-
if (id) void
|
|
7585
|
-
else void
|
|
8125
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8126
|
+
else void handleList2(res, sessionsDir);
|
|
7586
8127
|
return true;
|
|
7587
8128
|
}
|
|
7588
8129
|
case "POST":
|
|
@@ -7672,20 +8213,20 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7672
8213
|
}
|
|
7673
8214
|
|
|
7674
8215
|
// src/server/routes/auth.ts
|
|
7675
|
-
import { z as
|
|
8216
|
+
import { z as z15 } from "zod";
|
|
7676
8217
|
import {
|
|
7677
8218
|
TokenScopeSchema,
|
|
7678
8219
|
BridgeKindSchema,
|
|
7679
8220
|
AuthTokenPublicSchema
|
|
7680
8221
|
} from "@harness-engineering/types";
|
|
7681
|
-
var CreateBodySchema =
|
|
7682
|
-
name:
|
|
7683
|
-
scopes:
|
|
8222
|
+
var CreateBodySchema = z15.object({
|
|
8223
|
+
name: z15.string().min(1).max(100),
|
|
8224
|
+
scopes: z15.array(TokenScopeSchema).min(1),
|
|
7684
8225
|
bridgeKind: BridgeKindSchema.optional(),
|
|
7685
|
-
tenantId:
|
|
7686
|
-
expiresAt:
|
|
8226
|
+
tenantId: z15.string().optional(),
|
|
8227
|
+
expiresAt: z15.string().datetime().optional()
|
|
7687
8228
|
});
|
|
7688
|
-
function
|
|
8229
|
+
function sendJSON9(res, status, body) {
|
|
7689
8230
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7690
8231
|
res.end(JSON.stringify(body));
|
|
7691
8232
|
}
|
|
@@ -7695,19 +8236,19 @@ async function handlePost(req, res, store) {
|
|
|
7695
8236
|
raw = await readBody(req);
|
|
7696
8237
|
} catch (err) {
|
|
7697
8238
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7698
|
-
|
|
8239
|
+
sendJSON9(res, 413, { error: msg });
|
|
7699
8240
|
return;
|
|
7700
8241
|
}
|
|
7701
8242
|
let json;
|
|
7702
8243
|
try {
|
|
7703
8244
|
json = JSON.parse(raw);
|
|
7704
8245
|
} catch {
|
|
7705
|
-
|
|
8246
|
+
sendJSON9(res, 400, { error: "Invalid JSON body" });
|
|
7706
8247
|
return;
|
|
7707
8248
|
}
|
|
7708
8249
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7709
8250
|
if (!parsed.success) {
|
|
7710
|
-
|
|
8251
|
+
sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7711
8252
|
return;
|
|
7712
8253
|
}
|
|
7713
8254
|
try {
|
|
@@ -7720,37 +8261,37 @@ async function handlePost(req, res, store) {
|
|
|
7720
8261
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7721
8262
|
const result = await store.create(input);
|
|
7722
8263
|
const publicRecord = AuthTokenPublicSchema.parse(result.record);
|
|
7723
|
-
|
|
8264
|
+
sendJSON9(res, 200, {
|
|
7724
8265
|
...publicRecord,
|
|
7725
8266
|
token: result.token
|
|
7726
8267
|
});
|
|
7727
8268
|
} catch (err) {
|
|
7728
8269
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7729
8270
|
if (msg.includes("already exists")) {
|
|
7730
|
-
|
|
8271
|
+
sendJSON9(res, 409, { error: msg });
|
|
7731
8272
|
return;
|
|
7732
8273
|
}
|
|
7733
|
-
|
|
8274
|
+
sendJSON9(res, 500, { error: "Internal error creating token" });
|
|
7734
8275
|
}
|
|
7735
8276
|
}
|
|
7736
|
-
async function
|
|
8277
|
+
async function handleList3(res, store) {
|
|
7737
8278
|
try {
|
|
7738
8279
|
const list = await store.list();
|
|
7739
|
-
|
|
8280
|
+
sendJSON9(res, 200, list);
|
|
7740
8281
|
} catch {
|
|
7741
|
-
|
|
8282
|
+
sendJSON9(res, 500, { error: "Internal error listing tokens" });
|
|
7742
8283
|
}
|
|
7743
8284
|
}
|
|
7744
8285
|
async function handleDelete2(res, store, id) {
|
|
7745
8286
|
try {
|
|
7746
8287
|
const ok = await store.revoke(id);
|
|
7747
8288
|
if (!ok) {
|
|
7748
|
-
|
|
8289
|
+
sendJSON9(res, 404, { error: "Token not found" });
|
|
7749
8290
|
return;
|
|
7750
8291
|
}
|
|
7751
|
-
|
|
8292
|
+
sendJSON9(res, 200, { deleted: true });
|
|
7752
8293
|
} catch {
|
|
7753
|
-
|
|
8294
|
+
sendJSON9(res, 500, { error: "Internal error revoking token" });
|
|
7754
8295
|
}
|
|
7755
8296
|
}
|
|
7756
8297
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7764,7 +8305,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7764
8305
|
return true;
|
|
7765
8306
|
}
|
|
7766
8307
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7767
|
-
void
|
|
8308
|
+
void handleList3(res, store);
|
|
7768
8309
|
return true;
|
|
7769
8310
|
}
|
|
7770
8311
|
if (method === "DELETE") {
|
|
@@ -7775,12 +8316,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7775
8316
|
return true;
|
|
7776
8317
|
}
|
|
7777
8318
|
}
|
|
7778
|
-
|
|
8319
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
7779
8320
|
return true;
|
|
7780
8321
|
}
|
|
7781
8322
|
|
|
7782
8323
|
// src/server/routes/local-model.ts
|
|
7783
|
-
function
|
|
8324
|
+
function sendJSON10(res, status, body) {
|
|
7784
8325
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7785
8326
|
res.end(JSON.stringify(body));
|
|
7786
8327
|
}
|
|
@@ -7788,36 +8329,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7788
8329
|
const { method, url } = req;
|
|
7789
8330
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7790
8331
|
if (method !== "GET") {
|
|
7791
|
-
|
|
8332
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7792
8333
|
return true;
|
|
7793
8334
|
}
|
|
7794
8335
|
if (!getStatus) {
|
|
7795
|
-
|
|
8336
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7796
8337
|
return true;
|
|
7797
8338
|
}
|
|
7798
8339
|
const status = getStatus();
|
|
7799
8340
|
if (!status) {
|
|
7800
|
-
|
|
8341
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7801
8342
|
return true;
|
|
7802
8343
|
}
|
|
7803
|
-
|
|
8344
|
+
sendJSON10(res, 200, status);
|
|
7804
8345
|
return true;
|
|
7805
8346
|
}
|
|
7806
8347
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7807
8348
|
const { method, url } = req;
|
|
7808
8349
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7809
8350
|
if (method !== "GET") {
|
|
7810
|
-
|
|
8351
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7811
8352
|
return true;
|
|
7812
8353
|
}
|
|
7813
8354
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7814
|
-
|
|
8355
|
+
sendJSON10(res, 200, statuses);
|
|
7815
8356
|
return true;
|
|
7816
8357
|
}
|
|
7817
8358
|
|
|
7818
8359
|
// src/server/static.ts
|
|
7819
|
-
import * as
|
|
7820
|
-
import * as
|
|
8360
|
+
import * as fs13 from "fs";
|
|
8361
|
+
import * as path13 from "path";
|
|
7821
8362
|
var MIME_TYPES = {
|
|
7822
8363
|
".html": "text/html; charset=utf-8",
|
|
7823
8364
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7837,29 +8378,29 @@ var MIME_TYPES = {
|
|
|
7837
8378
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7838
8379
|
const { method, url } = req;
|
|
7839
8380
|
if (method !== "GET") return false;
|
|
7840
|
-
const apiPrefix =
|
|
7841
|
-
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");
|
|
7842
8383
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7843
8384
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7844
|
-
const requestedPath =
|
|
7845
|
-
const resolved =
|
|
7846
|
-
if (!resolved.startsWith(
|
|
7847
|
-
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);
|
|
7848
8389
|
}
|
|
7849
|
-
if (
|
|
8390
|
+
if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
|
|
7850
8391
|
return serveFile(resolved, res);
|
|
7851
8392
|
}
|
|
7852
|
-
const indexPath =
|
|
7853
|
-
if (
|
|
8393
|
+
const indexPath = path13.join(dashboardDir, "index.html");
|
|
8394
|
+
if (fs13.existsSync(indexPath)) {
|
|
7854
8395
|
return serveFile(indexPath, res);
|
|
7855
8396
|
}
|
|
7856
8397
|
return false;
|
|
7857
8398
|
}
|
|
7858
8399
|
function serveFile(filePath, res) {
|
|
7859
|
-
const ext =
|
|
8400
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
7860
8401
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7861
8402
|
try {
|
|
7862
|
-
const content =
|
|
8403
|
+
const content = fs13.readFileSync(filePath);
|
|
7863
8404
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7864
8405
|
res.end(content);
|
|
7865
8406
|
return true;
|
|
@@ -7869,8 +8410,8 @@ function serveFile(filePath, res) {
|
|
|
7869
8410
|
}
|
|
7870
8411
|
|
|
7871
8412
|
// src/server/plan-watcher.ts
|
|
7872
|
-
import * as
|
|
7873
|
-
import * as
|
|
8413
|
+
import * as fs14 from "fs";
|
|
8414
|
+
import * as path14 from "path";
|
|
7874
8415
|
var PlanWatcher = class {
|
|
7875
8416
|
plansDir;
|
|
7876
8417
|
queue;
|
|
@@ -7884,11 +8425,11 @@ var PlanWatcher = class {
|
|
|
7884
8425
|
* Creates the directory if it does not exist.
|
|
7885
8426
|
*/
|
|
7886
8427
|
start() {
|
|
7887
|
-
|
|
7888
|
-
this.watcher =
|
|
8428
|
+
fs14.mkdirSync(this.plansDir, { recursive: true });
|
|
8429
|
+
this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
|
|
7889
8430
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7890
|
-
const filePath =
|
|
7891
|
-
if (
|
|
8431
|
+
const filePath = path14.join(this.plansDir, filename);
|
|
8432
|
+
if (fs14.existsSync(filePath)) {
|
|
7892
8433
|
void this.handleNewPlan(filename);
|
|
7893
8434
|
}
|
|
7894
8435
|
}
|
|
@@ -7941,8 +8482,8 @@ function parseToken(raw) {
|
|
|
7941
8482
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7942
8483
|
}
|
|
7943
8484
|
var TokenStore = class {
|
|
7944
|
-
constructor(
|
|
7945
|
-
this.path =
|
|
8485
|
+
constructor(path22) {
|
|
8486
|
+
this.path = path22;
|
|
7946
8487
|
}
|
|
7947
8488
|
path;
|
|
7948
8489
|
cache = null;
|
|
@@ -8049,8 +8590,8 @@ import { appendFile, mkdir as mkdir8 } from "fs/promises";
|
|
|
8049
8590
|
import { dirname as dirname5 } from "path";
|
|
8050
8591
|
import { AuthAuditEntrySchema } from "@harness-engineering/types";
|
|
8051
8592
|
var AuditLogger = class {
|
|
8052
|
-
constructor(
|
|
8053
|
-
this.path =
|
|
8593
|
+
constructor(path22, opts = {}) {
|
|
8594
|
+
this.path = path22;
|
|
8054
8595
|
this.opts = opts;
|
|
8055
8596
|
}
|
|
8056
8597
|
path;
|
|
@@ -8134,6 +8675,43 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8134
8675
|
scope: "subscribe-webhook",
|
|
8135
8676
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
8136
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
|
+
},
|
|
8137
8715
|
// ── Phase 5 bridge primitives ──
|
|
8138
8716
|
{
|
|
8139
8717
|
method: "GET",
|
|
@@ -8145,9 +8723,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8145
8723
|
function isV1Bridge(method, url) {
|
|
8146
8724
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8147
8725
|
}
|
|
8148
|
-
function requiredBridgeScope(method,
|
|
8726
|
+
function requiredBridgeScope(method, path22) {
|
|
8149
8727
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8150
|
-
if (r.method === method && r.pattern.test(
|
|
8728
|
+
if (r.method === method && r.pattern.test(path22)) return r.scope;
|
|
8151
8729
|
}
|
|
8152
8730
|
return null;
|
|
8153
8731
|
}
|
|
@@ -8157,24 +8735,24 @@ function hasScope(held, required) {
|
|
|
8157
8735
|
if (held.includes("admin")) return true;
|
|
8158
8736
|
return held.includes(required);
|
|
8159
8737
|
}
|
|
8160
|
-
function requiredScopeForRoute(method,
|
|
8161
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
8738
|
+
function requiredScopeForRoute(method, path22) {
|
|
8739
|
+
const bridgeScope = requiredBridgeScope(method, path22);
|
|
8162
8740
|
if (bridgeScope) return bridgeScope;
|
|
8163
|
-
if (
|
|
8164
|
-
if (
|
|
8165
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
8166
|
-
if ((
|
|
8167
|
-
if (
|
|
8168
|
-
if (
|
|
8169
|
-
if (
|
|
8170
|
-
if (
|
|
8171
|
-
if (
|
|
8172
|
-
if (
|
|
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"))
|
|
8173
8751
|
return "read-status";
|
|
8174
|
-
if (
|
|
8175
|
-
if (
|
|
8176
|
-
if (
|
|
8177
|
-
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";
|
|
8178
8756
|
return null;
|
|
8179
8757
|
}
|
|
8180
8758
|
|
|
@@ -8228,6 +8806,11 @@ var OrchestratorServer = class {
|
|
|
8228
8806
|
roadmapPath;
|
|
8229
8807
|
dispatchAdHoc;
|
|
8230
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;
|
|
8231
8814
|
maintenanceDeps = null;
|
|
8232
8815
|
getLocalModelStatus = null;
|
|
8233
8816
|
getLocalModelStatuses = null;
|
|
@@ -8245,8 +8828,8 @@ var OrchestratorServer = class {
|
|
|
8245
8828
|
this.orchestrator = orchestrator;
|
|
8246
8829
|
this.port = port;
|
|
8247
8830
|
this.initDependencies(deps);
|
|
8248
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8249
|
-
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");
|
|
8250
8833
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8251
8834
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8252
8835
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8259,14 +8842,15 @@ var OrchestratorServer = class {
|
|
|
8259
8842
|
}
|
|
8260
8843
|
initDependencies(deps) {
|
|
8261
8844
|
this.interactionQueue = deps?.interactionQueue;
|
|
8262
|
-
this.plansDir = deps?.plansDir ??
|
|
8263
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
8845
|
+
this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
|
|
8846
|
+
this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
|
|
8264
8847
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8265
8848
|
this.pipeline = deps?.pipeline ?? null;
|
|
8266
8849
|
this.analysisArchive = deps?.analysisArchive;
|
|
8267
8850
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8268
8851
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8269
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
8852
|
+
this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
|
|
8853
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8270
8854
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8271
8855
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8272
8856
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -8437,6 +9021,15 @@ var OrchestratorServer = class {
|
|
|
8437
9021
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
8438
9022
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
8439
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
|
+
}),
|
|
8440
9033
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
8441
9034
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
8442
9035
|
];
|
|
@@ -8524,11 +9117,11 @@ var OrchestratorServer = class {
|
|
|
8524
9117
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
8525
9118
|
this.planWatcher.start();
|
|
8526
9119
|
}
|
|
8527
|
-
return new Promise((
|
|
9120
|
+
return new Promise((resolve7) => {
|
|
8528
9121
|
const host = getBindHost();
|
|
8529
9122
|
this.httpServer.listen(this.port, host, () => {
|
|
8530
9123
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
8531
|
-
|
|
9124
|
+
resolve7();
|
|
8532
9125
|
});
|
|
8533
9126
|
});
|
|
8534
9127
|
}
|
|
@@ -8578,8 +9171,8 @@ function genSecret2() {
|
|
|
8578
9171
|
return randomBytes4(32).toString("base64url");
|
|
8579
9172
|
}
|
|
8580
9173
|
var WebhookStore = class {
|
|
8581
|
-
constructor(
|
|
8582
|
-
this.path =
|
|
9174
|
+
constructor(path22) {
|
|
9175
|
+
this.path = path22;
|
|
8583
9176
|
}
|
|
8584
9177
|
path;
|
|
8585
9178
|
cache = null;
|
|
@@ -8970,7 +9563,12 @@ var WEBHOOK_TOPICS = [
|
|
|
8970
9563
|
"maintenance:completed",
|
|
8971
9564
|
"maintenance:error",
|
|
8972
9565
|
"webhook.subscription.created",
|
|
8973
|
-
"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"
|
|
8974
9572
|
];
|
|
8975
9573
|
function newEventId2() {
|
|
8976
9574
|
return `evt_${randomBytes6(8).toString("hex")}`;
|
|
@@ -9395,6 +9993,33 @@ var ENVELOPE_DERIVERS = {
|
|
|
9395
9993
|
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9396
9994
|
severity: "info"
|
|
9397
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
|
+
};
|
|
9398
10023
|
}
|
|
9399
10024
|
};
|
|
9400
10025
|
function truncate(s, max) {
|
|
@@ -9439,7 +10064,11 @@ var NOTIFICATION_TOPICS = [
|
|
|
9439
10064
|
"interaction.resolved",
|
|
9440
10065
|
"maintenance:started",
|
|
9441
10066
|
"maintenance:completed",
|
|
9442
|
-
"maintenance:error"
|
|
10067
|
+
"maintenance:error",
|
|
10068
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10069
|
+
"proposal.created",
|
|
10070
|
+
"proposal.approved",
|
|
10071
|
+
"proposal.rejected"
|
|
9443
10072
|
];
|
|
9444
10073
|
function newEventId4() {
|
|
9445
10074
|
return `evt_${randomBytes8(8).toString("hex")}`;
|
|
@@ -9535,8 +10164,8 @@ var StructuredLogger = class {
|
|
|
9535
10164
|
};
|
|
9536
10165
|
|
|
9537
10166
|
// src/workspace/config-scanner.ts
|
|
9538
|
-
import { existsSync as
|
|
9539
|
-
import { join as
|
|
10167
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
10168
|
+
import { join as join14, relative } from "path";
|
|
9540
10169
|
import {
|
|
9541
10170
|
scanForInjection,
|
|
9542
10171
|
SecurityScanner,
|
|
@@ -9560,10 +10189,10 @@ function adjustFindingSeverity(findings) {
|
|
|
9560
10189
|
});
|
|
9561
10190
|
}
|
|
9562
10191
|
async function scanSingleFile(filePath, targetDir, scanner) {
|
|
9563
|
-
if (!
|
|
10192
|
+
if (!existsSync5(filePath)) return null;
|
|
9564
10193
|
let content;
|
|
9565
10194
|
try {
|
|
9566
|
-
content =
|
|
10195
|
+
content = readFileSync5(filePath, "utf8");
|
|
9567
10196
|
} catch {
|
|
9568
10197
|
return null;
|
|
9569
10198
|
}
|
|
@@ -9582,7 +10211,7 @@ async function scanWorkspaceConfig(workspacePath) {
|
|
|
9582
10211
|
const scanner = new SecurityScanner(parseSecurityConfig({}));
|
|
9583
10212
|
const results = [];
|
|
9584
10213
|
for (const configFile of CONFIG_FILES) {
|
|
9585
|
-
const result = await scanSingleFile(
|
|
10214
|
+
const result = await scanSingleFile(join14(workspacePath, configFile), workspacePath, scanner);
|
|
9586
10215
|
if (result) results.push(result);
|
|
9587
10216
|
}
|
|
9588
10217
|
return { exitCode: computeScanExitCode(results), results };
|
|
@@ -9768,6 +10397,19 @@ var BUILT_IN_TASKS = [
|
|
|
9768
10397
|
schedule: "*/15 * * * *",
|
|
9769
10398
|
branch: null,
|
|
9770
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"]
|
|
9771
10413
|
}
|
|
9772
10414
|
];
|
|
9773
10415
|
|
|
@@ -9860,26 +10502,51 @@ var MaintenanceScheduler = class {
|
|
|
9860
10502
|
this.resolvedTasks = this.resolveTasks();
|
|
9861
10503
|
}
|
|
9862
10504
|
/**
|
|
9863
|
-
* Merge built-in task definitions with config overrides
|
|
9864
|
-
*
|
|
9865
|
-
*
|
|
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.
|
|
9866
10509
|
*/
|
|
9867
10510
|
resolveTasks() {
|
|
9868
10511
|
const overrides = this.config.tasks ?? {};
|
|
9869
|
-
|
|
9870
|
-
|
|
9871
|
-
|
|
9872
|
-
return true;
|
|
9873
|
-
}).map((task) => {
|
|
10512
|
+
const customs = this.config.customTasks ?? {};
|
|
10513
|
+
const merged = [];
|
|
10514
|
+
for (const task of BUILT_IN_TASKS) {
|
|
9874
10515
|
const override = overrides[task.id];
|
|
9875
|
-
if (
|
|
9876
|
-
|
|
10516
|
+
if (override?.enabled === false) continue;
|
|
10517
|
+
merged.push({
|
|
9877
10518
|
...task,
|
|
9878
|
-
...override
|
|
9879
|
-
};
|
|
9880
|
-
}
|
|
9881
|
-
|
|
9882
|
-
|
|
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;
|
|
10548
|
+
}
|
|
10549
|
+
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
9883
10550
|
getResolvedTasks() {
|
|
9884
10551
|
return this.resolvedTasks;
|
|
9885
10552
|
}
|
|
@@ -10058,19 +10725,19 @@ var SingleProcessLeaderElector = class {
|
|
|
10058
10725
|
};
|
|
10059
10726
|
|
|
10060
10727
|
// src/maintenance/reporter.ts
|
|
10061
|
-
import * as
|
|
10062
|
-
import * as
|
|
10063
|
-
import { z as
|
|
10064
|
-
var RunResultSchema =
|
|
10065
|
-
taskId:
|
|
10066
|
-
startedAt:
|
|
10067
|
-
completedAt:
|
|
10068
|
-
status:
|
|
10069
|
-
findings:
|
|
10070
|
-
fixed:
|
|
10071
|
-
prUrl:
|
|
10072
|
-
prUpdated:
|
|
10073
|
-
error:
|
|
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()
|
|
10074
10741
|
});
|
|
10075
10742
|
var MAX_HISTORY = 500;
|
|
10076
10743
|
var fallbackLogger = {
|
|
@@ -10094,10 +10761,10 @@ var MaintenanceReporter = class {
|
|
|
10094
10761
|
*/
|
|
10095
10762
|
async load() {
|
|
10096
10763
|
try {
|
|
10097
|
-
await
|
|
10098
|
-
const filePath =
|
|
10099
|
-
const data = await
|
|
10100
|
-
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));
|
|
10101
10768
|
if (parsed.success) {
|
|
10102
10769
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10103
10770
|
}
|
|
@@ -10130,9 +10797,9 @@ var MaintenanceReporter = class {
|
|
|
10130
10797
|
*/
|
|
10131
10798
|
async persist() {
|
|
10132
10799
|
try {
|
|
10133
|
-
await
|
|
10134
|
-
const filePath =
|
|
10135
|
-
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");
|
|
10136
10803
|
} catch (err) {
|
|
10137
10804
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10138
10805
|
}
|
|
@@ -10148,6 +10815,9 @@ var TaskRunner = class {
|
|
|
10148
10815
|
cwd;
|
|
10149
10816
|
prManager;
|
|
10150
10817
|
baseBranch;
|
|
10818
|
+
checkScriptRunner;
|
|
10819
|
+
contextResolver;
|
|
10820
|
+
outputStore;
|
|
10151
10821
|
constructor(options) {
|
|
10152
10822
|
this.config = options.config;
|
|
10153
10823
|
this.checkRunner = options.checkRunner;
|
|
@@ -10156,27 +10826,49 @@ var TaskRunner = class {
|
|
|
10156
10826
|
this.cwd = options.cwd;
|
|
10157
10827
|
this.prManager = options.prManager ?? null;
|
|
10158
10828
|
this.baseBranch = options.baseBranch ?? "main";
|
|
10829
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
10830
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
10831
|
+
this.outputStore = options.outputStore ?? null;
|
|
10159
10832
|
}
|
|
10160
10833
|
/**
|
|
10161
10834
|
* Run a maintenance task and return the result.
|
|
10162
10835
|
* Dispatches to the appropriate execution path based on task type.
|
|
10163
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.
|
|
10164
10841
|
*/
|
|
10165
|
-
async run(task) {
|
|
10842
|
+
async run(task, origin = "cron") {
|
|
10166
10843
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10844
|
+
let result;
|
|
10845
|
+
let captured;
|
|
10167
10846
|
try {
|
|
10168
10847
|
switch (task.type) {
|
|
10169
|
-
case "mechanical-ai":
|
|
10170
|
-
|
|
10848
|
+
case "mechanical-ai": {
|
|
10849
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
10850
|
+
result = out.result;
|
|
10851
|
+
captured = out.captured;
|
|
10852
|
+
break;
|
|
10853
|
+
}
|
|
10171
10854
|
case "pure-ai":
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
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
|
+
}
|
|
10177
10869
|
default: {
|
|
10178
10870
|
const _exhaustive = task.type;
|
|
10179
|
-
|
|
10871
|
+
result = this.failureResult(
|
|
10180
10872
|
task.id,
|
|
10181
10873
|
startedAt,
|
|
10182
10874
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -10184,69 +10876,174 @@ var TaskRunner = class {
|
|
|
10184
10876
|
}
|
|
10185
10877
|
}
|
|
10186
10878
|
} catch (err) {
|
|
10187
|
-
|
|
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 {
|
|
10188
10907
|
}
|
|
10189
10908
|
}
|
|
10190
10909
|
/**
|
|
10191
|
-
*
|
|
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.
|
|
10192
10914
|
*/
|
|
10193
|
-
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
|
+
}
|
|
10194
10931
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
10195
|
-
|
|
10932
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
10196
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) {
|
|
10197
10966
|
if (!task.fixSkill) {
|
|
10198
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
10967
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
10199
10968
|
}
|
|
10200
10969
|
if (!task.branch) {
|
|
10201
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
10970
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
10202
10971
|
}
|
|
10203
|
-
|
|
10204
|
-
|
|
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) {
|
|
10205
10996
|
return {
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
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
|
|
10214
11008
|
};
|
|
10215
11009
|
}
|
|
10216
11010
|
if (this.prManager) {
|
|
10217
11011
|
try {
|
|
10218
11012
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
10219
11013
|
} catch (err) {
|
|
10220
|
-
return
|
|
11014
|
+
return wrap(
|
|
11015
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11016
|
+
baseCaptured
|
|
11017
|
+
);
|
|
10221
11018
|
}
|
|
10222
11019
|
}
|
|
10223
11020
|
const backendName = this.resolveBackend(task.id);
|
|
10224
11021
|
let agentResult;
|
|
10225
11022
|
try {
|
|
10226
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
backendName,
|
|
10230
|
-
this.cwd
|
|
10231
|
-
);
|
|
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);
|
|
10232
11026
|
} catch (err) {
|
|
10233
11027
|
return {
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
10238
|
-
|
|
10239
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
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
|
|
10243
11040
|
};
|
|
10244
11041
|
}
|
|
10245
11042
|
let prUrl = null;
|
|
10246
11043
|
let prUpdated = false;
|
|
10247
11044
|
if (this.prManager && agentResult.producedCommits) {
|
|
10248
11045
|
try {
|
|
10249
|
-
const summary = `Findings: ${
|
|
11046
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
10250
11047
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
10251
11048
|
prUrl = prResult.prUrl;
|
|
10252
11049
|
prUpdated = prResult.prUpdated;
|
|
@@ -10255,14 +11052,17 @@ var TaskRunner = class {
|
|
|
10255
11052
|
}
|
|
10256
11053
|
}
|
|
10257
11054
|
return {
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
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
|
|
10266
11066
|
};
|
|
10267
11067
|
}
|
|
10268
11068
|
/**
|
|
@@ -10282,15 +11082,13 @@ var TaskRunner = class {
|
|
|
10282
11082
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
10283
11083
|
}
|
|
10284
11084
|
}
|
|
11085
|
+
const promptContext = await this.composePromptContext(task);
|
|
10285
11086
|
const backendName = this.resolveBackend(task.id);
|
|
10286
11087
|
let agentResult;
|
|
10287
11088
|
try {
|
|
10288
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
backendName,
|
|
10292
|
-
this.cwd
|
|
10293
|
-
);
|
|
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);
|
|
10294
11092
|
} catch (err) {
|
|
10295
11093
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
10296
11094
|
}
|
|
@@ -10318,7 +11116,7 @@ var TaskRunner = class {
|
|
|
10318
11116
|
};
|
|
10319
11117
|
}
|
|
10320
11118
|
/**
|
|
10321
|
-
* Report-only: run check
|
|
11119
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
10322
11120
|
*
|
|
10323
11121
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
10324
11122
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -10328,13 +11126,24 @@ var TaskRunner = class {
|
|
|
10328
11126
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
10329
11127
|
*/
|
|
10330
11128
|
async runReportOnly(task, startedAt) {
|
|
10331
|
-
if (!task.checkCommand
|
|
10332
|
-
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
|
+
);
|
|
10333
11137
|
}
|
|
10334
|
-
|
|
10335
|
-
|
|
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);
|
|
10336
11145
|
const status = parsed?.status ?? "success";
|
|
10337
|
-
const findings = parsed === null ?
|
|
11146
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
10338
11147
|
const result = {
|
|
10339
11148
|
taskId: task.id,
|
|
10340
11149
|
startedAt,
|
|
@@ -10348,7 +11157,10 @@ var TaskRunner = class {
|
|
|
10348
11157
|
if (parsed?.error) {
|
|
10349
11158
|
result.error = parsed.error;
|
|
10350
11159
|
}
|
|
10351
|
-
return
|
|
11160
|
+
return {
|
|
11161
|
+
result,
|
|
11162
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11163
|
+
};
|
|
10352
11164
|
}
|
|
10353
11165
|
/**
|
|
10354
11166
|
* Housekeeping: run command directly, no AI, no PR.
|
|
@@ -10359,17 +11171,39 @@ var TaskRunner = class {
|
|
|
10359
11171
|
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
10360
11172
|
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
10361
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.
|
|
10362
11177
|
*/
|
|
10363
11178
|
async runHousekeeping(task, startedAt) {
|
|
10364
|
-
if (!task.checkCommand
|
|
10365
|
-
return
|
|
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
|
+
);
|
|
10366
11187
|
}
|
|
10367
11188
|
let stdout;
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
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
|
+
}
|
|
10373
11207
|
}
|
|
10374
11208
|
const parsed = parseStatusLine(stdout);
|
|
10375
11209
|
const status = parsed?.status ?? "success";
|
|
@@ -10384,7 +11218,7 @@ var TaskRunner = class {
|
|
|
10384
11218
|
prUpdated: false
|
|
10385
11219
|
};
|
|
10386
11220
|
if (parsed?.error) result.error = parsed.error;
|
|
10387
|
-
return result;
|
|
11221
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
10388
11222
|
}
|
|
10389
11223
|
/**
|
|
10390
11224
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -10409,6 +11243,9 @@ var TaskRunner = class {
|
|
|
10409
11243
|
};
|
|
10410
11244
|
}
|
|
10411
11245
|
};
|
|
11246
|
+
function wrap(result, captured) {
|
|
11247
|
+
return captured ? { result, captured } : { result };
|
|
11248
|
+
}
|
|
10412
11249
|
function parseStatusLine(output) {
|
|
10413
11250
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10414
11251
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -10446,6 +11283,560 @@ function parseStatusLine(output) {
|
|
|
10446
11283
|
return null;
|
|
10447
11284
|
}
|
|
10448
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" });
|
|
11704
|
+
}
|
|
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
|
+
});
|
|
11710
|
+
}
|
|
11711
|
+
}
|
|
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
|
+
});
|
|
11726
|
+
}
|
|
11727
|
+
}
|
|
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
|
+
});
|
|
11733
|
+
}
|
|
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
|
+
});
|
|
11741
|
+
}
|
|
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
|
+
});
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
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
|
+
});
|
|
11838
|
+
}
|
|
11839
|
+
|
|
10449
11840
|
// src/orchestrator.ts
|
|
10450
11841
|
function useCaseForBackendParam(issue, backendParam) {
|
|
10451
11842
|
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
@@ -10546,7 +11937,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10546
11937
|
completionHandler;
|
|
10547
11938
|
/** Project root directory, derived from workspace root. */
|
|
10548
11939
|
get projectRoot() {
|
|
10549
|
-
return
|
|
11940
|
+
return path19.resolve(this.config.workspace.root, "..", "..");
|
|
10550
11941
|
}
|
|
10551
11942
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
10552
11943
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -10601,10 +11992,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10601
11992
|
this.renderer = new PromptRenderer();
|
|
10602
11993
|
this.overrideBackend = overrides?.backend ?? null;
|
|
10603
11994
|
this.interactionQueue = new InteractionQueue(
|
|
10604
|
-
|
|
11995
|
+
path19.join(config.workspace.root, "..", "interactions"),
|
|
10605
11996
|
this
|
|
10606
11997
|
);
|
|
10607
|
-
this.analysisArchive = new AnalysisArchive(
|
|
11998
|
+
this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
|
|
10608
11999
|
const backendsMap = this.config.agent.backends ?? {};
|
|
10609
12000
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
10610
12001
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -10648,7 +12039,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10648
12039
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
10649
12040
|
});
|
|
10650
12041
|
this.recorder = new StreamRecorder(
|
|
10651
|
-
|
|
12042
|
+
path19.resolve(config.workspace.root, "..", "streams"),
|
|
10652
12043
|
this.logger
|
|
10653
12044
|
);
|
|
10654
12045
|
const self = this;
|
|
@@ -10679,10 +12070,10 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10679
12070
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
10680
12071
|
if (config.server?.port) {
|
|
10681
12072
|
const webhookStore = new WebhookStore(
|
|
10682
|
-
|
|
12073
|
+
path19.join(this.projectRoot, ".harness", "webhooks.json")
|
|
10683
12074
|
);
|
|
10684
12075
|
this.webhookQueue = new WebhookQueue(
|
|
10685
|
-
|
|
12076
|
+
path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
10686
12077
|
);
|
|
10687
12078
|
const webhookDelivery = new WebhookDelivery({
|
|
10688
12079
|
queue: this.webhookQueue,
|
|
@@ -10720,7 +12111,7 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10720
12111
|
queue: this.webhookQueue
|
|
10721
12112
|
},
|
|
10722
12113
|
cacheMetrics: this.cacheMetrics,
|
|
10723
|
-
plansDir:
|
|
12114
|
+
plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
10724
12115
|
pipeline: this.pipeline,
|
|
10725
12116
|
analysisArchive: this.analysisArchive,
|
|
10726
12117
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -10776,13 +12167,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10776
12167
|
const logger = this.logger;
|
|
10777
12168
|
const checkRunner = {
|
|
10778
12169
|
run: async (command, cwd) => {
|
|
10779
|
-
const { execFile:
|
|
10780
|
-
const { promisify:
|
|
10781
|
-
const
|
|
12170
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12171
|
+
const { promisify: promisify5 } = await import("util");
|
|
12172
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10782
12173
|
const [cmd, ...args] = command;
|
|
10783
12174
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
10784
12175
|
try {
|
|
10785
|
-
const { stdout } = await
|
|
12176
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10786
12177
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
10787
12178
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
10788
12179
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -10811,13 +12202,13 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10811
12202
|
};
|
|
10812
12203
|
const commandExecutor = {
|
|
10813
12204
|
exec: async (command, cwd) => {
|
|
10814
|
-
const { execFile:
|
|
10815
|
-
const { promisify:
|
|
10816
|
-
const
|
|
12205
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12206
|
+
const { promisify: promisify5 } = await import("util");
|
|
12207
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10817
12208
|
const [cmd, ...args] = command;
|
|
10818
12209
|
if (!cmd) return { stdout: "" };
|
|
10819
12210
|
try {
|
|
10820
|
-
const { stdout } = await
|
|
12211
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10821
12212
|
return { stdout: String(stdout) };
|
|
10822
12213
|
} catch (err) {
|
|
10823
12214
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -10829,12 +12220,31 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10829
12220
|
}
|
|
10830
12221
|
}
|
|
10831
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
|
+
});
|
|
10832
12239
|
return new TaskRunner({
|
|
10833
12240
|
config: maintenanceConfig,
|
|
10834
12241
|
checkRunner,
|
|
10835
12242
|
agentDispatcher,
|
|
10836
12243
|
commandExecutor,
|
|
10837
|
-
cwd: this.projectRoot
|
|
12244
|
+
cwd: this.projectRoot,
|
|
12245
|
+
checkScriptRunner,
|
|
12246
|
+
contextResolver,
|
|
12247
|
+
outputStore
|
|
10838
12248
|
});
|
|
10839
12249
|
}
|
|
10840
12250
|
/**
|
|
@@ -10842,8 +12252,17 @@ var Orchestrator = class extends EventEmitter {
|
|
|
10842
12252
|
* Extracted from start() to keep function length under threshold.
|
|
10843
12253
|
*/
|
|
10844
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
|
+
}
|
|
10845
12264
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
10846
|
-
persistDir:
|
|
12265
|
+
persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
10847
12266
|
logger: this.logger
|
|
10848
12267
|
});
|
|
10849
12268
|
await this.maintenanceReporter.load();
|
|
@@ -12020,10 +13439,10 @@ function launchTUI(orchestrator) {
|
|
|
12020
13439
|
|
|
12021
13440
|
// src/maintenance/sync-main.ts
|
|
12022
13441
|
import { execFile as nodeExecFile } from "child_process";
|
|
12023
|
-
import { promisify as
|
|
13442
|
+
import { promisify as promisify4 } from "util";
|
|
12024
13443
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
12025
13444
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
12026
|
-
const exec =
|
|
13445
|
+
const exec = promisify4(execFileFn);
|
|
12027
13446
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
12028
13447
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
12029
13448
|
}
|
|
@@ -12163,8 +13582,8 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
12163
13582
|
}
|
|
12164
13583
|
|
|
12165
13584
|
// src/sessions/search-index.ts
|
|
12166
|
-
import * as
|
|
12167
|
-
import * as
|
|
13585
|
+
import * as fs17 from "fs";
|
|
13586
|
+
import * as path20 from "path";
|
|
12168
13587
|
import Database2 from "better-sqlite3";
|
|
12169
13588
|
import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
|
|
12170
13589
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
@@ -12209,7 +13628,7 @@ function normalizeFts5Query(query) {
|
|
|
12209
13628
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
12210
13629
|
}
|
|
12211
13630
|
function searchIndexPath(projectPath) {
|
|
12212
|
-
return
|
|
13631
|
+
return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
12213
13632
|
}
|
|
12214
13633
|
var FILE_KIND_TO_FILENAME = {
|
|
12215
13634
|
summary: "summary.md",
|
|
@@ -12224,7 +13643,7 @@ var SqliteSearchIndex = class {
|
|
|
12224
13643
|
removeSessionStmt;
|
|
12225
13644
|
totalStmt;
|
|
12226
13645
|
constructor(dbPath) {
|
|
12227
|
-
|
|
13646
|
+
fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
|
|
12228
13647
|
this.db = new Database2(dbPath);
|
|
12229
13648
|
this.db.pragma("journal_mode = WAL");
|
|
12230
13649
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -12329,14 +13748,14 @@ function indexSessionDirectory(idx, args) {
|
|
|
12329
13748
|
let docsWritten = 0;
|
|
12330
13749
|
for (const kind of kinds) {
|
|
12331
13750
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
12332
|
-
const filePath =
|
|
12333
|
-
if (!
|
|
12334
|
-
let body =
|
|
13751
|
+
const filePath = path20.join(args.sessionDir, fileName);
|
|
13752
|
+
if (!fs17.existsSync(filePath)) continue;
|
|
13753
|
+
let body = fs17.readFileSync(filePath, "utf8");
|
|
12335
13754
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
12336
13755
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
12337
13756
|
}
|
|
12338
|
-
const stat =
|
|
12339
|
-
const relPath =
|
|
13757
|
+
const stat = fs17.statSync(filePath);
|
|
13758
|
+
const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
12340
13759
|
idx.upsertSessionDoc({
|
|
12341
13760
|
sessionId: args.sessionId,
|
|
12342
13761
|
archived: args.archived,
|
|
@@ -12351,17 +13770,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
12351
13770
|
}
|
|
12352
13771
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
12353
13772
|
const start = Date.now();
|
|
12354
|
-
const archiveBase =
|
|
13773
|
+
const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
|
|
12355
13774
|
const idx = openSearchIndex(projectPath);
|
|
12356
13775
|
try {
|
|
12357
13776
|
idx.resetArchived();
|
|
12358
13777
|
let sessionsIndexed = 0;
|
|
12359
13778
|
let docsWritten = 0;
|
|
12360
|
-
if (
|
|
12361
|
-
const entries =
|
|
13779
|
+
if (fs17.existsSync(archiveBase)) {
|
|
13780
|
+
const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
|
|
12362
13781
|
for (const entry of entries) {
|
|
12363
13782
|
if (!entry.isDirectory()) continue;
|
|
12364
|
-
const sessionDir =
|
|
13783
|
+
const sessionDir = path20.join(archiveBase, entry.name);
|
|
12365
13784
|
const result = indexSessionDirectory(idx, {
|
|
12366
13785
|
sessionId: entry.name,
|
|
12367
13786
|
sessionDir,
|
|
@@ -12381,12 +13800,12 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
12381
13800
|
}
|
|
12382
13801
|
|
|
12383
13802
|
// src/sessions/summarize.ts
|
|
12384
|
-
import * as
|
|
12385
|
-
import * as
|
|
13803
|
+
import * as fs18 from "fs";
|
|
13804
|
+
import * as path21 from "path";
|
|
12386
13805
|
import {
|
|
12387
13806
|
SessionSummarySchema
|
|
12388
13807
|
} from "@harness-engineering/types";
|
|
12389
|
-
import { Ok as
|
|
13808
|
+
import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
|
|
12390
13809
|
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
12391
13810
|
var SUMMARY_INPUT_FILES = [
|
|
12392
13811
|
{ filename: "summary.md", kind: "summary" },
|
|
@@ -12412,10 +13831,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
12412
13831
|
function readInputCorpus(archiveDir) {
|
|
12413
13832
|
const parts = [];
|
|
12414
13833
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
12415
|
-
const p =
|
|
12416
|
-
if (!
|
|
13834
|
+
const p = path21.join(archiveDir, filename);
|
|
13835
|
+
if (!fs18.existsSync(p)) continue;
|
|
12417
13836
|
try {
|
|
12418
|
-
const content =
|
|
13837
|
+
const content = fs18.readFileSync(p, "utf8");
|
|
12419
13838
|
if (content.trim().length === 0) continue;
|
|
12420
13839
|
parts.push(`## FILE: ${kind}
|
|
12421
13840
|
|
|
@@ -12466,7 +13885,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
12466
13885
|
return lines.join("\n");
|
|
12467
13886
|
}
|
|
12468
13887
|
function writeStubMarkdown(archiveDir, reason) {
|
|
12469
|
-
const filePath =
|
|
13888
|
+
const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
|
|
12470
13889
|
const body = `---
|
|
12471
13890
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
12472
13891
|
schemaVersion: 1
|
|
@@ -12477,17 +13896,17 @@ status: failed
|
|
|
12477
13896
|
|
|
12478
13897
|
- reason: ${reason}
|
|
12479
13898
|
`;
|
|
12480
|
-
|
|
13899
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
12481
13900
|
return filePath;
|
|
12482
13901
|
}
|
|
12483
13902
|
async function summarizeArchivedSession(ctx) {
|
|
12484
13903
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
12485
|
-
if (!
|
|
12486
|
-
return
|
|
13904
|
+
if (!fs18.existsSync(ctx.archiveDir)) {
|
|
13905
|
+
return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
12487
13906
|
}
|
|
12488
13907
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
12489
13908
|
if (corpus.trim().length === 0) {
|
|
12490
|
-
return
|
|
13909
|
+
return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
12491
13910
|
}
|
|
12492
13911
|
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
12493
13912
|
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
@@ -12520,7 +13939,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12520
13939
|
} catch {
|
|
12521
13940
|
}
|
|
12522
13941
|
}
|
|
12523
|
-
return
|
|
13942
|
+
return Err21(
|
|
12524
13943
|
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
12525
13944
|
);
|
|
12526
13945
|
}
|
|
@@ -12534,7 +13953,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12534
13953
|
} catch {
|
|
12535
13954
|
}
|
|
12536
13955
|
}
|
|
12537
|
-
return
|
|
13956
|
+
return Err21(new Error(reason));
|
|
12538
13957
|
}
|
|
12539
13958
|
const meta = {
|
|
12540
13959
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12543,10 +13962,10 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12543
13962
|
outputTokens: response.tokenUsage.outputTokens,
|
|
12544
13963
|
schemaVersion: 1
|
|
12545
13964
|
};
|
|
12546
|
-
const filePath =
|
|
13965
|
+
const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
12547
13966
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
12548
|
-
|
|
12549
|
-
return
|
|
13967
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13968
|
+
return Ok24({ summary: parsed.data, meta, filePath });
|
|
12550
13969
|
}
|
|
12551
13970
|
function isSummaryEnabled(config) {
|
|
12552
13971
|
if (!config) return false;
|
|
@@ -12622,8 +14041,11 @@ function buildArchiveHooks(opts) {
|
|
|
12622
14041
|
}
|
|
12623
14042
|
export {
|
|
12624
14043
|
AnalysisArchive,
|
|
14044
|
+
BUILT_IN_TASKS,
|
|
12625
14045
|
BackendRouter,
|
|
12626
14046
|
ClaimManager,
|
|
14047
|
+
GateNotReadyError,
|
|
14048
|
+
GateRunError,
|
|
12627
14049
|
InteractionQueue,
|
|
12628
14050
|
LinearGraphQLStub,
|
|
12629
14051
|
MAX_ATTEMPTS,
|
|
@@ -12632,6 +14054,7 @@ export {
|
|
|
12632
14054
|
Orchestrator,
|
|
12633
14055
|
OrchestratorBackendFactory,
|
|
12634
14056
|
PRDetector,
|
|
14057
|
+
PromotionError,
|
|
12635
14058
|
PromptRenderer,
|
|
12636
14059
|
RETRY_DELAYS_MS,
|
|
12637
14060
|
RoadmapTrackerAdapter,
|
|
@@ -12640,6 +14063,7 @@ export {
|
|
|
12640
14063
|
SlackSink,
|
|
12641
14064
|
SqliteSearchIndex,
|
|
12642
14065
|
StreamRecorder,
|
|
14066
|
+
TaskOutputStore,
|
|
12643
14067
|
TokenStore,
|
|
12644
14068
|
WebhookQueue,
|
|
12645
14069
|
WorkflowLoader,
|
|
@@ -12654,6 +14078,9 @@ export {
|
|
|
12654
14078
|
createBackend,
|
|
12655
14079
|
createEmptyState,
|
|
12656
14080
|
detectScopeTier,
|
|
14081
|
+
emitProposalApproved,
|
|
14082
|
+
emitProposalCreated,
|
|
14083
|
+
emitProposalRejected,
|
|
12657
14084
|
extractHighlights,
|
|
12658
14085
|
extractTitlePrefix,
|
|
12659
14086
|
getAvailableSlots,
|
|
@@ -12667,6 +14094,7 @@ export {
|
|
|
12667
14094
|
migrateAgentConfig,
|
|
12668
14095
|
normalizeFts5Query,
|
|
12669
14096
|
openSearchIndex,
|
|
14097
|
+
promote,
|
|
12670
14098
|
reconcile,
|
|
12671
14099
|
reindexFromArchive,
|
|
12672
14100
|
renderAnalysisComment,
|
|
@@ -12675,6 +14103,7 @@ export {
|
|
|
12675
14103
|
resolveEscalationConfig,
|
|
12676
14104
|
resolveOrchestratorId,
|
|
12677
14105
|
routeIssue,
|
|
14106
|
+
runGate,
|
|
12678
14107
|
savePublishedIndex,
|
|
12679
14108
|
searchIndexPath,
|
|
12680
14109
|
selectCandidates,
|
|
@@ -12683,6 +14112,7 @@ export {
|
|
|
12683
14112
|
syncMain,
|
|
12684
14113
|
triageIssue,
|
|
12685
14114
|
truncateForBudget,
|
|
14115
|
+
validateCustomTasks,
|
|
12686
14116
|
validateWorkflowConfig,
|
|
12687
14117
|
wireNotificationSinks,
|
|
12688
14118
|
wrapAsEnvelope
|