@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.js
CHANGED
|
@@ -31,8 +31,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
AnalysisArchive: () => AnalysisArchive,
|
|
34
|
+
BUILT_IN_TASKS: () => BUILT_IN_TASKS,
|
|
34
35
|
BackendRouter: () => BackendRouter,
|
|
35
36
|
ClaimManager: () => ClaimManager,
|
|
37
|
+
GateNotReadyError: () => GateNotReadyError,
|
|
38
|
+
GateRunError: () => GateRunError,
|
|
36
39
|
InteractionQueue: () => InteractionQueue,
|
|
37
40
|
LinearGraphQLStub: () => LinearGraphQLStub,
|
|
38
41
|
MAX_ATTEMPTS: () => MAX_ATTEMPTS,
|
|
@@ -41,6 +44,7 @@ __export(index_exports, {
|
|
|
41
44
|
Orchestrator: () => Orchestrator,
|
|
42
45
|
OrchestratorBackendFactory: () => OrchestratorBackendFactory,
|
|
43
46
|
PRDetector: () => PRDetector,
|
|
47
|
+
PromotionError: () => PromotionError,
|
|
44
48
|
PromptRenderer: () => PromptRenderer,
|
|
45
49
|
RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
|
|
46
50
|
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
@@ -49,6 +53,7 @@ __export(index_exports, {
|
|
|
49
53
|
SlackSink: () => SlackSink,
|
|
50
54
|
SqliteSearchIndex: () => SqliteSearchIndex,
|
|
51
55
|
StreamRecorder: () => StreamRecorder,
|
|
56
|
+
TaskOutputStore: () => TaskOutputStore,
|
|
52
57
|
TokenStore: () => TokenStore,
|
|
53
58
|
WebhookQueue: () => WebhookQueue,
|
|
54
59
|
WorkflowLoader: () => WorkflowLoader,
|
|
@@ -63,6 +68,9 @@ __export(index_exports, {
|
|
|
63
68
|
createBackend: () => createBackend,
|
|
64
69
|
createEmptyState: () => createEmptyState,
|
|
65
70
|
detectScopeTier: () => detectScopeTier,
|
|
71
|
+
emitProposalApproved: () => emitProposalApproved,
|
|
72
|
+
emitProposalCreated: () => emitProposalCreated,
|
|
73
|
+
emitProposalRejected: () => emitProposalRejected,
|
|
66
74
|
extractHighlights: () => extractHighlights,
|
|
67
75
|
extractTitlePrefix: () => extractTitlePrefix,
|
|
68
76
|
getAvailableSlots: () => getAvailableSlots,
|
|
@@ -76,6 +84,7 @@ __export(index_exports, {
|
|
|
76
84
|
migrateAgentConfig: () => migrateAgentConfig,
|
|
77
85
|
normalizeFts5Query: () => normalizeFts5Query,
|
|
78
86
|
openSearchIndex: () => openSearchIndex,
|
|
87
|
+
promote: () => promote,
|
|
79
88
|
reconcile: () => reconcile,
|
|
80
89
|
reindexFromArchive: () => reindexFromArchive,
|
|
81
90
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
@@ -84,6 +93,7 @@ __export(index_exports, {
|
|
|
84
93
|
resolveEscalationConfig: () => resolveEscalationConfig,
|
|
85
94
|
resolveOrchestratorId: () => resolveOrchestratorId,
|
|
86
95
|
routeIssue: () => routeIssue,
|
|
96
|
+
runGate: () => runGate,
|
|
87
97
|
savePublishedIndex: () => savePublishedIndex,
|
|
88
98
|
searchIndexPath: () => searchIndexPath,
|
|
89
99
|
selectCandidates: () => selectCandidates,
|
|
@@ -92,6 +102,7 @@ __export(index_exports, {
|
|
|
92
102
|
syncMain: () => syncMain,
|
|
93
103
|
triageIssue: () => triageIssue,
|
|
94
104
|
truncateForBudget: () => truncateForBudget,
|
|
105
|
+
validateCustomTasks: () => validateCustomTasks,
|
|
95
106
|
validateWorkflowConfig: () => validateWorkflowConfig,
|
|
96
107
|
wireNotificationSinks: () => wireNotificationSinks,
|
|
97
108
|
wrapAsEnvelope: () => wrapAsEnvelope
|
|
@@ -1244,7 +1255,7 @@ var ClaimManager = class {
|
|
|
1244
1255
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1245
1256
|
if (!claimResult.ok) return claimResult;
|
|
1246
1257
|
if (this.verifyDelayMs > 0) {
|
|
1247
|
-
await new Promise((
|
|
1258
|
+
await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
|
|
1248
1259
|
}
|
|
1249
1260
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1250
1261
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1967,11 +1978,11 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
|
|
|
1967
1978
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1968
1979
|
const issues = [];
|
|
1969
1980
|
const names = new Set(Object.keys(backends));
|
|
1970
|
-
const checkRef = (
|
|
1981
|
+
const checkRef = (path22, name) => {
|
|
1971
1982
|
if (name !== void 0 && !names.has(name)) {
|
|
1972
1983
|
issues.push({
|
|
1973
|
-
path:
|
|
1974
|
-
message: `routing.${
|
|
1984
|
+
path: path22,
|
|
1985
|
+
message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1975
1986
|
});
|
|
1976
1987
|
}
|
|
1977
1988
|
};
|
|
@@ -2635,7 +2646,7 @@ var WorkspaceHooks = class {
|
|
|
2635
2646
|
if (!command) {
|
|
2636
2647
|
return (0, import_types7.Ok)(void 0);
|
|
2637
2648
|
}
|
|
2638
|
-
return new Promise((
|
|
2649
|
+
return new Promise((resolve7) => {
|
|
2639
2650
|
const filteredEnv = {};
|
|
2640
2651
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2641
2652
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2648,19 +2659,19 @@ var WorkspaceHooks = class {
|
|
|
2648
2659
|
});
|
|
2649
2660
|
const timeout = setTimeout(() => {
|
|
2650
2661
|
child.kill();
|
|
2651
|
-
|
|
2662
|
+
resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2652
2663
|
}, this.config.timeoutMs);
|
|
2653
2664
|
child.on("exit", (code) => {
|
|
2654
2665
|
clearTimeout(timeout);
|
|
2655
2666
|
if (code === 0) {
|
|
2656
|
-
|
|
2667
|
+
resolve7((0, import_types7.Ok)(void 0));
|
|
2657
2668
|
} else {
|
|
2658
|
-
|
|
2669
|
+
resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2659
2670
|
}
|
|
2660
2671
|
});
|
|
2661
2672
|
child.on("error", (error) => {
|
|
2662
2673
|
clearTimeout(timeout);
|
|
2663
|
-
|
|
2674
|
+
resolve7((0, import_types7.Err)(error));
|
|
2664
2675
|
});
|
|
2665
2676
|
});
|
|
2666
2677
|
}
|
|
@@ -2698,7 +2709,7 @@ var MockBackend = class {
|
|
|
2698
2709
|
content: "Thinking...",
|
|
2699
2710
|
sessionId: session.sessionId
|
|
2700
2711
|
};
|
|
2701
|
-
await new Promise((
|
|
2712
|
+
await new Promise((resolve7) => setTimeout(resolve7, 100));
|
|
2702
2713
|
yield {
|
|
2703
2714
|
type: "thought",
|
|
2704
2715
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2750,9 +2761,9 @@ var PromptRenderer = class {
|
|
|
2750
2761
|
|
|
2751
2762
|
// src/orchestrator.ts
|
|
2752
2763
|
var import_node_events = require("events");
|
|
2753
|
-
var
|
|
2764
|
+
var path19 = __toESM(require("path"));
|
|
2754
2765
|
var import_node_crypto16 = require("crypto");
|
|
2755
|
-
var
|
|
2766
|
+
var import_core14 = require("@harness-engineering/core");
|
|
2756
2767
|
|
|
2757
2768
|
// src/intelligence/pipeline-runner.ts
|
|
2758
2769
|
var path7 = __toESM(require("path"));
|
|
@@ -3313,7 +3324,7 @@ var CompletionHandler = class {
|
|
|
3313
3324
|
};
|
|
3314
3325
|
|
|
3315
3326
|
// src/orchestrator.ts
|
|
3316
|
-
var
|
|
3327
|
+
var import_core15 = require("@harness-engineering/core");
|
|
3317
3328
|
|
|
3318
3329
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3319
3330
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -3717,11 +3728,11 @@ function detectLegacyFields(agent) {
|
|
|
3717
3728
|
}
|
|
3718
3729
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3719
3730
|
const warnings = [];
|
|
3720
|
-
for (const
|
|
3721
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3722
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3731
|
+
for (const path22 of presentLegacy) {
|
|
3732
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
|
|
3733
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
|
|
3723
3734
|
warnings.push(
|
|
3724
|
-
`Ignoring legacy field '${
|
|
3735
|
+
`Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3725
3736
|
);
|
|
3726
3737
|
}
|
|
3727
3738
|
return warnings;
|
|
@@ -3749,7 +3760,7 @@ function migrateAgentConfig(agent) {
|
|
|
3749
3760
|
}
|
|
3750
3761
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3751
3762
|
const warnings = presentLegacy.map(
|
|
3752
|
-
(
|
|
3763
|
+
(path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3753
3764
|
);
|
|
3754
3765
|
return {
|
|
3755
3766
|
config: { ...agent, backends, routing },
|
|
@@ -3865,8 +3876,8 @@ var BackendRouter = class {
|
|
|
3865
3876
|
validateReferences() {
|
|
3866
3877
|
const known = new Set(Object.keys(this.backends));
|
|
3867
3878
|
const missing = [];
|
|
3868
|
-
const check = (
|
|
3869
|
-
if (name !== void 0 && !known.has(name)) missing.push({ path:
|
|
3879
|
+
const check = (path22, name) => {
|
|
3880
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
|
|
3870
3881
|
};
|
|
3871
3882
|
check("default", this.routing.default);
|
|
3872
3883
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3879,7 +3890,7 @@ var BackendRouter = class {
|
|
|
3879
3890
|
check("isolation.container", this.routing.isolation?.container);
|
|
3880
3891
|
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
3881
3892
|
if (missing.length > 0) {
|
|
3882
|
-
const detail = missing.map(({ path:
|
|
3893
|
+
const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
|
|
3883
3894
|
const known_ = [...known].join(", ") || "(none)";
|
|
3884
3895
|
throw new Error(
|
|
3885
3896
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3893,11 +3904,11 @@ var import_node_child_process4 = require("child_process");
|
|
|
3893
3904
|
var readline = __toESM(require("readline"));
|
|
3894
3905
|
var import_node_crypto3 = require("crypto");
|
|
3895
3906
|
var import_types10 = require("@harness-engineering/types");
|
|
3896
|
-
function resolveExitCode(code, command,
|
|
3907
|
+
function resolveExitCode(code, command, resolve7) {
|
|
3897
3908
|
if (code === 0) {
|
|
3898
|
-
|
|
3909
|
+
resolve7((0, import_types10.Ok)(void 0));
|
|
3899
3910
|
} else {
|
|
3900
|
-
|
|
3911
|
+
resolve7(
|
|
3901
3912
|
(0, import_types10.Err)({
|
|
3902
3913
|
category: "agent_not_found",
|
|
3903
3914
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3905,8 +3916,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3905
3916
|
);
|
|
3906
3917
|
}
|
|
3907
3918
|
}
|
|
3908
|
-
function resolveSpawnError(command,
|
|
3909
|
-
|
|
3919
|
+
function resolveSpawnError(command, resolve7) {
|
|
3920
|
+
resolve7((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3910
3921
|
}
|
|
3911
3922
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3912
3923
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4219,10 +4230,10 @@ var ClaudeBackend = class {
|
|
|
4219
4230
|
errRl.close();
|
|
4220
4231
|
}
|
|
4221
4232
|
if (exitCode === null) {
|
|
4222
|
-
await new Promise((
|
|
4233
|
+
await new Promise((resolve7) => {
|
|
4223
4234
|
child.on("exit", (code) => {
|
|
4224
4235
|
exitCode = code;
|
|
4225
|
-
|
|
4236
|
+
resolve7(null);
|
|
4226
4237
|
});
|
|
4227
4238
|
});
|
|
4228
4239
|
}
|
|
@@ -4244,10 +4255,10 @@ var ClaudeBackend = class {
|
|
|
4244
4255
|
return (0, import_types10.Ok)(void 0);
|
|
4245
4256
|
}
|
|
4246
4257
|
async healthCheck() {
|
|
4247
|
-
return new Promise((
|
|
4258
|
+
return new Promise((resolve7) => {
|
|
4248
4259
|
const child = (0, import_node_child_process4.spawn)(this.command, ["--version"]);
|
|
4249
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4250
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4260
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
|
|
4261
|
+
child.on("error", () => resolveSpawnError(this.command, resolve7));
|
|
4251
4262
|
});
|
|
4252
4263
|
}
|
|
4253
4264
|
};
|
|
@@ -4855,7 +4866,7 @@ var PiBackend = class {
|
|
|
4855
4866
|
} else {
|
|
4856
4867
|
resolvedModelName = this.config.model;
|
|
4857
4868
|
}
|
|
4858
|
-
const piSdk = await import("@
|
|
4869
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4859
4870
|
const model = buildLocalModel({
|
|
4860
4871
|
model: resolvedModelName,
|
|
4861
4872
|
endpoint: this.config.endpoint,
|
|
@@ -5010,7 +5021,7 @@ var PiBackend = class {
|
|
|
5010
5021
|
}
|
|
5011
5022
|
async healthCheck() {
|
|
5012
5023
|
try {
|
|
5013
|
-
await import("@
|
|
5024
|
+
await import("@earendil-works/pi-coding-agent");
|
|
5014
5025
|
return (0, import_types15.Ok)(void 0);
|
|
5015
5026
|
} catch (err) {
|
|
5016
5027
|
return (0, import_types15.Err)({
|
|
@@ -5161,14 +5172,14 @@ var SshBackend = class {
|
|
|
5161
5172
|
async healthCheck() {
|
|
5162
5173
|
const args = [...this.buildSshArgs()];
|
|
5163
5174
|
args[args.length - 1] = "true";
|
|
5164
|
-
return new Promise((
|
|
5175
|
+
return new Promise((resolve7) => {
|
|
5165
5176
|
let child;
|
|
5166
5177
|
try {
|
|
5167
5178
|
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5168
5179
|
stdio: ["ignore", "ignore", "pipe"]
|
|
5169
5180
|
});
|
|
5170
5181
|
} catch (err) {
|
|
5171
|
-
|
|
5182
|
+
resolve7(
|
|
5172
5183
|
(0, import_types16.Err)({
|
|
5173
5184
|
category: "agent_not_found",
|
|
5174
5185
|
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
@@ -5189,9 +5200,9 @@ var SshBackend = class {
|
|
|
5189
5200
|
child.on("close", (code) => {
|
|
5190
5201
|
clearTimeout(timer);
|
|
5191
5202
|
if (code === 0) {
|
|
5192
|
-
|
|
5203
|
+
resolve7((0, import_types16.Ok)(void 0));
|
|
5193
5204
|
} else {
|
|
5194
|
-
|
|
5205
|
+
resolve7(
|
|
5195
5206
|
(0, import_types16.Err)({
|
|
5196
5207
|
category: "agent_not_found",
|
|
5197
5208
|
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
@@ -5201,7 +5212,7 @@ var SshBackend = class {
|
|
|
5201
5212
|
});
|
|
5202
5213
|
child.on("error", (err) => {
|
|
5203
5214
|
clearTimeout(timer);
|
|
5204
|
-
|
|
5215
|
+
resolve7((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
|
|
5205
5216
|
});
|
|
5206
5217
|
});
|
|
5207
5218
|
}
|
|
@@ -5249,13 +5260,13 @@ async function* readLines(stream) {
|
|
|
5249
5260
|
if (buffer.length > 0) yield buffer;
|
|
5250
5261
|
}
|
|
5251
5262
|
function waitForExit(child) {
|
|
5252
|
-
return new Promise((
|
|
5263
|
+
return new Promise((resolve7) => {
|
|
5253
5264
|
if (child.exitCode !== null) {
|
|
5254
|
-
|
|
5265
|
+
resolve7(child.exitCode);
|
|
5255
5266
|
return;
|
|
5256
5267
|
}
|
|
5257
|
-
child.once("close", (code) =>
|
|
5258
|
-
child.once("error", () =>
|
|
5268
|
+
child.once("close", (code) => resolve7(code));
|
|
5269
|
+
child.once("error", () => resolve7(null));
|
|
5259
5270
|
});
|
|
5260
5271
|
}
|
|
5261
5272
|
|
|
@@ -5442,14 +5453,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5442
5453
|
return out;
|
|
5443
5454
|
}
|
|
5444
5455
|
runOneShot(binary, args) {
|
|
5445
|
-
return new Promise((
|
|
5456
|
+
return new Promise((resolve7) => {
|
|
5446
5457
|
let child;
|
|
5447
5458
|
try {
|
|
5448
5459
|
child = this.spawnImpl(binary, args, {
|
|
5449
5460
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5450
5461
|
});
|
|
5451
5462
|
} catch (err) {
|
|
5452
|
-
|
|
5463
|
+
resolve7(
|
|
5453
5464
|
(0, import_types17.Err)({
|
|
5454
5465
|
category: "agent_not_found",
|
|
5455
5466
|
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
@@ -5474,9 +5485,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5474
5485
|
child.on("close", (code) => {
|
|
5475
5486
|
clearTimeout(timer);
|
|
5476
5487
|
if (code === 0) {
|
|
5477
|
-
|
|
5488
|
+
resolve7((0, import_types17.Ok)(stdout));
|
|
5478
5489
|
} else {
|
|
5479
|
-
|
|
5490
|
+
resolve7(
|
|
5480
5491
|
(0, import_types17.Err)({
|
|
5481
5492
|
category: "response_error",
|
|
5482
5493
|
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
@@ -5486,7 +5497,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
|
|
|
5486
5497
|
});
|
|
5487
5498
|
child.on("error", (err) => {
|
|
5488
5499
|
clearTimeout(timer);
|
|
5489
|
-
|
|
5500
|
+
resolve7((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
|
|
5490
5501
|
});
|
|
5491
5502
|
});
|
|
5492
5503
|
}
|
|
@@ -5546,13 +5557,13 @@ async function* readLines2(stream) {
|
|
|
5546
5557
|
if (buffer.length > 0) yield buffer;
|
|
5547
5558
|
}
|
|
5548
5559
|
function waitForExit2(child) {
|
|
5549
|
-
return new Promise((
|
|
5560
|
+
return new Promise((resolve7) => {
|
|
5550
5561
|
if (child.exitCode !== null) {
|
|
5551
|
-
|
|
5562
|
+
resolve7(child.exitCode);
|
|
5552
5563
|
return;
|
|
5553
5564
|
}
|
|
5554
|
-
child.once("close", (code) =>
|
|
5555
|
-
child.once("error", () =>
|
|
5565
|
+
child.once("close", (code) => resolve7(code));
|
|
5566
|
+
child.once("error", () => resolve7(null));
|
|
5556
5567
|
});
|
|
5557
5568
|
}
|
|
5558
5569
|
|
|
@@ -5750,13 +5761,13 @@ var ContainerBackend = class {
|
|
|
5750
5761
|
var import_node_child_process7 = require("child_process");
|
|
5751
5762
|
var import_types19 = require("@harness-engineering/types");
|
|
5752
5763
|
function dockerExec(args) {
|
|
5753
|
-
return new Promise((
|
|
5764
|
+
return new Promise((resolve7, reject) => {
|
|
5754
5765
|
(0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
|
|
5755
5766
|
if (error) {
|
|
5756
5767
|
reject(error);
|
|
5757
5768
|
return;
|
|
5758
5769
|
}
|
|
5759
|
-
|
|
5770
|
+
resolve7(stdout.trim());
|
|
5760
5771
|
});
|
|
5761
5772
|
});
|
|
5762
5773
|
}
|
|
@@ -5815,11 +5826,11 @@ var DockerRuntime = class {
|
|
|
5815
5826
|
} finally {
|
|
5816
5827
|
rl.close();
|
|
5817
5828
|
}
|
|
5818
|
-
const exitCode = await new Promise((
|
|
5829
|
+
const exitCode = await new Promise((resolve7) => {
|
|
5819
5830
|
if (child.exitCode !== null) {
|
|
5820
|
-
|
|
5831
|
+
resolve7(child.exitCode);
|
|
5821
5832
|
} else {
|
|
5822
|
-
child.on("exit", (code) =>
|
|
5833
|
+
child.on("exit", (code) => resolve7(code ?? 1));
|
|
5823
5834
|
}
|
|
5824
5835
|
});
|
|
5825
5836
|
return exitCode;
|
|
@@ -5878,13 +5889,13 @@ var EnvSecretBackend = class {
|
|
|
5878
5889
|
var import_node_child_process8 = require("child_process");
|
|
5879
5890
|
var import_types21 = require("@harness-engineering/types");
|
|
5880
5891
|
function opExec(args) {
|
|
5881
|
-
return new Promise((
|
|
5892
|
+
return new Promise((resolve7, reject) => {
|
|
5882
5893
|
(0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
|
|
5883
5894
|
if (error) {
|
|
5884
5895
|
reject(error);
|
|
5885
5896
|
return;
|
|
5886
5897
|
}
|
|
5887
|
-
|
|
5898
|
+
resolve7(stdout.trim());
|
|
5888
5899
|
});
|
|
5889
5900
|
});
|
|
5890
5901
|
}
|
|
@@ -5927,13 +5938,13 @@ var OnePasswordSecretBackend = class {
|
|
|
5927
5938
|
var import_node_child_process9 = require("child_process");
|
|
5928
5939
|
var import_types22 = require("@harness-engineering/types");
|
|
5929
5940
|
function vaultExec(args, env) {
|
|
5930
|
-
return new Promise((
|
|
5941
|
+
return new Promise((resolve7, reject) => {
|
|
5931
5942
|
(0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5932
5943
|
if (error) {
|
|
5933
5944
|
reject(error);
|
|
5934
5945
|
return;
|
|
5935
5946
|
}
|
|
5936
|
-
|
|
5947
|
+
resolve7(stdout.trim());
|
|
5937
5948
|
});
|
|
5938
5949
|
});
|
|
5939
5950
|
}
|
|
@@ -6296,8 +6307,8 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
6296
6307
|
|
|
6297
6308
|
// src/server/http.ts
|
|
6298
6309
|
var http = __toESM(require("http"));
|
|
6299
|
-
var
|
|
6300
|
-
var
|
|
6310
|
+
var path15 = __toESM(require("path"));
|
|
6311
|
+
var import_core11 = require("@harness-engineering/core");
|
|
6301
6312
|
|
|
6302
6313
|
// src/server/websocket.ts
|
|
6303
6314
|
var import_ws = require("ws");
|
|
@@ -6359,7 +6370,7 @@ var import_zod3 = require("zod");
|
|
|
6359
6370
|
// src/server/utils.ts
|
|
6360
6371
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
6361
6372
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
6362
|
-
return new Promise((
|
|
6373
|
+
return new Promise((resolve7, reject) => {
|
|
6363
6374
|
let body = "";
|
|
6364
6375
|
let bytes = 0;
|
|
6365
6376
|
req.on("data", (chunk) => {
|
|
@@ -6371,7 +6382,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
6371
6382
|
}
|
|
6372
6383
|
body += String(chunk);
|
|
6373
6384
|
});
|
|
6374
|
-
req.on("end", () =>
|
|
6385
|
+
req.on("end", () => resolve7(body));
|
|
6375
6386
|
req.on("error", reject);
|
|
6376
6387
|
});
|
|
6377
6388
|
}
|
|
@@ -7499,35 +7510,561 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
7499
7510
|
return false;
|
|
7500
7511
|
}
|
|
7501
7512
|
|
|
7502
|
-
// src/server/routes/
|
|
7503
|
-
var fs11 = __toESM(require("fs/promises"));
|
|
7504
|
-
var path11 = __toESM(require("path"));
|
|
7513
|
+
// src/server/routes/v1/proposals.ts
|
|
7505
7514
|
var import_zod13 = require("zod");
|
|
7506
|
-
var
|
|
7507
|
-
|
|
7515
|
+
var import_core10 = require("@harness-engineering/core");
|
|
7516
|
+
var import_types24 = require("@harness-engineering/types");
|
|
7517
|
+
|
|
7518
|
+
// src/proposals/gate.ts
|
|
7519
|
+
var import_yaml2 = require("yaml");
|
|
7520
|
+
var import_core8 = require("@harness-engineering/core");
|
|
7521
|
+
var GateRunError = class extends Error {
|
|
7522
|
+
constructor(message) {
|
|
7523
|
+
super(message);
|
|
7524
|
+
this.name = "GateRunError";
|
|
7525
|
+
}
|
|
7526
|
+
};
|
|
7527
|
+
var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
7528
|
+
function checkSkillYaml(yaml) {
|
|
7529
|
+
const findings = [];
|
|
7530
|
+
let doc;
|
|
7531
|
+
try {
|
|
7532
|
+
doc = (0, import_yaml2.parse)(yaml);
|
|
7533
|
+
} catch (err) {
|
|
7534
|
+
findings.push({
|
|
7535
|
+
severity: "error",
|
|
7536
|
+
title: "skill.yaml does not parse",
|
|
7537
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
7538
|
+
});
|
|
7539
|
+
return findings;
|
|
7540
|
+
}
|
|
7541
|
+
if (!doc || typeof doc !== "object") {
|
|
7542
|
+
findings.push({
|
|
7543
|
+
severity: "error",
|
|
7544
|
+
title: "skill.yaml top-level is not a mapping",
|
|
7545
|
+
detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
|
|
7546
|
+
});
|
|
7547
|
+
return findings;
|
|
7548
|
+
}
|
|
7549
|
+
const obj = doc;
|
|
7550
|
+
if (typeof obj["name"] !== "string") {
|
|
7551
|
+
findings.push({
|
|
7552
|
+
severity: "error",
|
|
7553
|
+
title: "skill.yaml missing `name`",
|
|
7554
|
+
detail: "Every skill must declare its kebab-case name."
|
|
7555
|
+
});
|
|
7556
|
+
}
|
|
7557
|
+
if (typeof obj["version"] !== "string") {
|
|
7558
|
+
findings.push({
|
|
7559
|
+
severity: "error",
|
|
7560
|
+
title: "skill.yaml missing `version`",
|
|
7561
|
+
detail: "Every skill must declare a semver version string."
|
|
7562
|
+
});
|
|
7563
|
+
}
|
|
7564
|
+
if (typeof obj["description"] !== "string") {
|
|
7565
|
+
findings.push({
|
|
7566
|
+
severity: "warning",
|
|
7567
|
+
title: "skill.yaml missing `description`",
|
|
7568
|
+
detail: "Description is strongly recommended for discoverability."
|
|
7569
|
+
});
|
|
7570
|
+
}
|
|
7571
|
+
return findings;
|
|
7572
|
+
}
|
|
7573
|
+
function checkSkillMd(md) {
|
|
7574
|
+
const findings = [];
|
|
7575
|
+
if (md.trim().length < 40) {
|
|
7576
|
+
findings.push({
|
|
7577
|
+
severity: "error",
|
|
7578
|
+
title: "SKILL.md is too short",
|
|
7579
|
+
detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
|
|
7580
|
+
});
|
|
7581
|
+
}
|
|
7582
|
+
if (!/^#\s+\S/m.test(md)) {
|
|
7583
|
+
findings.push({
|
|
7584
|
+
severity: "warning",
|
|
7585
|
+
title: "SKILL.md has no top-level heading",
|
|
7586
|
+
detail: "Convention: open SKILL.md with `# <Skill Name>`."
|
|
7587
|
+
});
|
|
7588
|
+
}
|
|
7589
|
+
return findings;
|
|
7590
|
+
}
|
|
7591
|
+
function checkName(name) {
|
|
7592
|
+
if (SKILL_NAME_RE.test(name)) return [];
|
|
7593
|
+
return [
|
|
7594
|
+
{
|
|
7595
|
+
severity: "error",
|
|
7596
|
+
title: "skill name violates the kebab-case rule",
|
|
7597
|
+
detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
|
|
7598
|
+
}
|
|
7599
|
+
];
|
|
7600
|
+
}
|
|
7601
|
+
function checkDiff(diff) {
|
|
7602
|
+
const findings = [];
|
|
7603
|
+
if (!diff.includes("---") || !diff.includes("+++")) {
|
|
7604
|
+
findings.push({
|
|
7605
|
+
severity: "error",
|
|
7606
|
+
title: "Refinement diff is not in unified-diff format",
|
|
7607
|
+
detail: "Diffs must include both `---` and `+++` headers."
|
|
7608
|
+
});
|
|
7609
|
+
}
|
|
7610
|
+
if (!/^@@\s/m.test(diff)) {
|
|
7611
|
+
findings.push({
|
|
7612
|
+
severity: "warning",
|
|
7613
|
+
title: "Refinement diff has no hunk marker",
|
|
7614
|
+
detail: "A unified diff typically contains at least one `@@` line."
|
|
7615
|
+
});
|
|
7616
|
+
}
|
|
7617
|
+
return findings;
|
|
7618
|
+
}
|
|
7619
|
+
function deriveFindings(proposal) {
|
|
7620
|
+
const findings = [];
|
|
7621
|
+
findings.push(...checkName(proposal.content.name));
|
|
7622
|
+
if (proposal.kind === "new-skill") {
|
|
7623
|
+
findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
|
|
7624
|
+
findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
|
|
7625
|
+
} else if (proposal.kind === "refinement") {
|
|
7626
|
+
findings.push(...checkDiff(proposal.content.diff ?? ""));
|
|
7627
|
+
}
|
|
7628
|
+
return findings;
|
|
7629
|
+
}
|
|
7630
|
+
async function runGate(projectPath, proposalId) {
|
|
7631
|
+
const proposal = await (0, import_core8.getProposal)(projectPath, proposalId);
|
|
7632
|
+
if (!proposal) throw new import_core8.ProposalNotFoundError(proposalId);
|
|
7633
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7634
|
+
throw new GateRunError(
|
|
7635
|
+
`proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
|
|
7636
|
+
);
|
|
7637
|
+
}
|
|
7638
|
+
const findings = deriveFindings(proposal);
|
|
7639
|
+
const runAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7640
|
+
const hasError = findings.some((f) => f.severity === "error");
|
|
7641
|
+
const nextStatus = hasError ? "gate-failed" : "gate-running";
|
|
7642
|
+
const updated = await (0, import_core8.updateProposal)(projectPath, proposalId, {
|
|
7643
|
+
status: nextStatus,
|
|
7644
|
+
gate: { lastRunAt: runAt, findings }
|
|
7645
|
+
});
|
|
7646
|
+
return {
|
|
7647
|
+
proposalId: updated.id,
|
|
7648
|
+
status: updated.status,
|
|
7649
|
+
findings,
|
|
7650
|
+
runAt
|
|
7651
|
+
};
|
|
7652
|
+
}
|
|
7653
|
+
|
|
7654
|
+
// src/proposals/promote.ts
|
|
7655
|
+
var fs11 = __toESM(require("fs"));
|
|
7656
|
+
var path11 = __toESM(require("path"));
|
|
7657
|
+
var import_yaml3 = require("yaml");
|
|
7658
|
+
var import_core9 = require("@harness-engineering/core");
|
|
7659
|
+
var GateNotReadyError = class extends Error {
|
|
7660
|
+
constructor(message) {
|
|
7661
|
+
super(message);
|
|
7662
|
+
this.name = "GateNotReadyError";
|
|
7663
|
+
}
|
|
7664
|
+
};
|
|
7665
|
+
var PromotionError = class extends Error {
|
|
7666
|
+
constructor(message) {
|
|
7667
|
+
super(message);
|
|
7668
|
+
this.name = "PromotionError";
|
|
7669
|
+
}
|
|
7670
|
+
};
|
|
7671
|
+
var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
|
|
7672
|
+
function skillDir(projectPath, name) {
|
|
7673
|
+
return path11.join(projectPath, "agents", "skills", "claude-code", name);
|
|
7674
|
+
}
|
|
7675
|
+
function readIfExists(p) {
|
|
7676
|
+
try {
|
|
7677
|
+
return fs11.readFileSync(p, "utf-8");
|
|
7678
|
+
} catch {
|
|
7679
|
+
return null;
|
|
7680
|
+
}
|
|
7681
|
+
}
|
|
7682
|
+
function injectProvenanceIntoYaml(yamlText, proposalId) {
|
|
7683
|
+
let doc;
|
|
7684
|
+
try {
|
|
7685
|
+
doc = (0, import_yaml3.parse)(yamlText);
|
|
7686
|
+
} catch (err) {
|
|
7687
|
+
throw new PromotionError(
|
|
7688
|
+
`skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
|
|
7689
|
+
);
|
|
7690
|
+
}
|
|
7691
|
+
if (!doc || typeof doc !== "object") {
|
|
7692
|
+
throw new PromotionError("skill.yaml top-level is not a mapping");
|
|
7693
|
+
}
|
|
7694
|
+
const obj = doc;
|
|
7695
|
+
obj["provenance"] = "agent-proposed";
|
|
7696
|
+
obj["originatingProposalId"] = proposalId;
|
|
7697
|
+
return (0, import_yaml3.stringify)(obj);
|
|
7698
|
+
}
|
|
7699
|
+
function assertGateReady(proposal) {
|
|
7700
|
+
if (proposal.status !== "gate-running") {
|
|
7701
|
+
throw new GateNotReadyError(
|
|
7702
|
+
`proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
|
|
7703
|
+
);
|
|
7704
|
+
}
|
|
7705
|
+
const findings = proposal.gate?.findings ?? [];
|
|
7706
|
+
if (findings.some((f) => f.severity === "error")) {
|
|
7707
|
+
throw new GateNotReadyError(
|
|
7708
|
+
`proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
|
|
7709
|
+
);
|
|
7710
|
+
}
|
|
7711
|
+
if (!proposal.gate?.lastRunAt) {
|
|
7712
|
+
throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
|
|
7713
|
+
}
|
|
7714
|
+
const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
|
|
7715
|
+
if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
|
|
7716
|
+
throw new GateNotReadyError(
|
|
7717
|
+
`proposal ${proposal.id} gate run is older than 24h; re-run before approving`
|
|
7718
|
+
);
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
async function promoteNewSkill(projectPath, proposal) {
|
|
7722
|
+
const target = skillDir(projectPath, proposal.content.name);
|
|
7723
|
+
if (fs11.existsSync(target)) {
|
|
7724
|
+
throw new PromotionError(
|
|
7725
|
+
`a catalog skill already exists at ${target}; use a refinement proposal to update it`
|
|
7726
|
+
);
|
|
7727
|
+
}
|
|
7728
|
+
fs11.mkdirSync(target, { recursive: true });
|
|
7729
|
+
const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
|
|
7730
|
+
fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
|
|
7731
|
+
fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
|
|
7732
|
+
return { skillPath: target };
|
|
7733
|
+
}
|
|
7734
|
+
async function promoteRefinement(projectPath, proposal) {
|
|
7735
|
+
if (!proposal.targetSkill) {
|
|
7736
|
+
throw new PromotionError("refinement proposal is missing targetSkill");
|
|
7737
|
+
}
|
|
7738
|
+
const target = skillDir(projectPath, proposal.targetSkill);
|
|
7739
|
+
if (!fs11.existsSync(target)) {
|
|
7740
|
+
throw new PromotionError(
|
|
7741
|
+
`target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
|
|
7742
|
+
);
|
|
7743
|
+
}
|
|
7744
|
+
const yamlPath = path11.join(target, "skill.yaml");
|
|
7745
|
+
const before = readIfExists(yamlPath) ?? "";
|
|
7746
|
+
const after = injectProvenanceIntoYaml(before, proposal.id);
|
|
7747
|
+
if (after === before) {
|
|
7748
|
+
throw new PromotionError(
|
|
7749
|
+
"no metadata changes detected; check that the reviewer applied the proposed diff before approving"
|
|
7750
|
+
);
|
|
7751
|
+
}
|
|
7752
|
+
fs11.writeFileSync(yamlPath, after);
|
|
7753
|
+
return { skillPath: target };
|
|
7754
|
+
}
|
|
7755
|
+
async function promote(projectPath, proposalId, decidedBy) {
|
|
7756
|
+
const proposal = await (0, import_core9.getProposal)(projectPath, proposalId);
|
|
7757
|
+
if (!proposal) throw new import_core9.ProposalNotFoundError(proposalId);
|
|
7758
|
+
assertGateReady(proposal);
|
|
7759
|
+
const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
|
|
7760
|
+
await (0, import_core9.updateProposal)(projectPath, proposalId, {
|
|
7761
|
+
status: "approved",
|
|
7762
|
+
decision: {
|
|
7763
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7764
|
+
decidedBy,
|
|
7765
|
+
action: "approved"
|
|
7766
|
+
}
|
|
7767
|
+
});
|
|
7768
|
+
return {
|
|
7769
|
+
proposalId,
|
|
7770
|
+
skillPath: out.skillPath,
|
|
7771
|
+
provenance: "agent-proposed"
|
|
7772
|
+
};
|
|
7773
|
+
}
|
|
7774
|
+
|
|
7775
|
+
// src/proposals/events.ts
|
|
7776
|
+
function emit3(bus, topic, data) {
|
|
7777
|
+
bus.emit(topic, data);
|
|
7778
|
+
}
|
|
7779
|
+
function emitProposalCreated(bus, proposal) {
|
|
7780
|
+
const data = {
|
|
7781
|
+
id: proposal.id,
|
|
7782
|
+
kind: proposal.kind,
|
|
7783
|
+
name: proposal.content.name,
|
|
7784
|
+
proposedBy: proposal.proposedBy,
|
|
7785
|
+
justification: proposal.source.justification
|
|
7786
|
+
};
|
|
7787
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
7788
|
+
emit3(bus, "proposal.created", data);
|
|
7789
|
+
}
|
|
7790
|
+
function emitProposalApproved(bus, proposal) {
|
|
7791
|
+
const data = {
|
|
7792
|
+
id: proposal.id,
|
|
7793
|
+
kind: proposal.kind,
|
|
7794
|
+
name: proposal.content.name,
|
|
7795
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
|
|
7796
|
+
};
|
|
7797
|
+
if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
|
|
7798
|
+
emit3(bus, "proposal.approved", data);
|
|
7799
|
+
}
|
|
7800
|
+
function emitProposalRejected(bus, proposal) {
|
|
7801
|
+
const data = {
|
|
7802
|
+
id: proposal.id,
|
|
7803
|
+
kind: proposal.kind,
|
|
7804
|
+
name: proposal.content.name,
|
|
7805
|
+
decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
|
|
7806
|
+
reason: proposal.decision?.reason ?? "(no reason given)"
|
|
7807
|
+
};
|
|
7808
|
+
emit3(bus, "proposal.rejected", data);
|
|
7809
|
+
}
|
|
7810
|
+
|
|
7811
|
+
// src/server/routes/v1/proposals.ts
|
|
7812
|
+
var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
|
|
7813
|
+
var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
|
|
7814
|
+
var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
|
|
7815
|
+
var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
|
|
7816
|
+
var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
|
|
7817
|
+
var ProposalStatusValues = [
|
|
7818
|
+
"open",
|
|
7819
|
+
"gate-running",
|
|
7820
|
+
"gate-failed",
|
|
7821
|
+
"approved",
|
|
7822
|
+
"rejected"
|
|
7823
|
+
];
|
|
7824
|
+
var RejectBody = import_zod13.z.object({
|
|
7825
|
+
reason: import_zod13.z.string().min(1).max(280)
|
|
7826
|
+
});
|
|
7827
|
+
function sendJSON8(res, status, body) {
|
|
7828
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7829
|
+
res.end(JSON.stringify(body));
|
|
7830
|
+
}
|
|
7831
|
+
function getDecidedBy(req, deps) {
|
|
7832
|
+
if (deps.decidedByResolver) return deps.decidedByResolver(req);
|
|
7833
|
+
const token = req._authToken;
|
|
7834
|
+
return token?.id ?? "unknown";
|
|
7835
|
+
}
|
|
7836
|
+
function parseStatusFromQuery(url) {
|
|
7837
|
+
const queryIdx = url.indexOf("?");
|
|
7838
|
+
if (queryIdx === -1) return void 0;
|
|
7839
|
+
const params = new URLSearchParams(url.slice(queryIdx + 1));
|
|
7840
|
+
const raw = params.get("status");
|
|
7841
|
+
if (!raw) return void 0;
|
|
7842
|
+
if (raw === "all") return "all";
|
|
7843
|
+
if (ProposalStatusValues.includes(raw)) return raw;
|
|
7844
|
+
return void 0;
|
|
7845
|
+
}
|
|
7846
|
+
async function handleList(req, res, deps) {
|
|
7847
|
+
const url = req.url ?? "";
|
|
7848
|
+
const status = parseStatusFromQuery(url);
|
|
7849
|
+
const proposals = await (0, import_core10.listProposals)(deps.projectPath, status ? { status } : {});
|
|
7850
|
+
sendJSON8(res, 200, proposals);
|
|
7851
|
+
}
|
|
7852
|
+
async function handleGet(res, deps, id) {
|
|
7853
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
7854
|
+
if (!proposal) {
|
|
7855
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7856
|
+
return;
|
|
7857
|
+
}
|
|
7858
|
+
sendJSON8(res, 200, proposal);
|
|
7859
|
+
}
|
|
7860
|
+
async function handleRunGate(res, deps, id) {
|
|
7861
|
+
try {
|
|
7862
|
+
const result = await runGate(deps.projectPath, id);
|
|
7863
|
+
sendJSON8(res, 200, result);
|
|
7864
|
+
} catch (err) {
|
|
7865
|
+
if (err instanceof import_core10.ProposalNotFoundError) {
|
|
7866
|
+
sendJSON8(res, 404, { error: err.message });
|
|
7867
|
+
return;
|
|
7868
|
+
}
|
|
7869
|
+
if (err instanceof GateRunError) {
|
|
7870
|
+
sendJSON8(res, 409, { error: err.message });
|
|
7871
|
+
return;
|
|
7872
|
+
}
|
|
7873
|
+
sendJSON8(res, 500, {
|
|
7874
|
+
error: "gate run failed",
|
|
7875
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7876
|
+
});
|
|
7877
|
+
}
|
|
7878
|
+
}
|
|
7879
|
+
async function handleApprove(req, res, deps, id) {
|
|
7880
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
7881
|
+
try {
|
|
7882
|
+
const result = await promote(deps.projectPath, id, decidedBy);
|
|
7883
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
7884
|
+
if (proposal) emitProposalApproved(deps.bus, proposal);
|
|
7885
|
+
sendJSON8(res, 200, { promotion: result, proposal });
|
|
7886
|
+
} catch (err) {
|
|
7887
|
+
if (err instanceof import_core10.ProposalNotFoundError) {
|
|
7888
|
+
sendJSON8(res, 404, { error: err.message });
|
|
7889
|
+
return;
|
|
7890
|
+
}
|
|
7891
|
+
if (err instanceof GateNotReadyError) {
|
|
7892
|
+
sendJSON8(res, 409, { error: err.message });
|
|
7893
|
+
return;
|
|
7894
|
+
}
|
|
7895
|
+
if (err instanceof PromotionError) {
|
|
7896
|
+
sendJSON8(res, 422, { error: err.message });
|
|
7897
|
+
return;
|
|
7898
|
+
}
|
|
7899
|
+
sendJSON8(res, 500, {
|
|
7900
|
+
error: "approve failed",
|
|
7901
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7902
|
+
});
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
async function handleReject(req, res, deps, id) {
|
|
7906
|
+
let raw;
|
|
7907
|
+
try {
|
|
7908
|
+
raw = await readBody(req);
|
|
7909
|
+
} catch (err) {
|
|
7910
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
7911
|
+
return;
|
|
7912
|
+
}
|
|
7913
|
+
let json;
|
|
7914
|
+
try {
|
|
7915
|
+
json = raw.length > 0 ? JSON.parse(raw) : {};
|
|
7916
|
+
} catch {
|
|
7917
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
7918
|
+
return;
|
|
7919
|
+
}
|
|
7920
|
+
const parsed = RejectBody.safeParse(json);
|
|
7921
|
+
if (!parsed.success) {
|
|
7922
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
7923
|
+
return;
|
|
7924
|
+
}
|
|
7925
|
+
const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
7926
|
+
if (!proposal) {
|
|
7927
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7928
|
+
return;
|
|
7929
|
+
}
|
|
7930
|
+
if (proposal.status === "approved" || proposal.status === "rejected") {
|
|
7931
|
+
sendJSON8(res, 409, {
|
|
7932
|
+
error: `proposal already ${proposal.status}; cannot reject`
|
|
7933
|
+
});
|
|
7934
|
+
return;
|
|
7935
|
+
}
|
|
7936
|
+
const decidedBy = getDecidedBy(req, deps);
|
|
7937
|
+
const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
|
|
7938
|
+
status: "rejected",
|
|
7939
|
+
decision: {
|
|
7940
|
+
decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7941
|
+
decidedBy,
|
|
7942
|
+
action: "rejected",
|
|
7943
|
+
reason: parsed.data.reason
|
|
7944
|
+
}
|
|
7945
|
+
});
|
|
7946
|
+
emitProposalRejected(deps.bus, updated);
|
|
7947
|
+
sendJSON8(res, 200, updated);
|
|
7948
|
+
}
|
|
7949
|
+
async function handleEdit(req, res, deps, id) {
|
|
7950
|
+
let raw;
|
|
7951
|
+
try {
|
|
7952
|
+
raw = await readBody(req);
|
|
7953
|
+
} catch (err) {
|
|
7954
|
+
sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
|
|
7955
|
+
return;
|
|
7956
|
+
}
|
|
7957
|
+
let json;
|
|
7958
|
+
try {
|
|
7959
|
+
json = JSON.parse(raw);
|
|
7960
|
+
} catch {
|
|
7961
|
+
sendJSON8(res, 400, { error: "Invalid JSON body" });
|
|
7962
|
+
return;
|
|
7963
|
+
}
|
|
7964
|
+
const parsed = import_types24.EditProposalInputSchema.safeParse(json);
|
|
7965
|
+
if (!parsed.success) {
|
|
7966
|
+
sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
|
|
7967
|
+
return;
|
|
7968
|
+
}
|
|
7969
|
+
const existing = await (0, import_core10.getProposal)(deps.projectPath, id);
|
|
7970
|
+
if (!existing) {
|
|
7971
|
+
sendJSON8(res, 404, { error: "Proposal not found" });
|
|
7972
|
+
return;
|
|
7973
|
+
}
|
|
7974
|
+
if (existing.status === "approved" || existing.status === "rejected") {
|
|
7975
|
+
sendJSON8(res, 409, {
|
|
7976
|
+
error: `proposal already ${existing.status}; cannot edit`
|
|
7977
|
+
});
|
|
7978
|
+
return;
|
|
7979
|
+
}
|
|
7980
|
+
const mergedContent = {
|
|
7981
|
+
...existing.content,
|
|
7982
|
+
...parsed.data.content,
|
|
7983
|
+
name: parsed.data.content.name ?? existing.content.name,
|
|
7984
|
+
description: parsed.data.content.description ?? existing.content.description
|
|
7985
|
+
};
|
|
7986
|
+
try {
|
|
7987
|
+
const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
|
|
7988
|
+
content: mergedContent,
|
|
7989
|
+
status: "open",
|
|
7990
|
+
gate: void 0
|
|
7991
|
+
});
|
|
7992
|
+
sendJSON8(res, 200, updated);
|
|
7993
|
+
} catch (err) {
|
|
7994
|
+
sendJSON8(res, 422, {
|
|
7995
|
+
error: "edit failed",
|
|
7996
|
+
detail: err instanceof Error ? err.message : "unknown"
|
|
7997
|
+
});
|
|
7998
|
+
}
|
|
7999
|
+
}
|
|
8000
|
+
function handleV1ProposalsRoute(req, res, deps) {
|
|
8001
|
+
const url = req.url ?? "";
|
|
8002
|
+
const method = req.method ?? "GET";
|
|
8003
|
+
if (method === "GET" && LIST_RE.test(url)) {
|
|
8004
|
+
void handleList(req, res, deps);
|
|
8005
|
+
return true;
|
|
8006
|
+
}
|
|
8007
|
+
const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
|
|
8008
|
+
if (runGateMatch) {
|
|
8009
|
+
void handleRunGate(res, deps, runGateMatch[1]);
|
|
8010
|
+
return true;
|
|
8011
|
+
}
|
|
8012
|
+
const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
|
|
8013
|
+
if (approveMatch) {
|
|
8014
|
+
void handleApprove(req, res, deps, approveMatch[1]);
|
|
8015
|
+
return true;
|
|
8016
|
+
}
|
|
8017
|
+
const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
|
|
8018
|
+
if (rejectMatch) {
|
|
8019
|
+
void handleReject(req, res, deps, rejectMatch[1]);
|
|
8020
|
+
return true;
|
|
8021
|
+
}
|
|
8022
|
+
if (method === "PATCH") {
|
|
8023
|
+
const m = SINGLE_RE.exec(url);
|
|
8024
|
+
if (m) {
|
|
8025
|
+
void handleEdit(req, res, deps, m[1]);
|
|
8026
|
+
return true;
|
|
8027
|
+
}
|
|
8028
|
+
}
|
|
8029
|
+
if (method === "GET") {
|
|
8030
|
+
const m = SINGLE_RE.exec(url);
|
|
8031
|
+
if (m) {
|
|
8032
|
+
void handleGet(res, deps, m[1]);
|
|
8033
|
+
return true;
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
return false;
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
// src/server/routes/sessions.ts
|
|
8040
|
+
var fs12 = __toESM(require("fs/promises"));
|
|
8041
|
+
var path12 = __toESM(require("path"));
|
|
8042
|
+
var import_zod14 = require("zod");
|
|
8043
|
+
var SessionCreateSchema = import_zod14.z.object({
|
|
8044
|
+
sessionId: import_zod14.z.string().min(1)
|
|
7508
8045
|
}).passthrough();
|
|
7509
8046
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7510
8047
|
function isSafeId(id) {
|
|
7511
|
-
return UUID_RE2.test(id) ||
|
|
8048
|
+
return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
|
|
7512
8049
|
}
|
|
7513
8050
|
function jsonResponse(res, status, data) {
|
|
7514
8051
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7515
8052
|
res.end(JSON.stringify(data));
|
|
7516
8053
|
}
|
|
7517
8054
|
function extractSessionId(url) {
|
|
7518
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
8055
|
+
const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
|
|
7519
8056
|
const id = segments.pop();
|
|
7520
8057
|
return id && id !== "sessions" ? id : null;
|
|
7521
8058
|
}
|
|
7522
|
-
async function
|
|
8059
|
+
async function handleList2(res, sessionsDir) {
|
|
7523
8060
|
try {
|
|
7524
|
-
const entries = await
|
|
8061
|
+
const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
|
|
7525
8062
|
const sessions = [];
|
|
7526
8063
|
for (const entry of entries) {
|
|
7527
8064
|
if (!entry.isDirectory()) continue;
|
|
7528
8065
|
try {
|
|
7529
|
-
const content = await
|
|
7530
|
-
|
|
8066
|
+
const content = await fs12.readFile(
|
|
8067
|
+
path12.join(sessionsDir, entry.name, "session.json"),
|
|
7531
8068
|
"utf-8"
|
|
7532
8069
|
);
|
|
7533
8070
|
sessions.push(JSON.parse(content));
|
|
@@ -7546,13 +8083,13 @@ async function handleList(res, sessionsDir) {
|
|
|
7546
8083
|
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
7547
8084
|
}
|
|
7548
8085
|
}
|
|
7549
|
-
async function
|
|
8086
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
7550
8087
|
if (!isSafeId(id)) {
|
|
7551
8088
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7552
8089
|
return;
|
|
7553
8090
|
}
|
|
7554
8091
|
try {
|
|
7555
|
-
const content = await
|
|
8092
|
+
const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
|
|
7556
8093
|
jsonResponse(res, 200, JSON.parse(content));
|
|
7557
8094
|
} catch (err) {
|
|
7558
8095
|
if (err.code === "ENOENT") {
|
|
@@ -7575,9 +8112,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
7575
8112
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
7576
8113
|
return;
|
|
7577
8114
|
}
|
|
7578
|
-
const sessionDir =
|
|
7579
|
-
await
|
|
7580
|
-
await
|
|
8115
|
+
const sessionDir = path12.join(sessionsDir, session.sessionId);
|
|
8116
|
+
await fs12.mkdir(sessionDir, { recursive: true });
|
|
8117
|
+
await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
|
|
7581
8118
|
jsonResponse(res, 200, { ok: true });
|
|
7582
8119
|
} catch {
|
|
7583
8120
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -7591,10 +8128,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
7591
8128
|
return;
|
|
7592
8129
|
}
|
|
7593
8130
|
const body = await readBody(req);
|
|
7594
|
-
const updates =
|
|
7595
|
-
const sessionFilePath =
|
|
7596
|
-
const current = JSON.parse(await
|
|
7597
|
-
await
|
|
8131
|
+
const updates = import_zod14.z.record(import_zod14.z.unknown()).parse(JSON.parse(body));
|
|
8132
|
+
const sessionFilePath = path12.join(sessionsDir, id, "session.json");
|
|
8133
|
+
const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
|
|
8134
|
+
await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
|
|
7598
8135
|
jsonResponse(res, 200, { ok: true });
|
|
7599
8136
|
} catch {
|
|
7600
8137
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -7607,7 +8144,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
7607
8144
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
7608
8145
|
return;
|
|
7609
8146
|
}
|
|
7610
|
-
await
|
|
8147
|
+
await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
|
|
7611
8148
|
jsonResponse(res, 200, { ok: true });
|
|
7612
8149
|
} catch {
|
|
7613
8150
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7620,8 +8157,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7620
8157
|
switch (method) {
|
|
7621
8158
|
case "GET": {
|
|
7622
8159
|
const id = extractSessionId(url);
|
|
7623
|
-
if (id) void
|
|
7624
|
-
else void
|
|
8160
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8161
|
+
else void handleList2(res, sessionsDir);
|
|
7625
8162
|
return true;
|
|
7626
8163
|
}
|
|
7627
8164
|
case "POST":
|
|
@@ -7711,16 +8248,16 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7711
8248
|
}
|
|
7712
8249
|
|
|
7713
8250
|
// src/server/routes/auth.ts
|
|
7714
|
-
var
|
|
7715
|
-
var
|
|
7716
|
-
var CreateBodySchema =
|
|
7717
|
-
name:
|
|
7718
|
-
scopes:
|
|
7719
|
-
bridgeKind:
|
|
7720
|
-
tenantId:
|
|
7721
|
-
expiresAt:
|
|
8251
|
+
var import_zod15 = require("zod");
|
|
8252
|
+
var import_types25 = require("@harness-engineering/types");
|
|
8253
|
+
var CreateBodySchema = import_zod15.z.object({
|
|
8254
|
+
name: import_zod15.z.string().min(1).max(100),
|
|
8255
|
+
scopes: import_zod15.z.array(import_types25.TokenScopeSchema).min(1),
|
|
8256
|
+
bridgeKind: import_types25.BridgeKindSchema.optional(),
|
|
8257
|
+
tenantId: import_zod15.z.string().optional(),
|
|
8258
|
+
expiresAt: import_zod15.z.string().datetime().optional()
|
|
7722
8259
|
});
|
|
7723
|
-
function
|
|
8260
|
+
function sendJSON9(res, status, body) {
|
|
7724
8261
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7725
8262
|
res.end(JSON.stringify(body));
|
|
7726
8263
|
}
|
|
@@ -7730,19 +8267,19 @@ async function handlePost(req, res, store) {
|
|
|
7730
8267
|
raw = await readBody(req);
|
|
7731
8268
|
} catch (err) {
|
|
7732
8269
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7733
|
-
|
|
8270
|
+
sendJSON9(res, 413, { error: msg });
|
|
7734
8271
|
return;
|
|
7735
8272
|
}
|
|
7736
8273
|
let json;
|
|
7737
8274
|
try {
|
|
7738
8275
|
json = JSON.parse(raw);
|
|
7739
8276
|
} catch {
|
|
7740
|
-
|
|
8277
|
+
sendJSON9(res, 400, { error: "Invalid JSON body" });
|
|
7741
8278
|
return;
|
|
7742
8279
|
}
|
|
7743
8280
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7744
8281
|
if (!parsed.success) {
|
|
7745
|
-
|
|
8282
|
+
sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7746
8283
|
return;
|
|
7747
8284
|
}
|
|
7748
8285
|
try {
|
|
@@ -7754,38 +8291,38 @@ async function handlePost(req, res, store) {
|
|
|
7754
8291
|
if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
|
|
7755
8292
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7756
8293
|
const result = await store.create(input);
|
|
7757
|
-
const publicRecord =
|
|
7758
|
-
|
|
8294
|
+
const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
|
|
8295
|
+
sendJSON9(res, 200, {
|
|
7759
8296
|
...publicRecord,
|
|
7760
8297
|
token: result.token
|
|
7761
8298
|
});
|
|
7762
8299
|
} catch (err) {
|
|
7763
8300
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7764
8301
|
if (msg.includes("already exists")) {
|
|
7765
|
-
|
|
8302
|
+
sendJSON9(res, 409, { error: msg });
|
|
7766
8303
|
return;
|
|
7767
8304
|
}
|
|
7768
|
-
|
|
8305
|
+
sendJSON9(res, 500, { error: "Internal error creating token" });
|
|
7769
8306
|
}
|
|
7770
8307
|
}
|
|
7771
|
-
async function
|
|
8308
|
+
async function handleList3(res, store) {
|
|
7772
8309
|
try {
|
|
7773
8310
|
const list = await store.list();
|
|
7774
|
-
|
|
8311
|
+
sendJSON9(res, 200, list);
|
|
7775
8312
|
} catch {
|
|
7776
|
-
|
|
8313
|
+
sendJSON9(res, 500, { error: "Internal error listing tokens" });
|
|
7777
8314
|
}
|
|
7778
8315
|
}
|
|
7779
8316
|
async function handleDelete2(res, store, id) {
|
|
7780
8317
|
try {
|
|
7781
8318
|
const ok = await store.revoke(id);
|
|
7782
8319
|
if (!ok) {
|
|
7783
|
-
|
|
8320
|
+
sendJSON9(res, 404, { error: "Token not found" });
|
|
7784
8321
|
return;
|
|
7785
8322
|
}
|
|
7786
|
-
|
|
8323
|
+
sendJSON9(res, 200, { deleted: true });
|
|
7787
8324
|
} catch {
|
|
7788
|
-
|
|
8325
|
+
sendJSON9(res, 500, { error: "Internal error revoking token" });
|
|
7789
8326
|
}
|
|
7790
8327
|
}
|
|
7791
8328
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7799,7 +8336,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7799
8336
|
return true;
|
|
7800
8337
|
}
|
|
7801
8338
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7802
|
-
void
|
|
8339
|
+
void handleList3(res, store);
|
|
7803
8340
|
return true;
|
|
7804
8341
|
}
|
|
7805
8342
|
if (method === "DELETE") {
|
|
@@ -7810,12 +8347,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7810
8347
|
return true;
|
|
7811
8348
|
}
|
|
7812
8349
|
}
|
|
7813
|
-
|
|
8350
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
7814
8351
|
return true;
|
|
7815
8352
|
}
|
|
7816
8353
|
|
|
7817
8354
|
// src/server/routes/local-model.ts
|
|
7818
|
-
function
|
|
8355
|
+
function sendJSON10(res, status, body) {
|
|
7819
8356
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7820
8357
|
res.end(JSON.stringify(body));
|
|
7821
8358
|
}
|
|
@@ -7823,36 +8360,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7823
8360
|
const { method, url } = req;
|
|
7824
8361
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7825
8362
|
if (method !== "GET") {
|
|
7826
|
-
|
|
8363
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7827
8364
|
return true;
|
|
7828
8365
|
}
|
|
7829
8366
|
if (!getStatus) {
|
|
7830
|
-
|
|
8367
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7831
8368
|
return true;
|
|
7832
8369
|
}
|
|
7833
8370
|
const status = getStatus();
|
|
7834
8371
|
if (!status) {
|
|
7835
|
-
|
|
8372
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7836
8373
|
return true;
|
|
7837
8374
|
}
|
|
7838
|
-
|
|
8375
|
+
sendJSON10(res, 200, status);
|
|
7839
8376
|
return true;
|
|
7840
8377
|
}
|
|
7841
8378
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7842
8379
|
const { method, url } = req;
|
|
7843
8380
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7844
8381
|
if (method !== "GET") {
|
|
7845
|
-
|
|
8382
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7846
8383
|
return true;
|
|
7847
8384
|
}
|
|
7848
8385
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7849
|
-
|
|
8386
|
+
sendJSON10(res, 200, statuses);
|
|
7850
8387
|
return true;
|
|
7851
8388
|
}
|
|
7852
8389
|
|
|
7853
8390
|
// src/server/static.ts
|
|
7854
|
-
var
|
|
7855
|
-
var
|
|
8391
|
+
var fs13 = __toESM(require("fs"));
|
|
8392
|
+
var path13 = __toESM(require("path"));
|
|
7856
8393
|
var MIME_TYPES = {
|
|
7857
8394
|
".html": "text/html; charset=utf-8",
|
|
7858
8395
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7872,29 +8409,29 @@ var MIME_TYPES = {
|
|
|
7872
8409
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7873
8410
|
const { method, url } = req;
|
|
7874
8411
|
if (method !== "GET") return false;
|
|
7875
|
-
const apiPrefix =
|
|
7876
|
-
const wsPath =
|
|
8412
|
+
const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
|
|
8413
|
+
const wsPath = path13.posix.join(path13.posix.sep, "ws");
|
|
7877
8414
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7878
8415
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7879
|
-
const requestedPath =
|
|
7880
|
-
const resolved =
|
|
7881
|
-
if (!resolved.startsWith(
|
|
7882
|
-
return serveFile(
|
|
8416
|
+
const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
|
|
8417
|
+
const resolved = path13.resolve(requestedPath);
|
|
8418
|
+
if (!resolved.startsWith(path13.resolve(dashboardDir))) {
|
|
8419
|
+
return serveFile(path13.join(dashboardDir, "index.html"), res);
|
|
7883
8420
|
}
|
|
7884
|
-
if (
|
|
8421
|
+
if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
|
|
7885
8422
|
return serveFile(resolved, res);
|
|
7886
8423
|
}
|
|
7887
|
-
const indexPath =
|
|
7888
|
-
if (
|
|
8424
|
+
const indexPath = path13.join(dashboardDir, "index.html");
|
|
8425
|
+
if (fs13.existsSync(indexPath)) {
|
|
7889
8426
|
return serveFile(indexPath, res);
|
|
7890
8427
|
}
|
|
7891
8428
|
return false;
|
|
7892
8429
|
}
|
|
7893
8430
|
function serveFile(filePath, res) {
|
|
7894
|
-
const ext =
|
|
8431
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
7895
8432
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7896
8433
|
try {
|
|
7897
|
-
const content =
|
|
8434
|
+
const content = fs13.readFileSync(filePath);
|
|
7898
8435
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7899
8436
|
res.end(content);
|
|
7900
8437
|
return true;
|
|
@@ -7904,8 +8441,8 @@ function serveFile(filePath, res) {
|
|
|
7904
8441
|
}
|
|
7905
8442
|
|
|
7906
8443
|
// src/server/plan-watcher.ts
|
|
7907
|
-
var
|
|
7908
|
-
var
|
|
8444
|
+
var fs14 = __toESM(require("fs"));
|
|
8445
|
+
var path14 = __toESM(require("path"));
|
|
7909
8446
|
var PlanWatcher = class {
|
|
7910
8447
|
plansDir;
|
|
7911
8448
|
queue;
|
|
@@ -7919,11 +8456,11 @@ var PlanWatcher = class {
|
|
|
7919
8456
|
* Creates the directory if it does not exist.
|
|
7920
8457
|
*/
|
|
7921
8458
|
start() {
|
|
7922
|
-
|
|
7923
|
-
this.watcher =
|
|
8459
|
+
fs14.mkdirSync(this.plansDir, { recursive: true });
|
|
8460
|
+
this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
|
|
7924
8461
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7925
|
-
const filePath =
|
|
7926
|
-
if (
|
|
8462
|
+
const filePath = path14.join(this.plansDir, filename);
|
|
8463
|
+
if (fs14.existsSync(filePath)) {
|
|
7927
8464
|
void this.handleNewPlan(filename);
|
|
7928
8465
|
}
|
|
7929
8466
|
}
|
|
@@ -7958,7 +8495,7 @@ var import_node_crypto9 = require("crypto");
|
|
|
7958
8495
|
var import_promises = require("fs/promises");
|
|
7959
8496
|
var import_node_path = require("path");
|
|
7960
8497
|
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
7961
|
-
var
|
|
8498
|
+
var import_types26 = require("@harness-engineering/types");
|
|
7962
8499
|
var BCRYPT_ROUNDS = 12;
|
|
7963
8500
|
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
7964
8501
|
function genId() {
|
|
@@ -7973,8 +8510,8 @@ function parseToken(raw) {
|
|
|
7973
8510
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7974
8511
|
}
|
|
7975
8512
|
var TokenStore = class {
|
|
7976
|
-
constructor(
|
|
7977
|
-
this.path =
|
|
8513
|
+
constructor(path22) {
|
|
8514
|
+
this.path = path22;
|
|
7978
8515
|
}
|
|
7979
8516
|
path;
|
|
7980
8517
|
cache = null;
|
|
@@ -7985,7 +8522,7 @@ var TokenStore = class {
|
|
|
7985
8522
|
const parsed = JSON.parse(raw);
|
|
7986
8523
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
7987
8524
|
this.cache = list.map((entry) => {
|
|
7988
|
-
const r =
|
|
8525
|
+
const r = import_types26.AuthTokenSchema.safeParse(entry);
|
|
7989
8526
|
return r.success ? r.data : null;
|
|
7990
8527
|
}).filter((x) => x !== null);
|
|
7991
8528
|
} catch (err) {
|
|
@@ -8047,7 +8584,7 @@ var TokenStore = class {
|
|
|
8047
8584
|
}
|
|
8048
8585
|
async list() {
|
|
8049
8586
|
const records = await this.load();
|
|
8050
|
-
return records.map((r) =>
|
|
8587
|
+
return records.map((r) => import_types26.AuthTokenPublicSchema.parse(r));
|
|
8051
8588
|
}
|
|
8052
8589
|
async revoke(id) {
|
|
8053
8590
|
const records = await this.load();
|
|
@@ -8079,10 +8616,10 @@ var TokenStore = class {
|
|
|
8079
8616
|
// src/auth/audit.ts
|
|
8080
8617
|
var import_promises2 = require("fs/promises");
|
|
8081
8618
|
var import_node_path2 = require("path");
|
|
8082
|
-
var
|
|
8619
|
+
var import_types27 = require("@harness-engineering/types");
|
|
8083
8620
|
var AuditLogger = class {
|
|
8084
|
-
constructor(
|
|
8085
|
-
this.path =
|
|
8621
|
+
constructor(path22, opts = {}) {
|
|
8622
|
+
this.path = path22;
|
|
8086
8623
|
this.opts = opts;
|
|
8087
8624
|
}
|
|
8088
8625
|
path;
|
|
@@ -8090,7 +8627,7 @@ var AuditLogger = class {
|
|
|
8090
8627
|
queue = Promise.resolve();
|
|
8091
8628
|
dirEnsured = false;
|
|
8092
8629
|
async append(input) {
|
|
8093
|
-
const entry =
|
|
8630
|
+
const entry = import_types27.AuthAuditEntrySchema.parse({
|
|
8094
8631
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8095
8632
|
tokenId: input.tokenId,
|
|
8096
8633
|
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
@@ -8166,6 +8703,43 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8166
8703
|
scope: "subscribe-webhook",
|
|
8167
8704
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
8168
8705
|
},
|
|
8706
|
+
// Hermes Phase 4 — skill proposal review queue.
|
|
8707
|
+
{
|
|
8708
|
+
method: "GET",
|
|
8709
|
+
pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
|
|
8710
|
+
scope: "read-status",
|
|
8711
|
+
description: "List skill proposals (open + decided)."
|
|
8712
|
+
},
|
|
8713
|
+
{
|
|
8714
|
+
method: "GET",
|
|
8715
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
8716
|
+
scope: "read-status",
|
|
8717
|
+
description: "Get a single skill proposal."
|
|
8718
|
+
},
|
|
8719
|
+
{
|
|
8720
|
+
method: "POST",
|
|
8721
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
|
|
8722
|
+
scope: "manage-proposals",
|
|
8723
|
+
description: "Run the soundness-review gate against a proposal."
|
|
8724
|
+
},
|
|
8725
|
+
{
|
|
8726
|
+
method: "POST",
|
|
8727
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
|
|
8728
|
+
scope: "manage-proposals",
|
|
8729
|
+
description: "Approve a proposal \u2014 promotes the skill into the catalog."
|
|
8730
|
+
},
|
|
8731
|
+
{
|
|
8732
|
+
method: "POST",
|
|
8733
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
|
|
8734
|
+
scope: "manage-proposals",
|
|
8735
|
+
description: "Reject a proposal with a one-line reason."
|
|
8736
|
+
},
|
|
8737
|
+
{
|
|
8738
|
+
method: "PATCH",
|
|
8739
|
+
pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
|
|
8740
|
+
scope: "manage-proposals",
|
|
8741
|
+
description: "Edit proposal content (resets gate to not-run)."
|
|
8742
|
+
},
|
|
8169
8743
|
// ── Phase 5 bridge primitives ──
|
|
8170
8744
|
{
|
|
8171
8745
|
method: "GET",
|
|
@@ -8177,9 +8751,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
8177
8751
|
function isV1Bridge(method, url) {
|
|
8178
8752
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
8179
8753
|
}
|
|
8180
|
-
function requiredBridgeScope(method,
|
|
8754
|
+
function requiredBridgeScope(method, path22) {
|
|
8181
8755
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
8182
|
-
if (r.method === method && r.pattern.test(
|
|
8756
|
+
if (r.method === method && r.pattern.test(path22)) return r.scope;
|
|
8183
8757
|
}
|
|
8184
8758
|
return null;
|
|
8185
8759
|
}
|
|
@@ -8189,24 +8763,24 @@ function hasScope(held, required) {
|
|
|
8189
8763
|
if (held.includes("admin")) return true;
|
|
8190
8764
|
return held.includes(required);
|
|
8191
8765
|
}
|
|
8192
|
-
function requiredScopeForRoute(method,
|
|
8193
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
8766
|
+
function requiredScopeForRoute(method, path22) {
|
|
8767
|
+
const bridgeScope = requiredBridgeScope(method, path22);
|
|
8194
8768
|
if (bridgeScope) return bridgeScope;
|
|
8195
|
-
if (
|
|
8196
|
-
if (
|
|
8197
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
8198
|
-
if ((
|
|
8199
|
-
if (
|
|
8200
|
-
if (
|
|
8201
|
-
if (
|
|
8202
|
-
if (
|
|
8203
|
-
if (
|
|
8204
|
-
if (
|
|
8769
|
+
if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
8770
|
+
if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
8771
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
|
|
8772
|
+
if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
|
|
8773
|
+
if (path22.startsWith("/api/interactions")) return "resolve-interaction";
|
|
8774
|
+
if (path22.startsWith("/api/plans")) return "read-status";
|
|
8775
|
+
if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
|
|
8776
|
+
if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
8777
|
+
if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
8778
|
+
if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
|
|
8205
8779
|
return "read-status";
|
|
8206
|
-
if (
|
|
8207
|
-
if (
|
|
8208
|
-
if (
|
|
8209
|
-
if (
|
|
8780
|
+
if (path22.startsWith("/api/maintenance")) return "trigger-job";
|
|
8781
|
+
if (path22.startsWith("/api/streams")) return "read-status";
|
|
8782
|
+
if (path22.startsWith("/api/sessions")) return "read-status";
|
|
8783
|
+
if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
8210
8784
|
return null;
|
|
8211
8785
|
}
|
|
8212
8786
|
|
|
@@ -8260,6 +8834,11 @@ var OrchestratorServer = class {
|
|
|
8260
8834
|
roadmapPath;
|
|
8261
8835
|
dispatchAdHoc;
|
|
8262
8836
|
sessionsDir;
|
|
8837
|
+
/**
|
|
8838
|
+
* Project root used by file-backed routes (Phase 4 proposals at
|
|
8839
|
+
* `.harness/proposals/`). Defaults to process.cwd().
|
|
8840
|
+
*/
|
|
8841
|
+
projectPath;
|
|
8263
8842
|
maintenanceDeps = null;
|
|
8264
8843
|
getLocalModelStatus = null;
|
|
8265
8844
|
getLocalModelStatuses = null;
|
|
@@ -8277,8 +8856,8 @@ var OrchestratorServer = class {
|
|
|
8277
8856
|
this.orchestrator = orchestrator;
|
|
8278
8857
|
this.port = port;
|
|
8279
8858
|
this.initDependencies(deps);
|
|
8280
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
8281
|
-
const auditPath = process.env["HARNESS_AUDIT_PATH"] ??
|
|
8859
|
+
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
|
|
8860
|
+
const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
|
|
8282
8861
|
this.tokenStore = new TokenStore(tokensPath);
|
|
8283
8862
|
this.auditLogger = new AuditLogger(auditPath);
|
|
8284
8863
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -8291,14 +8870,15 @@ var OrchestratorServer = class {
|
|
|
8291
8870
|
}
|
|
8292
8871
|
initDependencies(deps) {
|
|
8293
8872
|
this.interactionQueue = deps?.interactionQueue;
|
|
8294
|
-
this.plansDir = deps?.plansDir ??
|
|
8295
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
8873
|
+
this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
|
|
8874
|
+
this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
|
|
8296
8875
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
8297
8876
|
this.pipeline = deps?.pipeline ?? null;
|
|
8298
8877
|
this.analysisArchive = deps?.analysisArchive;
|
|
8299
8878
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
8300
8879
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
8301
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
8880
|
+
this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
|
|
8881
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
8302
8882
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
8303
8883
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
8304
8884
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -8469,6 +9049,15 @@ var OrchestratorServer = class {
|
|
|
8469
9049
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
8470
9050
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
8471
9051
|
}),
|
|
9052
|
+
// Hermes Phase 4 — skill proposal review queue. Read scopes
|
|
9053
|
+
// (`read-status`) and write scopes (`manage-proposals`) are enforced
|
|
9054
|
+
// upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
|
|
9055
|
+
// business logic. `projectPath` defaults to process.cwd() — that is
|
|
9056
|
+
// where `.harness/proposals/` lives in every deployment we ship.
|
|
9057
|
+
(req, res) => handleV1ProposalsRoute(req, res, {
|
|
9058
|
+
projectPath: this.projectPath,
|
|
9059
|
+
bus: this.orchestrator
|
|
9060
|
+
}),
|
|
8472
9061
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
8473
9062
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
8474
9063
|
];
|
|
@@ -8551,16 +9140,16 @@ var OrchestratorServer = class {
|
|
|
8551
9140
|
return this.broadcaster.clientCount;
|
|
8552
9141
|
}
|
|
8553
9142
|
async start() {
|
|
8554
|
-
(0,
|
|
9143
|
+
(0, import_core11.assertPortUsable)(this.port, "orchestrator");
|
|
8555
9144
|
if (this.interactionQueue) {
|
|
8556
9145
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
8557
9146
|
this.planWatcher.start();
|
|
8558
9147
|
}
|
|
8559
|
-
return new Promise((
|
|
9148
|
+
return new Promise((resolve7) => {
|
|
8560
9149
|
const host = getBindHost();
|
|
8561
9150
|
this.httpServer.listen(this.port, host, () => {
|
|
8562
9151
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
8563
|
-
|
|
9152
|
+
resolve7();
|
|
8564
9153
|
});
|
|
8565
9154
|
});
|
|
8566
9155
|
}
|
|
@@ -8580,7 +9169,7 @@ var OrchestratorServer = class {
|
|
|
8580
9169
|
var import_node_crypto11 = require("crypto");
|
|
8581
9170
|
var import_promises3 = require("fs/promises");
|
|
8582
9171
|
var import_node_path3 = require("path");
|
|
8583
|
-
var
|
|
9172
|
+
var import_types28 = require("@harness-engineering/types");
|
|
8584
9173
|
|
|
8585
9174
|
// src/gateway/webhooks/signer.ts
|
|
8586
9175
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8610,8 +9199,8 @@ function genSecret2() {
|
|
|
8610
9199
|
return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
|
|
8611
9200
|
}
|
|
8612
9201
|
var WebhookStore = class {
|
|
8613
|
-
constructor(
|
|
8614
|
-
this.path =
|
|
9202
|
+
constructor(path22) {
|
|
9203
|
+
this.path = path22;
|
|
8615
9204
|
}
|
|
8616
9205
|
path;
|
|
8617
9206
|
cache = null;
|
|
@@ -8622,7 +9211,7 @@ var WebhookStore = class {
|
|
|
8622
9211
|
const parsed = JSON.parse(raw);
|
|
8623
9212
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
8624
9213
|
this.cache = list.map((entry) => {
|
|
8625
|
-
const r =
|
|
9214
|
+
const r = import_types28.WebhookSubscriptionSchema.safeParse(entry);
|
|
8626
9215
|
return r.success ? r.data : null;
|
|
8627
9216
|
}).filter((x) => x !== null);
|
|
8628
9217
|
} catch (err) {
|
|
@@ -9002,7 +9591,12 @@ var WEBHOOK_TOPICS = [
|
|
|
9002
9591
|
"maintenance:completed",
|
|
9003
9592
|
"maintenance:error",
|
|
9004
9593
|
"webhook.subscription.created",
|
|
9005
|
-
"webhook.subscription.deleted"
|
|
9594
|
+
"webhook.subscription.deleted",
|
|
9595
|
+
// Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
|
|
9596
|
+
// `proposal.*` glob pattern to receive all three.
|
|
9597
|
+
"proposal.created",
|
|
9598
|
+
"proposal.approved",
|
|
9599
|
+
"proposal.rejected"
|
|
9006
9600
|
];
|
|
9007
9601
|
function newEventId2() {
|
|
9008
9602
|
return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
|
|
@@ -9036,7 +9630,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
9036
9630
|
|
|
9037
9631
|
// src/gateway/telemetry/fanout.ts
|
|
9038
9632
|
var import_node_crypto14 = require("crypto");
|
|
9039
|
-
var
|
|
9633
|
+
var import_core12 = require("@harness-engineering/core");
|
|
9040
9634
|
var TOPICS = {
|
|
9041
9635
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
9042
9636
|
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
@@ -9171,7 +9765,7 @@ function wireTelemetryFanout(params) {
|
|
|
9171
9765
|
spanId,
|
|
9172
9766
|
...parentSpanId !== void 0 ? { parentSpanId } : {},
|
|
9173
9767
|
name: SPAN_NAME[topic],
|
|
9174
|
-
kind:
|
|
9768
|
+
kind: import_core12.SpanKind.INTERNAL,
|
|
9175
9769
|
startTimeNs: startNs,
|
|
9176
9770
|
endTimeNs: startNs,
|
|
9177
9771
|
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
@@ -9427,6 +10021,33 @@ var ENVELOPE_DERIVERS = {
|
|
|
9427
10021
|
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9428
10022
|
severity: "info"
|
|
9429
10023
|
};
|
|
10024
|
+
},
|
|
10025
|
+
// Hermes Phase 4 — skill proposal lifecycle events.
|
|
10026
|
+
"proposal.created": (event) => {
|
|
10027
|
+
const data = asObj(event.data);
|
|
10028
|
+
const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
|
|
10029
|
+
return {
|
|
10030
|
+
title: `New skill proposal: ${label}`,
|
|
10031
|
+
summary: truncate(data.justification ?? "No justification provided.", 240),
|
|
10032
|
+
severity: "info"
|
|
10033
|
+
};
|
|
10034
|
+
},
|
|
10035
|
+
"proposal.approved": (event) => {
|
|
10036
|
+
const data = asObj(event.data);
|
|
10037
|
+
const label = data.name ?? data.targetSkill ?? "(unknown skill)";
|
|
10038
|
+
return {
|
|
10039
|
+
title: `Skill proposal approved: ${label}`,
|
|
10040
|
+
summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
|
|
10041
|
+
severity: "success"
|
|
10042
|
+
};
|
|
10043
|
+
},
|
|
10044
|
+
"proposal.rejected": (event) => {
|
|
10045
|
+
const data = asObj(event.data);
|
|
10046
|
+
return {
|
|
10047
|
+
title: "Skill proposal rejected",
|
|
10048
|
+
summary: truncate(data.reason ?? "No reason provided.", 240),
|
|
10049
|
+
severity: "warning"
|
|
10050
|
+
};
|
|
9430
10051
|
}
|
|
9431
10052
|
};
|
|
9432
10053
|
function truncate(s, max) {
|
|
@@ -9471,7 +10092,11 @@ var NOTIFICATION_TOPICS = [
|
|
|
9471
10092
|
"interaction.resolved",
|
|
9472
10093
|
"maintenance:started",
|
|
9473
10094
|
"maintenance:completed",
|
|
9474
|
-
"maintenance:error"
|
|
10095
|
+
"maintenance:error",
|
|
10096
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10097
|
+
"proposal.created",
|
|
10098
|
+
"proposal.approved",
|
|
10099
|
+
"proposal.rejected"
|
|
9475
10100
|
];
|
|
9476
10101
|
function newEventId4() {
|
|
9477
10102
|
return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
|
|
@@ -9527,7 +10152,7 @@ function wireNotificationSinks({ bus, registry }) {
|
|
|
9527
10152
|
}
|
|
9528
10153
|
|
|
9529
10154
|
// src/orchestrator.ts
|
|
9530
|
-
var
|
|
10155
|
+
var import_core16 = require("@harness-engineering/core");
|
|
9531
10156
|
|
|
9532
10157
|
// src/logging/logger.ts
|
|
9533
10158
|
var StructuredLogger = class {
|
|
@@ -9569,7 +10194,7 @@ var StructuredLogger = class {
|
|
|
9569
10194
|
// src/workspace/config-scanner.ts
|
|
9570
10195
|
var import_node_fs = require("fs");
|
|
9571
10196
|
var import_node_path4 = require("path");
|
|
9572
|
-
var
|
|
10197
|
+
var import_core13 = require("@harness-engineering/core");
|
|
9573
10198
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
9574
10199
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
9575
10200
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -9591,25 +10216,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
9591
10216
|
} catch {
|
|
9592
10217
|
return null;
|
|
9593
10218
|
}
|
|
9594
|
-
const injectionFindings = (0,
|
|
9595
|
-
const findings = (0,
|
|
10219
|
+
const injectionFindings = (0, import_core13.scanForInjection)(content);
|
|
10220
|
+
const findings = (0, import_core13.mapInjectionFindings)(injectionFindings);
|
|
9596
10221
|
const secFindings = await scanner.scanFile(filePath);
|
|
9597
|
-
findings.push(...(0,
|
|
10222
|
+
findings.push(...(0, import_core13.mapSecurityFindings)(secFindings, findings));
|
|
9598
10223
|
const adjusted = adjustFindingSeverity(findings);
|
|
9599
10224
|
return {
|
|
9600
10225
|
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
9601
10226
|
findings: adjusted,
|
|
9602
|
-
overallSeverity: (0,
|
|
10227
|
+
overallSeverity: (0, import_core13.computeOverallSeverity)(adjusted)
|
|
9603
10228
|
};
|
|
9604
10229
|
}
|
|
9605
10230
|
async function scanWorkspaceConfig(workspacePath) {
|
|
9606
|
-
const scanner = new
|
|
10231
|
+
const scanner = new import_core13.SecurityScanner((0, import_core13.parseSecurityConfig)({}));
|
|
9607
10232
|
const results = [];
|
|
9608
10233
|
for (const configFile of CONFIG_FILES) {
|
|
9609
10234
|
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
9610
10235
|
if (result) results.push(result);
|
|
9611
10236
|
}
|
|
9612
|
-
return { exitCode: (0,
|
|
10237
|
+
return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
|
|
9613
10238
|
}
|
|
9614
10239
|
|
|
9615
10240
|
// src/maintenance/task-registry.ts
|
|
@@ -9792,6 +10417,19 @@ var BUILT_IN_TASKS = [
|
|
|
9792
10417
|
schedule: "*/15 * * * *",
|
|
9793
10418
|
branch: null,
|
|
9794
10419
|
checkCommand: ["harness", "sync-main", "--json"]
|
|
10420
|
+
},
|
|
10421
|
+
// Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
|
|
10422
|
+
// on every existing catalog skill. Schedule is Feb 31 (a date that never
|
|
10423
|
+
// exists) so the cron loop never fires it automatically; operators trigger
|
|
10424
|
+
// it once via the dashboard "Run now" button or `harness backfill-skill-
|
|
10425
|
+
// provenance` after upgrading to Phase 4.
|
|
10426
|
+
{
|
|
10427
|
+
id: "proposal-provenance-backfill",
|
|
10428
|
+
type: "housekeeping",
|
|
10429
|
+
description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
|
|
10430
|
+
schedule: "0 0 31 2 *",
|
|
10431
|
+
branch: null,
|
|
10432
|
+
checkCommand: ["backfill-skill-provenance"]
|
|
9795
10433
|
}
|
|
9796
10434
|
];
|
|
9797
10435
|
|
|
@@ -9884,24 +10522,49 @@ var MaintenanceScheduler = class {
|
|
|
9884
10522
|
this.resolvedTasks = this.resolveTasks();
|
|
9885
10523
|
}
|
|
9886
10524
|
/**
|
|
9887
|
-
* Merge built-in task definitions with config overrides
|
|
9888
|
-
*
|
|
9889
|
-
*
|
|
10525
|
+
* Merge built-in task definitions with config overrides, then append
|
|
10526
|
+
* Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
|
|
10527
|
+
* overrides). Tasks with `enabled: false` are filtered out. Schedule
|
|
10528
|
+
* overrides replace the default cron expression.
|
|
9890
10529
|
*/
|
|
9891
10530
|
resolveTasks() {
|
|
9892
10531
|
const overrides = this.config.tasks ?? {};
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
return true;
|
|
9897
|
-
}).map((task) => {
|
|
10532
|
+
const customs = this.config.customTasks ?? {};
|
|
10533
|
+
const merged = [];
|
|
10534
|
+
for (const task of BUILT_IN_TASKS) {
|
|
9898
10535
|
const override = overrides[task.id];
|
|
9899
|
-
if (
|
|
9900
|
-
|
|
10536
|
+
if (override?.enabled === false) continue;
|
|
10537
|
+
merged.push({
|
|
9901
10538
|
...task,
|
|
9902
|
-
...override
|
|
9903
|
-
};
|
|
9904
|
-
}
|
|
10539
|
+
...override?.schedule !== void 0 && { schedule: override.schedule }
|
|
10540
|
+
});
|
|
10541
|
+
}
|
|
10542
|
+
for (const [id, def] of Object.entries(customs)) {
|
|
10543
|
+
const override = overrides[id];
|
|
10544
|
+
if (override?.enabled === false) continue;
|
|
10545
|
+
merged.push({
|
|
10546
|
+
id,
|
|
10547
|
+
type: def.type,
|
|
10548
|
+
description: def.description,
|
|
10549
|
+
schedule: override?.schedule ?? def.schedule,
|
|
10550
|
+
branch: def.branch,
|
|
10551
|
+
...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
|
|
10552
|
+
...def.checkScript !== void 0 && { checkScript: def.checkScript },
|
|
10553
|
+
...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
|
|
10554
|
+
...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
|
|
10555
|
+
...def.inlineSkillsBudgetTokens !== void 0 && {
|
|
10556
|
+
inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
|
|
10557
|
+
},
|
|
10558
|
+
...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
|
|
10559
|
+
...def.contextFromMaxAgeMinutes !== void 0 && {
|
|
10560
|
+
contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
|
|
10561
|
+
},
|
|
10562
|
+
...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
|
|
10563
|
+
...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
|
|
10564
|
+
isCustom: true
|
|
10565
|
+
});
|
|
10566
|
+
}
|
|
10567
|
+
return merged;
|
|
9905
10568
|
}
|
|
9906
10569
|
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
9907
10570
|
getResolvedTasks() {
|
|
@@ -10074,27 +10737,27 @@ var MaintenanceScheduler = class {
|
|
|
10074
10737
|
};
|
|
10075
10738
|
|
|
10076
10739
|
// src/maintenance/leader-elector.ts
|
|
10077
|
-
var
|
|
10740
|
+
var import_types29 = require("@harness-engineering/types");
|
|
10078
10741
|
var SingleProcessLeaderElector = class {
|
|
10079
10742
|
async electLeader() {
|
|
10080
|
-
return (0,
|
|
10743
|
+
return (0, import_types29.Ok)("claimed");
|
|
10081
10744
|
}
|
|
10082
10745
|
};
|
|
10083
10746
|
|
|
10084
10747
|
// src/maintenance/reporter.ts
|
|
10085
|
-
var
|
|
10086
|
-
var
|
|
10087
|
-
var
|
|
10088
|
-
var RunResultSchema =
|
|
10089
|
-
taskId:
|
|
10090
|
-
startedAt:
|
|
10091
|
-
completedAt:
|
|
10092
|
-
status:
|
|
10093
|
-
findings:
|
|
10094
|
-
fixed:
|
|
10095
|
-
prUrl:
|
|
10096
|
-
prUpdated:
|
|
10097
|
-
error:
|
|
10748
|
+
var fs15 = __toESM(require("fs"));
|
|
10749
|
+
var path16 = __toESM(require("path"));
|
|
10750
|
+
var import_zod16 = require("zod");
|
|
10751
|
+
var RunResultSchema = import_zod16.z.object({
|
|
10752
|
+
taskId: import_zod16.z.string(),
|
|
10753
|
+
startedAt: import_zod16.z.string(),
|
|
10754
|
+
completedAt: import_zod16.z.string(),
|
|
10755
|
+
status: import_zod16.z.enum(["success", "failure", "skipped", "no-issues"]),
|
|
10756
|
+
findings: import_zod16.z.number(),
|
|
10757
|
+
fixed: import_zod16.z.number(),
|
|
10758
|
+
prUrl: import_zod16.z.string().nullable(),
|
|
10759
|
+
prUpdated: import_zod16.z.boolean(),
|
|
10760
|
+
error: import_zod16.z.string().optional()
|
|
10098
10761
|
});
|
|
10099
10762
|
var MAX_HISTORY = 500;
|
|
10100
10763
|
var fallbackLogger = {
|
|
@@ -10118,10 +10781,10 @@ var MaintenanceReporter = class {
|
|
|
10118
10781
|
*/
|
|
10119
10782
|
async load() {
|
|
10120
10783
|
try {
|
|
10121
|
-
await
|
|
10122
|
-
const filePath =
|
|
10123
|
-
const data = await
|
|
10124
|
-
const parsed =
|
|
10784
|
+
await fs15.promises.mkdir(this.persistDir, { recursive: true });
|
|
10785
|
+
const filePath = path16.join(this.persistDir, "history.json");
|
|
10786
|
+
const data = await fs15.promises.readFile(filePath, "utf-8");
|
|
10787
|
+
const parsed = import_zod16.z.array(RunResultSchema).safeParse(JSON.parse(data));
|
|
10125
10788
|
if (parsed.success) {
|
|
10126
10789
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
10127
10790
|
}
|
|
@@ -10154,9 +10817,9 @@ var MaintenanceReporter = class {
|
|
|
10154
10817
|
*/
|
|
10155
10818
|
async persist() {
|
|
10156
10819
|
try {
|
|
10157
|
-
await
|
|
10158
|
-
const filePath =
|
|
10159
|
-
await
|
|
10820
|
+
await fs15.promises.mkdir(this.persistDir, { recursive: true });
|
|
10821
|
+
const filePath = path16.join(this.persistDir, "history.json");
|
|
10822
|
+
await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
|
|
10160
10823
|
} catch (err) {
|
|
10161
10824
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
10162
10825
|
}
|
|
@@ -10172,6 +10835,9 @@ var TaskRunner = class {
|
|
|
10172
10835
|
cwd;
|
|
10173
10836
|
prManager;
|
|
10174
10837
|
baseBranch;
|
|
10838
|
+
checkScriptRunner;
|
|
10839
|
+
contextResolver;
|
|
10840
|
+
outputStore;
|
|
10175
10841
|
constructor(options) {
|
|
10176
10842
|
this.config = options.config;
|
|
10177
10843
|
this.checkRunner = options.checkRunner;
|
|
@@ -10180,27 +10846,49 @@ var TaskRunner = class {
|
|
|
10180
10846
|
this.cwd = options.cwd;
|
|
10181
10847
|
this.prManager = options.prManager ?? null;
|
|
10182
10848
|
this.baseBranch = options.baseBranch ?? "main";
|
|
10849
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
10850
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
10851
|
+
this.outputStore = options.outputStore ?? null;
|
|
10183
10852
|
}
|
|
10184
10853
|
/**
|
|
10185
10854
|
* Run a maintenance task and return the result.
|
|
10186
10855
|
* Dispatches to the appropriate execution path based on task type.
|
|
10187
10856
|
* Never throws -- errors are captured in the RunResult.
|
|
10857
|
+
*
|
|
10858
|
+
* @param task - Resolved task definition.
|
|
10859
|
+
* @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
|
|
10860
|
+
* when called from the scheduler path.
|
|
10188
10861
|
*/
|
|
10189
|
-
async run(task) {
|
|
10862
|
+
async run(task, origin = "cron") {
|
|
10190
10863
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10864
|
+
let result;
|
|
10865
|
+
let captured;
|
|
10191
10866
|
try {
|
|
10192
10867
|
switch (task.type) {
|
|
10193
|
-
case "mechanical-ai":
|
|
10194
|
-
|
|
10868
|
+
case "mechanical-ai": {
|
|
10869
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
10870
|
+
result = out.result;
|
|
10871
|
+
captured = out.captured;
|
|
10872
|
+
break;
|
|
10873
|
+
}
|
|
10195
10874
|
case "pure-ai":
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10875
|
+
result = await this.runPureAI(task, startedAt);
|
|
10876
|
+
break;
|
|
10877
|
+
case "report-only": {
|
|
10878
|
+
const out = await this.runReportOnly(task, startedAt);
|
|
10879
|
+
result = out.result;
|
|
10880
|
+
captured = out.captured;
|
|
10881
|
+
break;
|
|
10882
|
+
}
|
|
10883
|
+
case "housekeeping": {
|
|
10884
|
+
const out = await this.runHousekeeping(task, startedAt);
|
|
10885
|
+
result = out.result;
|
|
10886
|
+
captured = out.captured;
|
|
10887
|
+
break;
|
|
10888
|
+
}
|
|
10201
10889
|
default: {
|
|
10202
10890
|
const _exhaustive = task.type;
|
|
10203
|
-
|
|
10891
|
+
result = this.failureResult(
|
|
10204
10892
|
task.id,
|
|
10205
10893
|
startedAt,
|
|
10206
10894
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -10208,69 +10896,174 @@ var TaskRunner = class {
|
|
|
10208
10896
|
}
|
|
10209
10897
|
}
|
|
10210
10898
|
} catch (err) {
|
|
10211
|
-
|
|
10899
|
+
result = this.failureResult(task.id, startedAt, String(err));
|
|
10900
|
+
}
|
|
10901
|
+
result.origin = origin;
|
|
10902
|
+
await this.persistOutput(task, result, captured, origin);
|
|
10903
|
+
return result;
|
|
10904
|
+
}
|
|
10905
|
+
async persistOutput(task, result, captured, origin) {
|
|
10906
|
+
if (!this.outputStore) return;
|
|
10907
|
+
const entry = {
|
|
10908
|
+
taskId: result.taskId,
|
|
10909
|
+
startedAt: result.startedAt,
|
|
10910
|
+
completedAt: result.completedAt,
|
|
10911
|
+
status: result.status,
|
|
10912
|
+
findings: result.findings,
|
|
10913
|
+
fixed: result.fixed,
|
|
10914
|
+
prUrl: result.prUrl,
|
|
10915
|
+
prUpdated: result.prUpdated,
|
|
10916
|
+
origin,
|
|
10917
|
+
...result.error !== void 0 && { error: result.error },
|
|
10918
|
+
...result.costUsd !== void 0 && { costUsd: result.costUsd },
|
|
10919
|
+
...captured?.stdout !== void 0 && { stdout: captured.stdout },
|
|
10920
|
+
...captured?.stderr !== void 0 && { stderr: captured.stderr },
|
|
10921
|
+
...captured?.structured !== void 0 && { structured: captured.structured },
|
|
10922
|
+
...captured?.context !== void 0 && { context: captured.context }
|
|
10923
|
+
};
|
|
10924
|
+
try {
|
|
10925
|
+
await this.outputStore.write(task.id, entry, task.outputRetention);
|
|
10926
|
+
} catch {
|
|
10212
10927
|
}
|
|
10213
10928
|
}
|
|
10214
10929
|
/**
|
|
10215
|
-
*
|
|
10930
|
+
* Run the check step using whichever runner the task asks for. Custom
|
|
10931
|
+
* tasks that declare `checkScript` go through the Hermes Phase 2
|
|
10932
|
+
* `CheckScriptRunner`; built-ins (and customs that use the legacy
|
|
10933
|
+
* `checkCommand` shape) go through the original heuristic runner.
|
|
10216
10934
|
*/
|
|
10217
|
-
async
|
|
10935
|
+
async runCheckStep(task) {
|
|
10936
|
+
if (task.checkScript) {
|
|
10937
|
+
if (!this.checkScriptRunner) {
|
|
10938
|
+
throw new Error(
|
|
10939
|
+
`task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
|
|
10940
|
+
);
|
|
10941
|
+
}
|
|
10942
|
+
const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
|
|
10943
|
+
return {
|
|
10944
|
+
passed: r2.passed,
|
|
10945
|
+
findings: r2.findings,
|
|
10946
|
+
stdout: r2.output,
|
|
10947
|
+
stderr: r2.stderr,
|
|
10948
|
+
structured: r2.structured ? r2.structured : null
|
|
10949
|
+
};
|
|
10950
|
+
}
|
|
10218
10951
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
10219
|
-
|
|
10952
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
10220
10953
|
}
|
|
10954
|
+
const r = await this.checkRunner.run(task.checkCommand, this.cwd);
|
|
10955
|
+
return {
|
|
10956
|
+
passed: r.passed,
|
|
10957
|
+
findings: r.findings,
|
|
10958
|
+
stdout: r.output,
|
|
10959
|
+
stderr: "",
|
|
10960
|
+
structured: null
|
|
10961
|
+
};
|
|
10962
|
+
}
|
|
10963
|
+
/**
|
|
10964
|
+
* Hermes Phase 2 — Compose the agent prompt-context block from inlined
|
|
10965
|
+
* skills + upstream task outputs. Returns an empty string when nothing
|
|
10966
|
+
* is configured (or when the resolver is absent), which is the safe
|
|
10967
|
+
* no-op default.
|
|
10968
|
+
*/
|
|
10969
|
+
async composePromptContext(task) {
|
|
10970
|
+
if (!this.contextResolver) return "";
|
|
10971
|
+
const skills = await this.contextResolver.resolveInlineSkills(
|
|
10972
|
+
task.inlineSkills,
|
|
10973
|
+
task.inlineSkillsBudgetTokens ?? 8e3
|
|
10974
|
+
);
|
|
10975
|
+
const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
|
|
10976
|
+
maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
|
|
10977
|
+
});
|
|
10978
|
+
return [skills, upstream].filter(Boolean).join("\n");
|
|
10979
|
+
}
|
|
10980
|
+
/**
|
|
10981
|
+
* Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
|
|
10982
|
+
* only if fixable findings exist; persist captured stdout/stderr/context
|
|
10983
|
+
* via the output store on the way out.
|
|
10984
|
+
*/
|
|
10985
|
+
async runMechanicalAI(task, startedAt) {
|
|
10221
10986
|
if (!task.fixSkill) {
|
|
10222
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
10987
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
10223
10988
|
}
|
|
10224
10989
|
if (!task.branch) {
|
|
10225
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
10990
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
10226
10991
|
}
|
|
10227
|
-
|
|
10228
|
-
|
|
10992
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
10993
|
+
return wrap(
|
|
10994
|
+
this.failureResult(
|
|
10995
|
+
task.id,
|
|
10996
|
+
startedAt,
|
|
10997
|
+
"mechanical-ai task missing checkCommand or checkScript"
|
|
10998
|
+
)
|
|
10999
|
+
);
|
|
11000
|
+
}
|
|
11001
|
+
let check;
|
|
11002
|
+
try {
|
|
11003
|
+
check = await this.runCheckStep(task);
|
|
11004
|
+
} catch (err) {
|
|
11005
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11006
|
+
}
|
|
11007
|
+
const promptContext = await this.composePromptContext(task);
|
|
11008
|
+
const baseCaptured = {
|
|
11009
|
+
stdout: check.stdout,
|
|
11010
|
+
stderr: check.stderr,
|
|
11011
|
+
structured: check.structured,
|
|
11012
|
+
...promptContext ? { context: promptContext } : {}
|
|
11013
|
+
};
|
|
11014
|
+
const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
|
|
11015
|
+
if (check.findings === 0 || wakeAgentExplicitlyFalse) {
|
|
10229
11016
|
return {
|
|
10230
|
-
|
|
10231
|
-
|
|
10232
|
-
|
|
10233
|
-
|
|
10234
|
-
|
|
10235
|
-
|
|
10236
|
-
|
|
10237
|
-
|
|
11017
|
+
result: {
|
|
11018
|
+
taskId: task.id,
|
|
11019
|
+
startedAt,
|
|
11020
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11021
|
+
status: "no-issues",
|
|
11022
|
+
findings: check.findings,
|
|
11023
|
+
fixed: 0,
|
|
11024
|
+
prUrl: null,
|
|
11025
|
+
prUpdated: false
|
|
11026
|
+
},
|
|
11027
|
+
captured: baseCaptured
|
|
10238
11028
|
};
|
|
10239
11029
|
}
|
|
10240
11030
|
if (this.prManager) {
|
|
10241
11031
|
try {
|
|
10242
11032
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
10243
11033
|
} catch (err) {
|
|
10244
|
-
return
|
|
11034
|
+
return wrap(
|
|
11035
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11036
|
+
baseCaptured
|
|
11037
|
+
);
|
|
10245
11038
|
}
|
|
10246
11039
|
}
|
|
10247
11040
|
const backendName = this.resolveBackend(task.id);
|
|
10248
11041
|
let agentResult;
|
|
10249
11042
|
try {
|
|
10250
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
backendName,
|
|
10254
|
-
this.cwd
|
|
10255
|
-
);
|
|
11043
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11044
|
+
promptContext
|
|
11045
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10256
11046
|
} catch (err) {
|
|
10257
11047
|
return {
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
|
|
10266
|
-
|
|
11048
|
+
result: {
|
|
11049
|
+
taskId: task.id,
|
|
11050
|
+
startedAt,
|
|
11051
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11052
|
+
status: "failure",
|
|
11053
|
+
findings: check.findings,
|
|
11054
|
+
fixed: 0,
|
|
11055
|
+
prUrl: null,
|
|
11056
|
+
prUpdated: false,
|
|
11057
|
+
error: `Agent dispatch failed: ${String(err)}`
|
|
11058
|
+
},
|
|
11059
|
+
captured: baseCaptured
|
|
10267
11060
|
};
|
|
10268
11061
|
}
|
|
10269
11062
|
let prUrl = null;
|
|
10270
11063
|
let prUpdated = false;
|
|
10271
11064
|
if (this.prManager && agentResult.producedCommits) {
|
|
10272
11065
|
try {
|
|
10273
|
-
const summary = `Findings: ${
|
|
11066
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
10274
11067
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
10275
11068
|
prUrl = prResult.prUrl;
|
|
10276
11069
|
prUpdated = prResult.prUpdated;
|
|
@@ -10279,14 +11072,17 @@ var TaskRunner = class {
|
|
|
10279
11072
|
}
|
|
10280
11073
|
}
|
|
10281
11074
|
return {
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10286
|
-
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
11075
|
+
result: {
|
|
11076
|
+
taskId: task.id,
|
|
11077
|
+
startedAt,
|
|
11078
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11079
|
+
status: "success",
|
|
11080
|
+
findings: check.findings,
|
|
11081
|
+
fixed: agentResult.fixed,
|
|
11082
|
+
prUrl,
|
|
11083
|
+
prUpdated
|
|
11084
|
+
},
|
|
11085
|
+
captured: baseCaptured
|
|
10290
11086
|
};
|
|
10291
11087
|
}
|
|
10292
11088
|
/**
|
|
@@ -10306,15 +11102,13 @@ var TaskRunner = class {
|
|
|
10306
11102
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
10307
11103
|
}
|
|
10308
11104
|
}
|
|
11105
|
+
const promptContext = await this.composePromptContext(task);
|
|
10309
11106
|
const backendName = this.resolveBackend(task.id);
|
|
10310
11107
|
let agentResult;
|
|
10311
11108
|
try {
|
|
10312
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
backendName,
|
|
10316
|
-
this.cwd
|
|
10317
|
-
);
|
|
11109
|
+
agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
|
|
11110
|
+
promptContext
|
|
11111
|
+
}) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
|
|
10318
11112
|
} catch (err) {
|
|
10319
11113
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
10320
11114
|
}
|
|
@@ -10342,7 +11136,7 @@ var TaskRunner = class {
|
|
|
10342
11136
|
};
|
|
10343
11137
|
}
|
|
10344
11138
|
/**
|
|
10345
|
-
* Report-only: run check
|
|
11139
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
10346
11140
|
*
|
|
10347
11141
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
10348
11142
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -10352,13 +11146,24 @@ var TaskRunner = class {
|
|
|
10352
11146
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
10353
11147
|
*/
|
|
10354
11148
|
async runReportOnly(task, startedAt) {
|
|
10355
|
-
if (!task.checkCommand
|
|
10356
|
-
return
|
|
11149
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11150
|
+
return wrap(
|
|
11151
|
+
this.failureResult(
|
|
11152
|
+
task.id,
|
|
11153
|
+
startedAt,
|
|
11154
|
+
"report-only task missing checkCommand or checkScript"
|
|
11155
|
+
)
|
|
11156
|
+
);
|
|
11157
|
+
}
|
|
11158
|
+
let check;
|
|
11159
|
+
try {
|
|
11160
|
+
check = await this.runCheckStep(task);
|
|
11161
|
+
} catch (err) {
|
|
11162
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
10357
11163
|
}
|
|
10358
|
-
const
|
|
10359
|
-
const parsed = parseStatusLine(checkResult.output);
|
|
11164
|
+
const parsed = parseStatusLine(check.stdout);
|
|
10360
11165
|
const status = parsed?.status ?? "success";
|
|
10361
|
-
const findings = parsed === null ?
|
|
11166
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
10362
11167
|
const result = {
|
|
10363
11168
|
taskId: task.id,
|
|
10364
11169
|
startedAt,
|
|
@@ -10372,7 +11177,10 @@ var TaskRunner = class {
|
|
|
10372
11177
|
if (parsed?.error) {
|
|
10373
11178
|
result.error = parsed.error;
|
|
10374
11179
|
}
|
|
10375
|
-
return
|
|
11180
|
+
return {
|
|
11181
|
+
result,
|
|
11182
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11183
|
+
};
|
|
10376
11184
|
}
|
|
10377
11185
|
/**
|
|
10378
11186
|
* Housekeeping: run command directly, no AI, no PR.
|
|
@@ -10383,17 +11191,39 @@ var TaskRunner = class {
|
|
|
10383
11191
|
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
10384
11192
|
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
10385
11193
|
* status: 'success', findings: 0.
|
|
11194
|
+
*
|
|
11195
|
+
* Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
|
|
11196
|
+
* tasks; the runner falls through to the same JSON-status parsing path.
|
|
10386
11197
|
*/
|
|
10387
11198
|
async runHousekeeping(task, startedAt) {
|
|
10388
|
-
if (!task.checkCommand
|
|
10389
|
-
return
|
|
11199
|
+
if (!task.checkCommand && !task.checkScript) {
|
|
11200
|
+
return wrap(
|
|
11201
|
+
this.failureResult(
|
|
11202
|
+
task.id,
|
|
11203
|
+
startedAt,
|
|
11204
|
+
"housekeeping task missing checkCommand or checkScript"
|
|
11205
|
+
)
|
|
11206
|
+
);
|
|
10390
11207
|
}
|
|
10391
11208
|
let stdout;
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
11209
|
+
let stderr = "";
|
|
11210
|
+
let structured = null;
|
|
11211
|
+
if (task.checkScript) {
|
|
11212
|
+
try {
|
|
11213
|
+
const r = await this.runCheckStep(task);
|
|
11214
|
+
stdout = r.stdout;
|
|
11215
|
+
stderr = r.stderr;
|
|
11216
|
+
structured = r.structured;
|
|
11217
|
+
} catch (err) {
|
|
11218
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11219
|
+
}
|
|
11220
|
+
} else {
|
|
11221
|
+
try {
|
|
11222
|
+
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
11223
|
+
stdout = out.stdout ?? "";
|
|
11224
|
+
} catch (err) {
|
|
11225
|
+
return wrap(this.failureResult(task.id, startedAt, String(err)));
|
|
11226
|
+
}
|
|
10397
11227
|
}
|
|
10398
11228
|
const parsed = parseStatusLine(stdout);
|
|
10399
11229
|
const status = parsed?.status ?? "success";
|
|
@@ -10408,7 +11238,7 @@ var TaskRunner = class {
|
|
|
10408
11238
|
prUpdated: false
|
|
10409
11239
|
};
|
|
10410
11240
|
if (parsed?.error) result.error = parsed.error;
|
|
10411
|
-
return result;
|
|
11241
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
10412
11242
|
}
|
|
10413
11243
|
/**
|
|
10414
11244
|
* Resolve which AI backend name to use for a given task.
|
|
@@ -10433,6 +11263,9 @@ var TaskRunner = class {
|
|
|
10433
11263
|
};
|
|
10434
11264
|
}
|
|
10435
11265
|
};
|
|
11266
|
+
function wrap(result, captured) {
|
|
11267
|
+
return captured ? { result, captured } : { result };
|
|
11268
|
+
}
|
|
10436
11269
|
function parseStatusLine(output) {
|
|
10437
11270
|
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
10438
11271
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
@@ -10470,6 +11303,560 @@ function parseStatusLine(output) {
|
|
|
10470
11303
|
return null;
|
|
10471
11304
|
}
|
|
10472
11305
|
|
|
11306
|
+
// src/maintenance/check-script-runner.ts
|
|
11307
|
+
var import_node_child_process11 = require("child_process");
|
|
11308
|
+
var import_node_util3 = require("util");
|
|
11309
|
+
var path17 = __toESM(require("path"));
|
|
11310
|
+
var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process11.execFile);
|
|
11311
|
+
var CheckScriptRunner = class {
|
|
11312
|
+
constructor(cwd) {
|
|
11313
|
+
this.cwd = cwd;
|
|
11314
|
+
}
|
|
11315
|
+
cwd;
|
|
11316
|
+
async run(spec, cwd) {
|
|
11317
|
+
const projectRoot = cwd ?? this.cwd;
|
|
11318
|
+
const captured = await captureScript(spec, projectRoot);
|
|
11319
|
+
const parseJson = spec.parseStdoutJson !== false;
|
|
11320
|
+
const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
|
|
11321
|
+
if (structured) {
|
|
11322
|
+
return mapStructured(structured, captured.stdout, captured.stderr);
|
|
11323
|
+
}
|
|
11324
|
+
return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
|
|
11325
|
+
}
|
|
11326
|
+
};
|
|
11327
|
+
async function captureScript(spec, projectRoot) {
|
|
11328
|
+
const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
|
|
11329
|
+
const args = spec.args ?? [];
|
|
11330
|
+
const timeoutMs = spec.timeoutMs ?? 12e4;
|
|
11331
|
+
try {
|
|
11332
|
+
const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
|
|
11333
|
+
return {
|
|
11334
|
+
stdout: String(result.stdout ?? ""),
|
|
11335
|
+
stderr: String(result.stderr ?? ""),
|
|
11336
|
+
exitedAbnormally: false
|
|
11337
|
+
};
|
|
11338
|
+
} catch (err) {
|
|
11339
|
+
const e = err;
|
|
11340
|
+
return {
|
|
11341
|
+
stdout: String(e.stdout ?? ""),
|
|
11342
|
+
stderr: String(e.stderr ?? ""),
|
|
11343
|
+
exitedAbnormally: true
|
|
11344
|
+
};
|
|
11345
|
+
}
|
|
11346
|
+
}
|
|
11347
|
+
function parseStatusEnvelope(stdout) {
|
|
11348
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11349
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11350
|
+
const env = classifyLine2(lines[i]);
|
|
11351
|
+
if (env) return env;
|
|
11352
|
+
}
|
|
11353
|
+
return null;
|
|
11354
|
+
}
|
|
11355
|
+
var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
|
|
11356
|
+
function classifyLine2(line) {
|
|
11357
|
+
const obj = tryParseJsonObject(line);
|
|
11358
|
+
if (!obj) return null;
|
|
11359
|
+
const s = obj.status;
|
|
11360
|
+
if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
|
|
11361
|
+
return buildEnvelope(s, obj);
|
|
11362
|
+
}
|
|
11363
|
+
function tryParseJsonObject(line) {
|
|
11364
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
|
|
11365
|
+
try {
|
|
11366
|
+
return JSON.parse(line);
|
|
11367
|
+
} catch {
|
|
11368
|
+
return null;
|
|
11369
|
+
}
|
|
11370
|
+
}
|
|
11371
|
+
function buildEnvelope(status, obj) {
|
|
11372
|
+
const env = { status };
|
|
11373
|
+
if (typeof obj.findings === "number") env.findings = obj.findings;
|
|
11374
|
+
if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
|
|
11375
|
+
if (typeof obj.message === "string") env.message = obj.message;
|
|
11376
|
+
if (obj.outputs && typeof obj.outputs === "object") {
|
|
11377
|
+
env.outputs = obj.outputs;
|
|
11378
|
+
}
|
|
11379
|
+
return env;
|
|
11380
|
+
}
|
|
11381
|
+
function mapStructured(env, stdout, stderr) {
|
|
11382
|
+
const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
|
|
11383
|
+
switch (env.status) {
|
|
11384
|
+
case "ok":
|
|
11385
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11386
|
+
case "findings": {
|
|
11387
|
+
const wake = env.wakeAgent ?? findings > 0;
|
|
11388
|
+
return { passed: !wake, findings, output: stdout, stderr, structured: env };
|
|
11389
|
+
}
|
|
11390
|
+
case "skip":
|
|
11391
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11392
|
+
case "error":
|
|
11393
|
+
return {
|
|
11394
|
+
passed: false,
|
|
11395
|
+
findings: Math.max(findings, 1),
|
|
11396
|
+
output: stdout,
|
|
11397
|
+
stderr,
|
|
11398
|
+
structured: env
|
|
11399
|
+
};
|
|
11400
|
+
default:
|
|
11401
|
+
return { passed: true, findings: 0, output: stdout, stderr, structured: env };
|
|
11402
|
+
}
|
|
11403
|
+
}
|
|
11404
|
+
function heuristicResult(stdout, stderr, exitedAbnormally) {
|
|
11405
|
+
const combined = [stdout, stderr].filter(Boolean).join("\n");
|
|
11406
|
+
const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
11407
|
+
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
|
|
11408
|
+
return {
|
|
11409
|
+
passed: findings === 0 && !exitedAbnormally,
|
|
11410
|
+
findings,
|
|
11411
|
+
output: stdout,
|
|
11412
|
+
stderr,
|
|
11413
|
+
structured: null
|
|
11414
|
+
};
|
|
11415
|
+
}
|
|
11416
|
+
|
|
11417
|
+
// src/maintenance/output-store.ts
|
|
11418
|
+
var fs16 = __toESM(require("fs"));
|
|
11419
|
+
var path18 = __toESM(require("path"));
|
|
11420
|
+
var DEFAULT_RETENTION = {
|
|
11421
|
+
runs: 50,
|
|
11422
|
+
maxAgeDays: 30
|
|
11423
|
+
};
|
|
11424
|
+
var fallbackLogger2 = {
|
|
11425
|
+
info: () => {
|
|
11426
|
+
},
|
|
11427
|
+
warn: (m, c) => console.warn(m, c),
|
|
11428
|
+
error: (m, c) => console.error(m, c)
|
|
11429
|
+
};
|
|
11430
|
+
var TaskOutputStore = class {
|
|
11431
|
+
rootDir;
|
|
11432
|
+
retentionDefaults;
|
|
11433
|
+
logger;
|
|
11434
|
+
constructor(options) {
|
|
11435
|
+
this.rootDir = options.rootDir;
|
|
11436
|
+
this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
|
|
11437
|
+
this.logger = options.logger ?? fallbackLogger2;
|
|
11438
|
+
}
|
|
11439
|
+
/**
|
|
11440
|
+
* Reject task IDs that don't match the validator's kebab-case pattern —
|
|
11441
|
+
* defends `dirFor()` against caller-supplied path-traversal segments
|
|
11442
|
+
* (`'../foo'`) when the store is invoked from CLI surfaces that don't
|
|
11443
|
+
* round-trip through `validateCustomTasks`.
|
|
11444
|
+
*/
|
|
11445
|
+
ensureSafeTaskId(taskId) {
|
|
11446
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
|
|
11447
|
+
throw new Error(
|
|
11448
|
+
`TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
|
|
11449
|
+
);
|
|
11450
|
+
}
|
|
11451
|
+
}
|
|
11452
|
+
/**
|
|
11453
|
+
* Persist a single run entry. Retention is applied after the write so
|
|
11454
|
+
* the latest record is durable even if pruning fails.
|
|
11455
|
+
*/
|
|
11456
|
+
async write(taskId, entry, retention) {
|
|
11457
|
+
this.ensureSafeTaskId(taskId);
|
|
11458
|
+
const dir = this.dirFor(taskId);
|
|
11459
|
+
await fs16.promises.mkdir(dir, { recursive: true });
|
|
11460
|
+
const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
|
|
11461
|
+
const filePath = path18.join(dir, fileName);
|
|
11462
|
+
const tmpPath = `${filePath}.tmp`;
|
|
11463
|
+
const payload = JSON.stringify(entry, null, 2);
|
|
11464
|
+
await fs16.promises.writeFile(tmpPath, payload, "utf-8");
|
|
11465
|
+
await fs16.promises.rename(tmpPath, filePath);
|
|
11466
|
+
try {
|
|
11467
|
+
await this.applyRetention(taskId, retention);
|
|
11468
|
+
} catch (err) {
|
|
11469
|
+
this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
|
|
11470
|
+
}
|
|
11471
|
+
}
|
|
11472
|
+
/**
|
|
11473
|
+
* Return the most recent persisted entry for the task, or null if none.
|
|
11474
|
+
*/
|
|
11475
|
+
async latest(taskId) {
|
|
11476
|
+
const entries = await this.list(taskId, 1, 0);
|
|
11477
|
+
return entries[0] ?? null;
|
|
11478
|
+
}
|
|
11479
|
+
/**
|
|
11480
|
+
* List entries newest-first with offset+limit pagination.
|
|
11481
|
+
*/
|
|
11482
|
+
async list(taskId, limit, offset) {
|
|
11483
|
+
this.ensureSafeTaskId(taskId);
|
|
11484
|
+
const dir = this.dirFor(taskId);
|
|
11485
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
11486
|
+
const slice = fileNames.slice(offset, offset + limit);
|
|
11487
|
+
const out = [];
|
|
11488
|
+
for (const name of slice) {
|
|
11489
|
+
const entry = await this.readEntry(path18.join(dir, name));
|
|
11490
|
+
if (entry) out.push(entry);
|
|
11491
|
+
}
|
|
11492
|
+
return out;
|
|
11493
|
+
}
|
|
11494
|
+
/**
|
|
11495
|
+
* Lookup a specific run by its file name (without the `.json` suffix) or
|
|
11496
|
+
* by its raw completion timestamp.
|
|
11497
|
+
*/
|
|
11498
|
+
async get(taskId, runId) {
|
|
11499
|
+
this.ensureSafeTaskId(taskId);
|
|
11500
|
+
if (/[\\/]|\.\./.test(runId)) {
|
|
11501
|
+
throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
|
|
11502
|
+
}
|
|
11503
|
+
const dir = this.dirFor(taskId);
|
|
11504
|
+
const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
|
|
11505
|
+
return this.readEntry(path18.join(dir, fileName));
|
|
11506
|
+
}
|
|
11507
|
+
/**
|
|
11508
|
+
* The on-disk root for a given task. Exposed for tooling that needs to walk
|
|
11509
|
+
* outputs from outside the store API.
|
|
11510
|
+
*/
|
|
11511
|
+
dirFor(taskId) {
|
|
11512
|
+
return path18.join(this.rootDir, taskId, "outputs");
|
|
11513
|
+
}
|
|
11514
|
+
async readEntry(filePath) {
|
|
11515
|
+
try {
|
|
11516
|
+
const buf = await fs16.promises.readFile(filePath, "utf-8");
|
|
11517
|
+
const parsed = JSON.parse(buf);
|
|
11518
|
+
return parsed;
|
|
11519
|
+
} catch {
|
|
11520
|
+
return null;
|
|
11521
|
+
}
|
|
11522
|
+
}
|
|
11523
|
+
async applyRetention(taskId, retention) {
|
|
11524
|
+
const runs = retention?.runs ?? this.retentionDefaults.runs;
|
|
11525
|
+
const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
|
|
11526
|
+
const dir = this.dirFor(taskId);
|
|
11527
|
+
const fileNames = await listJsonFilesDescending(dir);
|
|
11528
|
+
const overflow = fileNames.slice(runs);
|
|
11529
|
+
const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
|
|
11530
|
+
const aged = [];
|
|
11531
|
+
for (const name of fileNames) {
|
|
11532
|
+
const ts = parseIsoFromFileName(name);
|
|
11533
|
+
if (ts !== null && ts < cutoffMs) aged.push(name);
|
|
11534
|
+
}
|
|
11535
|
+
const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
|
|
11536
|
+
for (const name of toRemove) {
|
|
11537
|
+
try {
|
|
11538
|
+
await fs16.promises.unlink(path18.join(dir, name));
|
|
11539
|
+
} catch {
|
|
11540
|
+
}
|
|
11541
|
+
}
|
|
11542
|
+
}
|
|
11543
|
+
};
|
|
11544
|
+
async function listJsonFilesDescending(dir) {
|
|
11545
|
+
let names;
|
|
11546
|
+
try {
|
|
11547
|
+
names = await fs16.promises.readdir(dir);
|
|
11548
|
+
} catch {
|
|
11549
|
+
return [];
|
|
11550
|
+
}
|
|
11551
|
+
return names.filter((n) => n.endsWith(".json")).sort().reverse();
|
|
11552
|
+
}
|
|
11553
|
+
function sanitizeIso(iso) {
|
|
11554
|
+
return iso.replace(/:/g, "-");
|
|
11555
|
+
}
|
|
11556
|
+
function parseIsoFromFileName(fileName) {
|
|
11557
|
+
const stem = fileName.replace(/\.json$/, "");
|
|
11558
|
+
const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
|
|
11559
|
+
const ms = Date.parse(restored);
|
|
11560
|
+
return Number.isFinite(ms) ? ms : null;
|
|
11561
|
+
}
|
|
11562
|
+
|
|
11563
|
+
// src/maintenance/context-resolver.ts
|
|
11564
|
+
var ContextResolver = class {
|
|
11565
|
+
outputStore;
|
|
11566
|
+
skillReader;
|
|
11567
|
+
logger;
|
|
11568
|
+
perUpstreamMaxChars;
|
|
11569
|
+
constructor(options) {
|
|
11570
|
+
this.outputStore = options.outputStore;
|
|
11571
|
+
this.skillReader = options.skillReader ?? null;
|
|
11572
|
+
this.logger = options.logger ?? fallbackLogger3;
|
|
11573
|
+
this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
|
|
11574
|
+
}
|
|
11575
|
+
async resolveContextFrom(upstreamTaskIds, options = {}) {
|
|
11576
|
+
if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
|
|
11577
|
+
const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
|
|
11578
|
+
const now = Date.now();
|
|
11579
|
+
const sections = [];
|
|
11580
|
+
for (const id of upstreamTaskIds) {
|
|
11581
|
+
const entry = await this.outputStore.latest(id);
|
|
11582
|
+
sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
|
|
11583
|
+
}
|
|
11584
|
+
return `## Upstream context
|
|
11585
|
+
|
|
11586
|
+
${sections.join("\n\n")}
|
|
11587
|
+
`;
|
|
11588
|
+
}
|
|
11589
|
+
async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
|
|
11590
|
+
if (!skillNames || skillNames.length === 0) return "";
|
|
11591
|
+
if (!this.skillReader) return "";
|
|
11592
|
+
const charBudget = budgetTokens * 4;
|
|
11593
|
+
let used = 0;
|
|
11594
|
+
const sections = [];
|
|
11595
|
+
let truncatedAt = -1;
|
|
11596
|
+
for (let i = 0; i < skillNames.length; i++) {
|
|
11597
|
+
const name = skillNames[i];
|
|
11598
|
+
const body = await this.skillReader.read(name);
|
|
11599
|
+
if (body === null) {
|
|
11600
|
+
this.logger.warn("inlineSkills: skill not found in registry", { name });
|
|
11601
|
+
continue;
|
|
11602
|
+
}
|
|
11603
|
+
const block = `### ${name}
|
|
11604
|
+
|
|
11605
|
+
${body}`;
|
|
11606
|
+
if (used + block.length > charBudget) {
|
|
11607
|
+
truncatedAt = i;
|
|
11608
|
+
break;
|
|
11609
|
+
}
|
|
11610
|
+
used += block.length;
|
|
11611
|
+
sections.push(block);
|
|
11612
|
+
}
|
|
11613
|
+
if (truncatedAt >= 0) {
|
|
11614
|
+
this.logger.warn(
|
|
11615
|
+
`inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
|
|
11616
|
+
);
|
|
11617
|
+
}
|
|
11618
|
+
if (sections.length === 0) return "";
|
|
11619
|
+
return `## Reference skills
|
|
11620
|
+
|
|
11621
|
+
${sections.join("\n\n")}
|
|
11622
|
+
`;
|
|
11623
|
+
}
|
|
11624
|
+
formatUpstream(id, entry, now, maxAgeMs) {
|
|
11625
|
+
if (!entry) {
|
|
11626
|
+
return `### ${id}
|
|
11627
|
+
|
|
11628
|
+
_[no prior run]_`;
|
|
11629
|
+
}
|
|
11630
|
+
const completedMs = Date.parse(entry.completedAt);
|
|
11631
|
+
if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
|
|
11632
|
+
return `### ${id} (last run ${entry.completedAt}, stale)
|
|
11633
|
+
|
|
11634
|
+
_[stale: omitted]_`;
|
|
11635
|
+
}
|
|
11636
|
+
const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
|
|
11637
|
+
const body = (entry.stdout ?? "").trim();
|
|
11638
|
+
const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
|
|
11639
|
+
|
|
11640
|
+
_[truncated]_` : body;
|
|
11641
|
+
return `${head}
|
|
11642
|
+
|
|
11643
|
+
${truncated || "_[no stdout captured]_"}`;
|
|
11644
|
+
}
|
|
11645
|
+
};
|
|
11646
|
+
var fallbackLogger3 = {
|
|
11647
|
+
info: () => {
|
|
11648
|
+
},
|
|
11649
|
+
warn: () => {
|
|
11650
|
+
},
|
|
11651
|
+
error: () => {
|
|
11652
|
+
}
|
|
11653
|
+
};
|
|
11654
|
+
|
|
11655
|
+
// src/maintenance/custom-task-validator.ts
|
|
11656
|
+
var import_types30 = require("@harness-engineering/types");
|
|
11657
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
|
|
11658
|
+
var REQUIRED_FIELDS_BY_TYPE = {
|
|
11659
|
+
"mechanical-ai": ["branch", "fixSkill"],
|
|
11660
|
+
"pure-ai": ["branch", "fixSkill"],
|
|
11661
|
+
"report-only": [],
|
|
11662
|
+
housekeeping: []
|
|
11663
|
+
};
|
|
11664
|
+
function validateCustomTasks(customTasks, builtIns, deps = {}) {
|
|
11665
|
+
const errors = [];
|
|
11666
|
+
if (!customTasks) return (0, import_types30.Ok)(void 0);
|
|
11667
|
+
const builtInIds = new Set(builtIns.map((t) => t.id));
|
|
11668
|
+
const customIds = Object.keys(customTasks);
|
|
11669
|
+
const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
|
|
11670
|
+
for (const id of customIds) {
|
|
11671
|
+
const task = customTasks[id];
|
|
11672
|
+
if (!task) continue;
|
|
11673
|
+
validateOne(id, task, builtInIds, allIds, deps, errors);
|
|
11674
|
+
}
|
|
11675
|
+
detectCycles(customTasks, builtIns, errors);
|
|
11676
|
+
return errors.length === 0 ? (0, import_types30.Ok)(void 0) : (0, import_types30.Err)(errors);
|
|
11677
|
+
}
|
|
11678
|
+
function validateOne(id, task, builtInIds, allIds, deps, errors) {
|
|
11679
|
+
const prefix = `customTasks.${id}`;
|
|
11680
|
+
if (!ID_PATTERN.test(id)) {
|
|
11681
|
+
errors.push({
|
|
11682
|
+
path: prefix,
|
|
11683
|
+
message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
|
|
11684
|
+
});
|
|
11685
|
+
}
|
|
11686
|
+
if (builtInIds.has(id)) {
|
|
11687
|
+
errors.push({
|
|
11688
|
+
path: prefix,
|
|
11689
|
+
message: `task ID '${id}' collides with a built-in task; choose a different name`
|
|
11690
|
+
});
|
|
11691
|
+
}
|
|
11692
|
+
if (!task.description || task.description.trim().length === 0) {
|
|
11693
|
+
errors.push({ path: `${prefix}.description`, message: "description is required" });
|
|
11694
|
+
}
|
|
11695
|
+
if (!task.schedule || task.schedule.trim().length === 0) {
|
|
11696
|
+
errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
|
|
11697
|
+
}
|
|
11698
|
+
validateCheckShape(prefix, task, errors);
|
|
11699
|
+
validateRequiredByType(prefix, task, errors);
|
|
11700
|
+
validateContextFrom(prefix, id, task, allIds, errors);
|
|
11701
|
+
validateInlineSkills(prefix, task, deps, errors);
|
|
11702
|
+
validateScriptPath(prefix, task, deps, errors);
|
|
11703
|
+
}
|
|
11704
|
+
function validateCheckShape(prefix, task, errors) {
|
|
11705
|
+
const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
|
|
11706
|
+
const hasScript = task.checkScript !== void 0;
|
|
11707
|
+
if (hasCommand && hasScript) {
|
|
11708
|
+
errors.push({
|
|
11709
|
+
path: prefix,
|
|
11710
|
+
message: "a task may declare checkCommand OR checkScript, not both"
|
|
11711
|
+
});
|
|
11712
|
+
}
|
|
11713
|
+
const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
|
|
11714
|
+
if (needsCheck && !hasCommand && !hasScript) {
|
|
11715
|
+
errors.push({
|
|
11716
|
+
path: prefix,
|
|
11717
|
+
message: `${task.type} task must declare either checkCommand or checkScript`
|
|
11718
|
+
});
|
|
11719
|
+
}
|
|
11720
|
+
if (hasScript) {
|
|
11721
|
+
const path22 = task.checkScript?.path;
|
|
11722
|
+
if (!path22 || path22.trim().length === 0) {
|
|
11723
|
+
errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
|
|
11724
|
+
}
|
|
11725
|
+
if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
|
|
11726
|
+
errors.push({
|
|
11727
|
+
path: `${prefix}.checkScript.timeoutMs`,
|
|
11728
|
+
message: "timeoutMs must be a positive integer"
|
|
11729
|
+
});
|
|
11730
|
+
}
|
|
11731
|
+
}
|
|
11732
|
+
}
|
|
11733
|
+
function validateRequiredByType(prefix, task, errors) {
|
|
11734
|
+
const required = REQUIRED_FIELDS_BY_TYPE[task.type];
|
|
11735
|
+
if (!required) {
|
|
11736
|
+
errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
|
|
11737
|
+
return;
|
|
11738
|
+
}
|
|
11739
|
+
for (const field of required) {
|
|
11740
|
+
const value = task[field];
|
|
11741
|
+
if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
|
|
11742
|
+
errors.push({
|
|
11743
|
+
path: `${prefix}.${String(field)}`,
|
|
11744
|
+
message: `${task.type} task requires ${String(field)}`
|
|
11745
|
+
});
|
|
11746
|
+
}
|
|
11747
|
+
}
|
|
11748
|
+
if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
|
|
11749
|
+
errors.push({
|
|
11750
|
+
path: `${prefix}.branch`,
|
|
11751
|
+
message: `${task.type} task requires a non-null branch`
|
|
11752
|
+
});
|
|
11753
|
+
}
|
|
11754
|
+
}
|
|
11755
|
+
function validateContextFrom(prefix, selfId, task, allIds, errors) {
|
|
11756
|
+
if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
|
|
11757
|
+
errors.push({
|
|
11758
|
+
path: `${prefix}.contextFromMaxAgeMinutes`,
|
|
11759
|
+
message: "contextFromMaxAgeMinutes must be a positive integer"
|
|
11760
|
+
});
|
|
11761
|
+
}
|
|
11762
|
+
if (!task.contextFrom) return;
|
|
11763
|
+
for (let i = 0; i < task.contextFrom.length; i++) {
|
|
11764
|
+
const upstreamId = task.contextFrom[i];
|
|
11765
|
+
if (!upstreamId) continue;
|
|
11766
|
+
if (upstreamId === selfId) {
|
|
11767
|
+
errors.push({
|
|
11768
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
11769
|
+
message: `task '${selfId}' cannot reference itself in contextFrom`
|
|
11770
|
+
});
|
|
11771
|
+
}
|
|
11772
|
+
if (!allIds.has(upstreamId)) {
|
|
11773
|
+
errors.push({
|
|
11774
|
+
path: `${prefix}.contextFrom[${i}]`,
|
|
11775
|
+
message: `references unknown task '${upstreamId}'`
|
|
11776
|
+
});
|
|
11777
|
+
}
|
|
11778
|
+
}
|
|
11779
|
+
}
|
|
11780
|
+
function validateInlineSkills(prefix, task, deps, errors) {
|
|
11781
|
+
if (!task.inlineSkills) return;
|
|
11782
|
+
if (!deps.skillExists) return;
|
|
11783
|
+
for (let i = 0; i < task.inlineSkills.length; i++) {
|
|
11784
|
+
const name = task.inlineSkills[i];
|
|
11785
|
+
if (!name) continue;
|
|
11786
|
+
if (!deps.skillExists(name)) {
|
|
11787
|
+
errors.push({
|
|
11788
|
+
path: `${prefix}.inlineSkills[${i}]`,
|
|
11789
|
+
message: `skill '${name}' not found in the registry`
|
|
11790
|
+
});
|
|
11791
|
+
}
|
|
11792
|
+
}
|
|
11793
|
+
if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
|
|
11794
|
+
errors.push({
|
|
11795
|
+
path: `${prefix}.inlineSkillsBudgetTokens`,
|
|
11796
|
+
message: "inlineSkillsBudgetTokens must be a positive integer"
|
|
11797
|
+
});
|
|
11798
|
+
}
|
|
11799
|
+
}
|
|
11800
|
+
function validateScriptPath(prefix, task, deps, errors) {
|
|
11801
|
+
if (!task.checkScript?.path) return;
|
|
11802
|
+
if (!deps.scriptExists) return;
|
|
11803
|
+
if (!deps.scriptExists(task.checkScript.path)) {
|
|
11804
|
+
errors.push({
|
|
11805
|
+
path: `${prefix}.checkScript.path`,
|
|
11806
|
+
message: `executable not found: ${task.checkScript.path}`
|
|
11807
|
+
});
|
|
11808
|
+
}
|
|
11809
|
+
}
|
|
11810
|
+
function detectCycles(customTasks, builtIns, errors) {
|
|
11811
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
11812
|
+
for (const t of builtIns) adjacency.set(t.id, []);
|
|
11813
|
+
for (const [id, task] of Object.entries(customTasks)) {
|
|
11814
|
+
adjacency.set(id, (task.contextFrom ?? []).slice());
|
|
11815
|
+
}
|
|
11816
|
+
const color = /* @__PURE__ */ new Map();
|
|
11817
|
+
for (const id of adjacency.keys()) color.set(id, "white");
|
|
11818
|
+
const reported = /* @__PURE__ */ new Set();
|
|
11819
|
+
for (const id of Object.keys(customTasks)) {
|
|
11820
|
+
if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
|
|
11821
|
+
}
|
|
11822
|
+
}
|
|
11823
|
+
function visitFromRoot(start, adjacency, color, errors, reported) {
|
|
11824
|
+
const stack = [{ id: start, nextIdx: 0, path: [start] }];
|
|
11825
|
+
color.set(start, "grey");
|
|
11826
|
+
while (stack.length) {
|
|
11827
|
+
const top = stack[stack.length - 1];
|
|
11828
|
+
const neighbors = adjacency.get(top.id) ?? [];
|
|
11829
|
+
if (top.nextIdx >= neighbors.length) {
|
|
11830
|
+
color.set(top.id, "black");
|
|
11831
|
+
stack.pop();
|
|
11832
|
+
continue;
|
|
11833
|
+
}
|
|
11834
|
+
const next = neighbors[top.nextIdx++];
|
|
11835
|
+
if (!next || !adjacency.has(next)) continue;
|
|
11836
|
+
handleEdge(top, next, color, stack, errors, reported);
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
function handleEdge(top, next, color, stack, errors, reported) {
|
|
11840
|
+
const nextColor = color.get(next);
|
|
11841
|
+
if (nextColor === "grey") {
|
|
11842
|
+
reportCycle(top.path, next, errors, reported);
|
|
11843
|
+
} else if (nextColor === "white") {
|
|
11844
|
+
color.set(next, "grey");
|
|
11845
|
+
stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
function reportCycle(path22, next, errors, reported) {
|
|
11849
|
+
const cycleStart = path22.indexOf(next);
|
|
11850
|
+
const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
|
|
11851
|
+
const key = cyclePath.join("\u2192");
|
|
11852
|
+
if (reported.has(key)) return;
|
|
11853
|
+
reported.add(key);
|
|
11854
|
+
errors.push({
|
|
11855
|
+
path: `customTasks.${cyclePath[0]}.contextFrom`,
|
|
11856
|
+
message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
|
|
11857
|
+
});
|
|
11858
|
+
}
|
|
11859
|
+
|
|
10473
11860
|
// src/orchestrator.ts
|
|
10474
11861
|
function useCaseForBackendParam(issue, backendParam) {
|
|
10475
11862
|
if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
|
|
@@ -10570,7 +11957,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10570
11957
|
completionHandler;
|
|
10571
11958
|
/** Project root directory, derived from workspace root. */
|
|
10572
11959
|
get projectRoot() {
|
|
10573
|
-
return
|
|
11960
|
+
return path19.resolve(this.config.workspace.root, "..", "..");
|
|
10574
11961
|
}
|
|
10575
11962
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
10576
11963
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -10625,10 +12012,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10625
12012
|
this.renderer = new PromptRenderer();
|
|
10626
12013
|
this.overrideBackend = overrides?.backend ?? null;
|
|
10627
12014
|
this.interactionQueue = new InteractionQueue(
|
|
10628
|
-
|
|
12015
|
+
path19.join(config.workspace.root, "..", "interactions"),
|
|
10629
12016
|
this
|
|
10630
12017
|
);
|
|
10631
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12018
|
+
this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
|
|
10632
12019
|
const backendsMap = this.config.agent.backends ?? {};
|
|
10633
12020
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
10634
12021
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -10642,7 +12029,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10642
12029
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
10643
12030
|
}
|
|
10644
12031
|
}
|
|
10645
|
-
this.cacheMetrics = new
|
|
12032
|
+
this.cacheMetrics = new import_core16.CacheMetricsRecorder();
|
|
10646
12033
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
10647
12034
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
10648
12035
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -10672,7 +12059,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10672
12059
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
10673
12060
|
});
|
|
10674
12061
|
this.recorder = new StreamRecorder(
|
|
10675
|
-
|
|
12062
|
+
path19.resolve(config.workspace.root, "..", "streams"),
|
|
10676
12063
|
this.logger
|
|
10677
12064
|
);
|
|
10678
12065
|
const self = this;
|
|
@@ -10703,10 +12090,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10703
12090
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
10704
12091
|
if (config.server?.port) {
|
|
10705
12092
|
const webhookStore = new WebhookStore(
|
|
10706
|
-
|
|
12093
|
+
path19.join(this.projectRoot, ".harness", "webhooks.json")
|
|
10707
12094
|
);
|
|
10708
12095
|
this.webhookQueue = new WebhookQueue(
|
|
10709
|
-
|
|
12096
|
+
path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
10710
12097
|
);
|
|
10711
12098
|
const webhookDelivery = new WebhookDelivery({
|
|
10712
12099
|
queue: this.webhookQueue,
|
|
@@ -10722,7 +12109,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10722
12109
|
this.setupNotifications(config.notifications);
|
|
10723
12110
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
10724
12111
|
if (otlpCfg) {
|
|
10725
|
-
this.otlpExporter = new
|
|
12112
|
+
this.otlpExporter = new import_core16.OTLPExporter({
|
|
10726
12113
|
endpoint: otlpCfg.endpoint,
|
|
10727
12114
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
10728
12115
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -10744,7 +12131,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10744
12131
|
queue: this.webhookQueue
|
|
10745
12132
|
},
|
|
10746
12133
|
cacheMetrics: this.cacheMetrics,
|
|
10747
|
-
plansDir:
|
|
12134
|
+
plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
10748
12135
|
pipeline: this.pipeline,
|
|
10749
12136
|
analysisArchive: this.analysisArchive,
|
|
10750
12137
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -10782,7 +12169,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10782
12169
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
10783
12170
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
10784
12171
|
};
|
|
10785
|
-
const clientResult = (0,
|
|
12172
|
+
const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
|
|
10786
12173
|
if (!clientResult.ok) throw clientResult.error;
|
|
10787
12174
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
10788
12175
|
}
|
|
@@ -10800,13 +12187,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10800
12187
|
const logger = this.logger;
|
|
10801
12188
|
const checkRunner = {
|
|
10802
12189
|
run: async (command, cwd) => {
|
|
10803
|
-
const { execFile:
|
|
10804
|
-
const { promisify:
|
|
10805
|
-
const
|
|
12190
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12191
|
+
const { promisify: promisify5 } = await import("util");
|
|
12192
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10806
12193
|
const [cmd, ...args] = command;
|
|
10807
12194
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
10808
12195
|
try {
|
|
10809
|
-
const { stdout } = await
|
|
12196
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10810
12197
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
10811
12198
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
10812
12199
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -10835,13 +12222,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10835
12222
|
};
|
|
10836
12223
|
const commandExecutor = {
|
|
10837
12224
|
exec: async (command, cwd) => {
|
|
10838
|
-
const { execFile:
|
|
10839
|
-
const { promisify:
|
|
10840
|
-
const
|
|
12225
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12226
|
+
const { promisify: promisify5 } = await import("util");
|
|
12227
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
10841
12228
|
const [cmd, ...args] = command;
|
|
10842
12229
|
if (!cmd) return { stdout: "" };
|
|
10843
12230
|
try {
|
|
10844
|
-
const { stdout } = await
|
|
12231
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
10845
12232
|
return { stdout: String(stdout) };
|
|
10846
12233
|
} catch (err) {
|
|
10847
12234
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -10853,12 +12240,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10853
12240
|
}
|
|
10854
12241
|
}
|
|
10855
12242
|
};
|
|
12243
|
+
const outputStore = new TaskOutputStore({
|
|
12244
|
+
rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
12245
|
+
logger: this.logger
|
|
12246
|
+
});
|
|
12247
|
+
const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
|
|
12248
|
+
const skillReader = {
|
|
12249
|
+
// The orchestrator does not own the skill registry; CLI-side skill
|
|
12250
|
+
// resolution wires this in via direct injection. Default: skill not
|
|
12251
|
+
// resolvable from the orchestrator boundary.
|
|
12252
|
+
read: async () => null
|
|
12253
|
+
};
|
|
12254
|
+
const contextResolver = new ContextResolver({
|
|
12255
|
+
outputStore,
|
|
12256
|
+
skillReader,
|
|
12257
|
+
logger: this.logger
|
|
12258
|
+
});
|
|
10856
12259
|
return new TaskRunner({
|
|
10857
12260
|
config: maintenanceConfig,
|
|
10858
12261
|
checkRunner,
|
|
10859
12262
|
agentDispatcher,
|
|
10860
12263
|
commandExecutor,
|
|
10861
|
-
cwd: this.projectRoot
|
|
12264
|
+
cwd: this.projectRoot,
|
|
12265
|
+
checkScriptRunner,
|
|
12266
|
+
contextResolver,
|
|
12267
|
+
outputStore
|
|
10862
12268
|
});
|
|
10863
12269
|
}
|
|
10864
12270
|
/**
|
|
@@ -10866,8 +12272,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10866
12272
|
* Extracted from start() to keep function length under threshold.
|
|
10867
12273
|
*/
|
|
10868
12274
|
async initMaintenance(maintenanceConfig) {
|
|
12275
|
+
const validation = validateCustomTasks(
|
|
12276
|
+
maintenanceConfig.customTasks,
|
|
12277
|
+
BUILT_IN_TASKS
|
|
12278
|
+
);
|
|
12279
|
+
if (!validation.ok) {
|
|
12280
|
+
const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
|
|
12281
|
+
throw new Error(`Invalid maintenance.customTasks configuration:
|
|
12282
|
+
${messages}`);
|
|
12283
|
+
}
|
|
10869
12284
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
10870
|
-
persistDir:
|
|
12285
|
+
persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
10871
12286
|
logger: this.logger
|
|
10872
12287
|
});
|
|
10873
12288
|
await this.maintenanceReporter.load();
|
|
@@ -11306,12 +12721,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11306
12721
|
async postLifecycleComment(identifier, externalId, event) {
|
|
11307
12722
|
try {
|
|
11308
12723
|
if (!externalId) return;
|
|
11309
|
-
const trackerConfig = (0,
|
|
12724
|
+
const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
|
|
11310
12725
|
if (!trackerConfig) return;
|
|
11311
12726
|
const token = process.env.GITHUB_TOKEN;
|
|
11312
12727
|
if (!token) return;
|
|
11313
12728
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
11314
|
-
const adapter = new
|
|
12729
|
+
const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
11315
12730
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
11316
12731
|
const actionMap = {
|
|
11317
12732
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -11384,7 +12799,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
11384
12799
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
11385
12800
|
}))
|
|
11386
12801
|
);
|
|
11387
|
-
(0,
|
|
12802
|
+
(0, import_core14.writeTaint)(
|
|
11388
12803
|
workspacePath,
|
|
11389
12804
|
issue.id,
|
|
11390
12805
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -12043,11 +13458,11 @@ function launchTUI(orchestrator) {
|
|
|
12043
13458
|
}
|
|
12044
13459
|
|
|
12045
13460
|
// src/maintenance/sync-main.ts
|
|
12046
|
-
var
|
|
12047
|
-
var
|
|
13461
|
+
var import_node_child_process12 = require("child_process");
|
|
13462
|
+
var import_node_util4 = require("util");
|
|
12048
13463
|
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
12049
13464
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
12050
|
-
const exec = (0,
|
|
13465
|
+
const exec = (0, import_node_util4.promisify)(execFileFn);
|
|
12051
13466
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
12052
13467
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
12053
13468
|
}
|
|
@@ -12109,7 +13524,7 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
12109
13524
|
}
|
|
12110
13525
|
}
|
|
12111
13526
|
async function syncMain(repoRoot, opts = {}) {
|
|
12112
|
-
const execFileFn = opts.execFileFn ??
|
|
13527
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
|
|
12113
13528
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
12114
13529
|
try {
|
|
12115
13530
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
@@ -12187,10 +13602,10 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
12187
13602
|
}
|
|
12188
13603
|
|
|
12189
13604
|
// src/sessions/search-index.ts
|
|
12190
|
-
var
|
|
12191
|
-
var
|
|
13605
|
+
var fs17 = __toESM(require("fs"));
|
|
13606
|
+
var path20 = __toESM(require("path"));
|
|
12192
13607
|
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
12193
|
-
var
|
|
13608
|
+
var import_types31 = require("@harness-engineering/types");
|
|
12194
13609
|
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
12195
13610
|
var SCHEMA_SQL2 = `
|
|
12196
13611
|
CREATE TABLE IF NOT EXISTS session_docs (
|
|
@@ -12233,7 +13648,7 @@ function normalizeFts5Query(query) {
|
|
|
12233
13648
|
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
12234
13649
|
}
|
|
12235
13650
|
function searchIndexPath(projectPath) {
|
|
12236
|
-
return
|
|
13651
|
+
return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
12237
13652
|
}
|
|
12238
13653
|
var FILE_KIND_TO_FILENAME = {
|
|
12239
13654
|
summary: "summary.md",
|
|
@@ -12248,7 +13663,7 @@ var SqliteSearchIndex = class {
|
|
|
12248
13663
|
removeSessionStmt;
|
|
12249
13664
|
totalStmt;
|
|
12250
13665
|
constructor(dbPath) {
|
|
12251
|
-
|
|
13666
|
+
fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
|
|
12252
13667
|
this.db = new import_better_sqlite32.default(dbPath);
|
|
12253
13668
|
this.db.pragma("journal_mode = WAL");
|
|
12254
13669
|
this.db.pragma("synchronous = NORMAL");
|
|
@@ -12348,19 +13763,19 @@ function openSearchIndex(projectPath) {
|
|
|
12348
13763
|
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
12349
13764
|
}
|
|
12350
13765
|
function indexSessionDirectory(idx, args) {
|
|
12351
|
-
const kinds = args.fileKinds ?? [...
|
|
13766
|
+
const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
|
|
12352
13767
|
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
12353
13768
|
let docsWritten = 0;
|
|
12354
13769
|
for (const kind of kinds) {
|
|
12355
13770
|
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
12356
|
-
const filePath =
|
|
12357
|
-
if (!
|
|
12358
|
-
let body =
|
|
13771
|
+
const filePath = path20.join(args.sessionDir, fileName);
|
|
13772
|
+
if (!fs17.existsSync(filePath)) continue;
|
|
13773
|
+
let body = fs17.readFileSync(filePath, "utf8");
|
|
12359
13774
|
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
12360
13775
|
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
12361
13776
|
}
|
|
12362
|
-
const stat =
|
|
12363
|
-
const relPath =
|
|
13777
|
+
const stat = fs17.statSync(filePath);
|
|
13778
|
+
const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
12364
13779
|
idx.upsertSessionDoc({
|
|
12365
13780
|
sessionId: args.sessionId,
|
|
12366
13781
|
archived: args.archived,
|
|
@@ -12375,17 +13790,17 @@ function indexSessionDirectory(idx, args) {
|
|
|
12375
13790
|
}
|
|
12376
13791
|
function reindexFromArchive(projectPath, opts = {}) {
|
|
12377
13792
|
const start = Date.now();
|
|
12378
|
-
const archiveBase =
|
|
13793
|
+
const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
|
|
12379
13794
|
const idx = openSearchIndex(projectPath);
|
|
12380
13795
|
try {
|
|
12381
13796
|
idx.resetArchived();
|
|
12382
13797
|
let sessionsIndexed = 0;
|
|
12383
13798
|
let docsWritten = 0;
|
|
12384
|
-
if (
|
|
12385
|
-
const entries =
|
|
13799
|
+
if (fs17.existsSync(archiveBase)) {
|
|
13800
|
+
const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
|
|
12386
13801
|
for (const entry of entries) {
|
|
12387
13802
|
if (!entry.isDirectory()) continue;
|
|
12388
|
-
const sessionDir =
|
|
13803
|
+
const sessionDir = path20.join(archiveBase, entry.name);
|
|
12389
13804
|
const result = indexSessionDirectory(idx, {
|
|
12390
13805
|
sessionId: entry.name,
|
|
12391
13806
|
sessionDir,
|
|
@@ -12405,10 +13820,10 @@ function reindexFromArchive(projectPath, opts = {}) {
|
|
|
12405
13820
|
}
|
|
12406
13821
|
|
|
12407
13822
|
// src/sessions/summarize.ts
|
|
12408
|
-
var
|
|
12409
|
-
var
|
|
12410
|
-
var
|
|
12411
|
-
var
|
|
13823
|
+
var fs18 = __toESM(require("fs"));
|
|
13824
|
+
var path21 = __toESM(require("path"));
|
|
13825
|
+
var import_types32 = require("@harness-engineering/types");
|
|
13826
|
+
var import_types33 = require("@harness-engineering/types");
|
|
12412
13827
|
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
12413
13828
|
var SUMMARY_INPUT_FILES = [
|
|
12414
13829
|
{ filename: "summary.md", kind: "summary" },
|
|
@@ -12434,10 +13849,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
|
|
|
12434
13849
|
function readInputCorpus(archiveDir) {
|
|
12435
13850
|
const parts = [];
|
|
12436
13851
|
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
12437
|
-
const p =
|
|
12438
|
-
if (!
|
|
13852
|
+
const p = path21.join(archiveDir, filename);
|
|
13853
|
+
if (!fs18.existsSync(p)) continue;
|
|
12439
13854
|
try {
|
|
12440
|
-
const content =
|
|
13855
|
+
const content = fs18.readFileSync(p, "utf8");
|
|
12441
13856
|
if (content.trim().length === 0) continue;
|
|
12442
13857
|
parts.push(`## FILE: ${kind}
|
|
12443
13858
|
|
|
@@ -12488,7 +13903,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
|
|
|
12488
13903
|
return lines.join("\n");
|
|
12489
13904
|
}
|
|
12490
13905
|
function writeStubMarkdown(archiveDir, reason) {
|
|
12491
|
-
const filePath =
|
|
13906
|
+
const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
|
|
12492
13907
|
const body = `---
|
|
12493
13908
|
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
12494
13909
|
schemaVersion: 1
|
|
@@ -12499,17 +13914,17 @@ status: failed
|
|
|
12499
13914
|
|
|
12500
13915
|
- reason: ${reason}
|
|
12501
13916
|
`;
|
|
12502
|
-
|
|
13917
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
12503
13918
|
return filePath;
|
|
12504
13919
|
}
|
|
12505
13920
|
async function summarizeArchivedSession(ctx) {
|
|
12506
13921
|
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
12507
|
-
if (!
|
|
12508
|
-
return (0,
|
|
13922
|
+
if (!fs18.existsSync(ctx.archiveDir)) {
|
|
13923
|
+
return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
12509
13924
|
}
|
|
12510
13925
|
const corpus = readInputCorpus(ctx.archiveDir);
|
|
12511
13926
|
if (corpus.trim().length === 0) {
|
|
12512
|
-
return (0,
|
|
13927
|
+
return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
12513
13928
|
}
|
|
12514
13929
|
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
12515
13930
|
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
@@ -12518,7 +13933,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12518
13933
|
const analyzeOpts = {
|
|
12519
13934
|
prompt,
|
|
12520
13935
|
systemPrompt: SYSTEM_PROMPT,
|
|
12521
|
-
responseSchema:
|
|
13936
|
+
responseSchema: import_types32.SessionSummarySchema,
|
|
12522
13937
|
...ctx.config?.model && { model: ctx.config.model }
|
|
12523
13938
|
};
|
|
12524
13939
|
let response;
|
|
@@ -12542,11 +13957,11 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12542
13957
|
} catch {
|
|
12543
13958
|
}
|
|
12544
13959
|
}
|
|
12545
|
-
return (0,
|
|
13960
|
+
return (0, import_types33.Err)(
|
|
12546
13961
|
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
12547
13962
|
);
|
|
12548
13963
|
}
|
|
12549
|
-
const parsed =
|
|
13964
|
+
const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
|
|
12550
13965
|
if (!parsed.success) {
|
|
12551
13966
|
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
12552
13967
|
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
@@ -12556,7 +13971,7 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12556
13971
|
} catch {
|
|
12557
13972
|
}
|
|
12558
13973
|
}
|
|
12559
|
-
return (0,
|
|
13974
|
+
return (0, import_types33.Err)(new Error(reason));
|
|
12560
13975
|
}
|
|
12561
13976
|
const meta = {
|
|
12562
13977
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -12565,10 +13980,10 @@ async function summarizeArchivedSession(ctx) {
|
|
|
12565
13980
|
outputTokens: response.tokenUsage.outputTokens,
|
|
12566
13981
|
schemaVersion: 1
|
|
12567
13982
|
};
|
|
12568
|
-
const filePath =
|
|
13983
|
+
const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
12569
13984
|
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
12570
|
-
|
|
12571
|
-
return (0,
|
|
13985
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13986
|
+
return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
|
|
12572
13987
|
}
|
|
12573
13988
|
function isSummaryEnabled(config) {
|
|
12574
13989
|
if (!config) return false;
|
|
@@ -12645,8 +14060,11 @@ function buildArchiveHooks(opts) {
|
|
|
12645
14060
|
// Annotate the CommonJS export names for ESM import in node:
|
|
12646
14061
|
0 && (module.exports = {
|
|
12647
14062
|
AnalysisArchive,
|
|
14063
|
+
BUILT_IN_TASKS,
|
|
12648
14064
|
BackendRouter,
|
|
12649
14065
|
ClaimManager,
|
|
14066
|
+
GateNotReadyError,
|
|
14067
|
+
GateRunError,
|
|
12650
14068
|
InteractionQueue,
|
|
12651
14069
|
LinearGraphQLStub,
|
|
12652
14070
|
MAX_ATTEMPTS,
|
|
@@ -12655,6 +14073,7 @@ function buildArchiveHooks(opts) {
|
|
|
12655
14073
|
Orchestrator,
|
|
12656
14074
|
OrchestratorBackendFactory,
|
|
12657
14075
|
PRDetector,
|
|
14076
|
+
PromotionError,
|
|
12658
14077
|
PromptRenderer,
|
|
12659
14078
|
RETRY_DELAYS_MS,
|
|
12660
14079
|
RoadmapTrackerAdapter,
|
|
@@ -12663,6 +14082,7 @@ function buildArchiveHooks(opts) {
|
|
|
12663
14082
|
SlackSink,
|
|
12664
14083
|
SqliteSearchIndex,
|
|
12665
14084
|
StreamRecorder,
|
|
14085
|
+
TaskOutputStore,
|
|
12666
14086
|
TokenStore,
|
|
12667
14087
|
WebhookQueue,
|
|
12668
14088
|
WorkflowLoader,
|
|
@@ -12677,6 +14097,9 @@ function buildArchiveHooks(opts) {
|
|
|
12677
14097
|
createBackend,
|
|
12678
14098
|
createEmptyState,
|
|
12679
14099
|
detectScopeTier,
|
|
14100
|
+
emitProposalApproved,
|
|
14101
|
+
emitProposalCreated,
|
|
14102
|
+
emitProposalRejected,
|
|
12680
14103
|
extractHighlights,
|
|
12681
14104
|
extractTitlePrefix,
|
|
12682
14105
|
getAvailableSlots,
|
|
@@ -12690,6 +14113,7 @@ function buildArchiveHooks(opts) {
|
|
|
12690
14113
|
migrateAgentConfig,
|
|
12691
14114
|
normalizeFts5Query,
|
|
12692
14115
|
openSearchIndex,
|
|
14116
|
+
promote,
|
|
12693
14117
|
reconcile,
|
|
12694
14118
|
reindexFromArchive,
|
|
12695
14119
|
renderAnalysisComment,
|
|
@@ -12698,6 +14122,7 @@ function buildArchiveHooks(opts) {
|
|
|
12698
14122
|
resolveEscalationConfig,
|
|
12699
14123
|
resolveOrchestratorId,
|
|
12700
14124
|
routeIssue,
|
|
14125
|
+
runGate,
|
|
12701
14126
|
savePublishedIndex,
|
|
12702
14127
|
searchIndexPath,
|
|
12703
14128
|
selectCandidates,
|
|
@@ -12706,6 +14131,7 @@ function buildArchiveHooks(opts) {
|
|
|
12706
14131
|
syncMain,
|
|
12707
14132
|
triageIssue,
|
|
12708
14133
|
truncateForBudget,
|
|
14134
|
+
validateCustomTasks,
|
|
12709
14135
|
validateWorkflowConfig,
|
|
12710
14136
|
wireNotificationSinks,
|
|
12711
14137
|
wrapAsEnvelope
|