@harness-engineering/orchestrator 0.4.6 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +599 -5
- package/dist/index.d.ts +599 -5
- package/dist/index.js +3313 -455
- package/dist/index.mjs +3283 -429
- package/package.json +5 -5
package/dist/index.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,10 +44,16 @@ __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,
|
|
51
|
+
SinkConfigError: () => SinkConfigError,
|
|
52
|
+
SinkRegistry: () => SinkRegistry,
|
|
53
|
+
SlackSink: () => SlackSink,
|
|
54
|
+
SqliteSearchIndex: () => SqliteSearchIndex,
|
|
47
55
|
StreamRecorder: () => StreamRecorder,
|
|
56
|
+
TaskOutputStore: () => TaskOutputStore,
|
|
48
57
|
TokenStore: () => TokenStore,
|
|
49
58
|
WebhookQueue: () => WebhookQueue,
|
|
50
59
|
WorkflowLoader: () => WorkflowLoader,
|
|
@@ -52,33 +61,51 @@ __export(index_exports, {
|
|
|
52
61
|
WorkspaceManager: () => WorkspaceManager,
|
|
53
62
|
applyEvent: () => applyEvent,
|
|
54
63
|
artifactPresenceFromIssue: () => artifactPresenceFromIssue,
|
|
64
|
+
buildArchiveHooks: () => buildArchiveHooks,
|
|
55
65
|
calculateRetryDelay: () => calculateRetryDelay,
|
|
56
66
|
canDispatch: () => canDispatch,
|
|
57
67
|
computeRateLimitDelay: () => computeRateLimitDelay,
|
|
58
68
|
createBackend: () => createBackend,
|
|
59
69
|
createEmptyState: () => createEmptyState,
|
|
60
70
|
detectScopeTier: () => detectScopeTier,
|
|
71
|
+
emitProposalApproved: () => emitProposalApproved,
|
|
72
|
+
emitProposalCreated: () => emitProposalCreated,
|
|
73
|
+
emitProposalRejected: () => emitProposalRejected,
|
|
61
74
|
extractHighlights: () => extractHighlights,
|
|
62
75
|
extractTitlePrefix: () => extractTitlePrefix,
|
|
63
76
|
getAvailableSlots: () => getAvailableSlots,
|
|
64
77
|
getDefaultConfig: () => getDefaultConfig,
|
|
65
78
|
getPerStateCount: () => getPerStateCount,
|
|
79
|
+
indexSessionDirectory: () => indexSessionDirectory,
|
|
66
80
|
isEligible: () => isEligible,
|
|
81
|
+
isSummaryEnabled: () => isSummaryEnabled,
|
|
67
82
|
launchTUI: () => launchTUI,
|
|
68
83
|
loadPublishedIndex: () => loadPublishedIndex,
|
|
69
84
|
migrateAgentConfig: () => migrateAgentConfig,
|
|
85
|
+
normalizeFts5Query: () => normalizeFts5Query,
|
|
86
|
+
openSearchIndex: () => openSearchIndex,
|
|
87
|
+
promote: () => promote,
|
|
70
88
|
reconcile: () => reconcile,
|
|
89
|
+
reindexFromArchive: () => reindexFromArchive,
|
|
71
90
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
91
|
+
renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
|
|
72
92
|
renderPRComment: () => renderPRComment,
|
|
73
93
|
resolveEscalationConfig: () => resolveEscalationConfig,
|
|
74
94
|
resolveOrchestratorId: () => resolveOrchestratorId,
|
|
75
95
|
routeIssue: () => routeIssue,
|
|
96
|
+
runGate: () => runGate,
|
|
76
97
|
savePublishedIndex: () => savePublishedIndex,
|
|
98
|
+
searchIndexPath: () => searchIndexPath,
|
|
77
99
|
selectCandidates: () => selectCandidates,
|
|
78
100
|
sortCandidates: () => sortCandidates,
|
|
101
|
+
summarizeArchivedSession: () => summarizeArchivedSession,
|
|
79
102
|
syncMain: () => syncMain,
|
|
80
103
|
triageIssue: () => triageIssue,
|
|
81
|
-
|
|
104
|
+
truncateForBudget: () => truncateForBudget,
|
|
105
|
+
validateCustomTasks: () => validateCustomTasks,
|
|
106
|
+
validateWorkflowConfig: () => validateWorkflowConfig,
|
|
107
|
+
wireNotificationSinks: () => wireNotificationSinks,
|
|
108
|
+
wrapAsEnvelope: () => wrapAsEnvelope
|
|
82
109
|
});
|
|
83
110
|
module.exports = __toCommonJS(index_exports);
|
|
84
111
|
|
|
@@ -1228,7 +1255,7 @@ var ClaimManager = class {
|
|
|
1228
1255
|
const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
|
|
1229
1256
|
if (!claimResult.ok) return claimResult;
|
|
1230
1257
|
if (this.verifyDelayMs > 0) {
|
|
1231
|
-
await new Promise((
|
|
1258
|
+
await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
|
|
1232
1259
|
}
|
|
1233
1260
|
const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
|
|
1234
1261
|
if (!statesResult.ok) return statesResult;
|
|
@@ -1951,11 +1978,11 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
|
|
|
1951
1978
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1952
1979
|
const issues = [];
|
|
1953
1980
|
const names = new Set(Object.keys(backends));
|
|
1954
|
-
const checkRef = (
|
|
1981
|
+
const checkRef = (path22, name) => {
|
|
1955
1982
|
if (name !== void 0 && !names.has(name)) {
|
|
1956
1983
|
issues.push({
|
|
1957
|
-
path:
|
|
1958
|
-
message: `routing.${
|
|
1984
|
+
path: path22,
|
|
1985
|
+
message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1959
1986
|
});
|
|
1960
1987
|
}
|
|
1961
1988
|
};
|
|
@@ -2619,7 +2646,7 @@ var WorkspaceHooks = class {
|
|
|
2619
2646
|
if (!command) {
|
|
2620
2647
|
return (0, import_types7.Ok)(void 0);
|
|
2621
2648
|
}
|
|
2622
|
-
return new Promise((
|
|
2649
|
+
return new Promise((resolve7) => {
|
|
2623
2650
|
const filteredEnv = {};
|
|
2624
2651
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2625
2652
|
if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
|
|
@@ -2632,19 +2659,19 @@ var WorkspaceHooks = class {
|
|
|
2632
2659
|
});
|
|
2633
2660
|
const timeout = setTimeout(() => {
|
|
2634
2661
|
child.kill();
|
|
2635
|
-
|
|
2662
|
+
resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
|
|
2636
2663
|
}, this.config.timeoutMs);
|
|
2637
2664
|
child.on("exit", (code) => {
|
|
2638
2665
|
clearTimeout(timeout);
|
|
2639
2666
|
if (code === 0) {
|
|
2640
|
-
|
|
2667
|
+
resolve7((0, import_types7.Ok)(void 0));
|
|
2641
2668
|
} else {
|
|
2642
|
-
|
|
2669
|
+
resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
|
|
2643
2670
|
}
|
|
2644
2671
|
});
|
|
2645
2672
|
child.on("error", (error) => {
|
|
2646
2673
|
clearTimeout(timeout);
|
|
2647
|
-
|
|
2674
|
+
resolve7((0, import_types7.Err)(error));
|
|
2648
2675
|
});
|
|
2649
2676
|
});
|
|
2650
2677
|
}
|
|
@@ -2682,7 +2709,7 @@ var MockBackend = class {
|
|
|
2682
2709
|
content: "Thinking...",
|
|
2683
2710
|
sessionId: session.sessionId
|
|
2684
2711
|
};
|
|
2685
|
-
await new Promise((
|
|
2712
|
+
await new Promise((resolve7) => setTimeout(resolve7, 100));
|
|
2686
2713
|
yield {
|
|
2687
2714
|
type: "thought",
|
|
2688
2715
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2734,9 +2761,9 @@ var PromptRenderer = class {
|
|
|
2734
2761
|
|
|
2735
2762
|
// src/orchestrator.ts
|
|
2736
2763
|
var import_node_events = require("events");
|
|
2737
|
-
var
|
|
2738
|
-
var
|
|
2739
|
-
var
|
|
2764
|
+
var path19 = __toESM(require("path"));
|
|
2765
|
+
var import_node_crypto16 = require("crypto");
|
|
2766
|
+
var import_core14 = require("@harness-engineering/core");
|
|
2740
2767
|
|
|
2741
2768
|
// src/intelligence/pipeline-runner.ts
|
|
2742
2769
|
var path7 = __toESM(require("path"));
|
|
@@ -3297,7 +3324,7 @@ var CompletionHandler = class {
|
|
|
3297
3324
|
};
|
|
3298
3325
|
|
|
3299
3326
|
// src/orchestrator.ts
|
|
3300
|
-
var
|
|
3327
|
+
var import_core15 = require("@harness-engineering/core");
|
|
3301
3328
|
|
|
3302
3329
|
// src/tracker/adapters/github-issues-issue-tracker.ts
|
|
3303
3330
|
var import_types9 = require("@harness-engineering/types");
|
|
@@ -3701,11 +3728,11 @@ function detectLegacyFields(agent) {
|
|
|
3701
3728
|
}
|
|
3702
3729
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3703
3730
|
const warnings = [];
|
|
3704
|
-
for (const
|
|
3705
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3706
|
-
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;
|
|
3707
3734
|
warnings.push(
|
|
3708
|
-
`Ignoring legacy field '${
|
|
3735
|
+
`Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3709
3736
|
);
|
|
3710
3737
|
}
|
|
3711
3738
|
return warnings;
|
|
@@ -3733,7 +3760,7 @@ function migrateAgentConfig(agent) {
|
|
|
3733
3760
|
}
|
|
3734
3761
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3735
3762
|
const warnings = presentLegacy.map(
|
|
3736
|
-
(
|
|
3763
|
+
(path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3737
3764
|
);
|
|
3738
3765
|
return {
|
|
3739
3766
|
config: { ...agent, backends, routing },
|
|
@@ -3822,6 +3849,10 @@ var BackendRouter = class {
|
|
|
3822
3849
|
const intel = this.routing.intelligence;
|
|
3823
3850
|
return intel?.[useCase.layer] ?? this.routing.default;
|
|
3824
3851
|
}
|
|
3852
|
+
case "isolation": {
|
|
3853
|
+
const iso = this.routing.isolation;
|
|
3854
|
+
return iso?.[useCase.tier] ?? this.routing.default;
|
|
3855
|
+
}
|
|
3825
3856
|
case "maintenance":
|
|
3826
3857
|
case "chat":
|
|
3827
3858
|
return this.routing.default;
|
|
@@ -3845,8 +3876,8 @@ var BackendRouter = class {
|
|
|
3845
3876
|
validateReferences() {
|
|
3846
3877
|
const known = new Set(Object.keys(this.backends));
|
|
3847
3878
|
const missing = [];
|
|
3848
|
-
const check = (
|
|
3849
|
-
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 });
|
|
3850
3881
|
};
|
|
3851
3882
|
check("default", this.routing.default);
|
|
3852
3883
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3855,8 +3886,11 @@ var BackendRouter = class {
|
|
|
3855
3886
|
check("diagnostic", this.routing.diagnostic);
|
|
3856
3887
|
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3857
3888
|
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3889
|
+
check("isolation.none", this.routing.isolation?.none);
|
|
3890
|
+
check("isolation.container", this.routing.isolation?.container);
|
|
3891
|
+
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
3858
3892
|
if (missing.length > 0) {
|
|
3859
|
-
const detail = missing.map(({ path:
|
|
3893
|
+
const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
|
|
3860
3894
|
const known_ = [...known].join(", ") || "(none)";
|
|
3861
3895
|
throw new Error(
|
|
3862
3896
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -3870,11 +3904,11 @@ var import_node_child_process4 = require("child_process");
|
|
|
3870
3904
|
var readline = __toESM(require("readline"));
|
|
3871
3905
|
var import_node_crypto3 = require("crypto");
|
|
3872
3906
|
var import_types10 = require("@harness-engineering/types");
|
|
3873
|
-
function resolveExitCode(code, command,
|
|
3907
|
+
function resolveExitCode(code, command, resolve7) {
|
|
3874
3908
|
if (code === 0) {
|
|
3875
|
-
|
|
3909
|
+
resolve7((0, import_types10.Ok)(void 0));
|
|
3876
3910
|
} else {
|
|
3877
|
-
|
|
3911
|
+
resolve7(
|
|
3878
3912
|
(0, import_types10.Err)({
|
|
3879
3913
|
category: "agent_not_found",
|
|
3880
3914
|
message: `Claude command '${command}' not found or failed`
|
|
@@ -3882,8 +3916,8 @@ function resolveExitCode(code, command, resolve6) {
|
|
|
3882
3916
|
);
|
|
3883
3917
|
}
|
|
3884
3918
|
}
|
|
3885
|
-
function resolveSpawnError(command,
|
|
3886
|
-
|
|
3919
|
+
function resolveSpawnError(command, resolve7) {
|
|
3920
|
+
resolve7((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
|
|
3887
3921
|
}
|
|
3888
3922
|
var JUST_PAST_GRACE_MS = 5 * 6e4;
|
|
3889
3923
|
var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
|
|
@@ -4196,10 +4230,10 @@ var ClaudeBackend = class {
|
|
|
4196
4230
|
errRl.close();
|
|
4197
4231
|
}
|
|
4198
4232
|
if (exitCode === null) {
|
|
4199
|
-
await new Promise((
|
|
4233
|
+
await new Promise((resolve7) => {
|
|
4200
4234
|
child.on("exit", (code) => {
|
|
4201
4235
|
exitCode = code;
|
|
4202
|
-
|
|
4236
|
+
resolve7(null);
|
|
4203
4237
|
});
|
|
4204
4238
|
});
|
|
4205
4239
|
}
|
|
@@ -4221,10 +4255,10 @@ var ClaudeBackend = class {
|
|
|
4221
4255
|
return (0, import_types10.Ok)(void 0);
|
|
4222
4256
|
}
|
|
4223
4257
|
async healthCheck() {
|
|
4224
|
-
return new Promise((
|
|
4258
|
+
return new Promise((resolve7) => {
|
|
4225
4259
|
const child = (0, import_node_child_process4.spawn)(this.command, ["--version"]);
|
|
4226
|
-
child.on("exit", (code) => resolveExitCode(code, this.command,
|
|
4227
|
-
child.on("error", () => resolveSpawnError(this.command,
|
|
4260
|
+
child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
|
|
4261
|
+
child.on("error", () => resolveSpawnError(this.command, resolve7));
|
|
4228
4262
|
});
|
|
4229
4263
|
}
|
|
4230
4264
|
};
|
|
@@ -4832,7 +4866,7 @@ var PiBackend = class {
|
|
|
4832
4866
|
} else {
|
|
4833
4867
|
resolvedModelName = this.config.model;
|
|
4834
4868
|
}
|
|
4835
|
-
const piSdk = await import("@
|
|
4869
|
+
const piSdk = await import("@earendil-works/pi-coding-agent");
|
|
4836
4870
|
const model = buildLocalModel({
|
|
4837
4871
|
model: resolvedModelName,
|
|
4838
4872
|
endpoint: this.config.endpoint,
|
|
@@ -4987,7 +5021,7 @@ var PiBackend = class {
|
|
|
4987
5021
|
}
|
|
4988
5022
|
async healthCheck() {
|
|
4989
5023
|
try {
|
|
4990
|
-
await import("@
|
|
5024
|
+
await import("@earendil-works/pi-coding-agent");
|
|
4991
5025
|
return (0, import_types15.Ok)(void 0);
|
|
4992
5026
|
} catch (err) {
|
|
4993
5027
|
return (0, import_types15.Err)({
|
|
@@ -4998,6 +5032,541 @@ var PiBackend = class {
|
|
|
4998
5032
|
}
|
|
4999
5033
|
};
|
|
5000
5034
|
|
|
5035
|
+
// src/agent/backends/ssh.ts
|
|
5036
|
+
var import_node_child_process5 = require("child_process");
|
|
5037
|
+
var import_types16 = require("@harness-engineering/types");
|
|
5038
|
+
var DEFAULT_TIMEOUT_MS2 = 9e4;
|
|
5039
|
+
var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
|
|
5040
|
+
var SshBackend = class {
|
|
5041
|
+
name = "ssh";
|
|
5042
|
+
config;
|
|
5043
|
+
spawnImpl;
|
|
5044
|
+
constructor(config) {
|
|
5045
|
+
if (!config.host || typeof config.host !== "string") {
|
|
5046
|
+
throw new Error("SshBackend: `host` is required");
|
|
5047
|
+
}
|
|
5048
|
+
if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
|
|
5049
|
+
throw new Error(
|
|
5050
|
+
`SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
|
|
5051
|
+
);
|
|
5052
|
+
}
|
|
5053
|
+
if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
|
|
5054
|
+
throw new Error("SshBackend: `remoteCommand` is required");
|
|
5055
|
+
}
|
|
5056
|
+
if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
|
|
5057
|
+
throw new Error(`SshBackend: invalid user '${config.user}'`);
|
|
5058
|
+
}
|
|
5059
|
+
this.config = {
|
|
5060
|
+
host: config.host,
|
|
5061
|
+
remoteCommand: config.remoteCommand,
|
|
5062
|
+
sshBinary: config.sshBinary ?? "ssh",
|
|
5063
|
+
sshOptions: config.sshOptions ?? [],
|
|
5064
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
5065
|
+
...config.user !== void 0 ? { user: config.user } : {},
|
|
5066
|
+
...config.port !== void 0 ? { port: config.port } : {},
|
|
5067
|
+
...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
|
|
5068
|
+
};
|
|
5069
|
+
this.spawnImpl = config.spawnImpl ?? import_node_child_process5.spawn;
|
|
5070
|
+
}
|
|
5071
|
+
/**
|
|
5072
|
+
* Builds the argv passed to the `ssh` binary. Exported as a method on
|
|
5073
|
+
* the class so tests can assert the exact shape without spawning.
|
|
5074
|
+
*
|
|
5075
|
+
* Layout: `[options..., target, '--', remoteCommand]`
|
|
5076
|
+
*/
|
|
5077
|
+
buildSshArgs() {
|
|
5078
|
+
const args = [];
|
|
5079
|
+
if (this.config.identityFile) {
|
|
5080
|
+
args.push("-i", this.config.identityFile);
|
|
5081
|
+
}
|
|
5082
|
+
if (this.config.port !== void 0) {
|
|
5083
|
+
args.push("-p", String(this.config.port));
|
|
5084
|
+
}
|
|
5085
|
+
args.push("-o", "BatchMode=yes");
|
|
5086
|
+
for (const opt of this.config.sshOptions) {
|
|
5087
|
+
args.push("-o", opt);
|
|
5088
|
+
}
|
|
5089
|
+
const target = this.config.user ? `${this.config.user}@${this.config.host}` : this.config.host;
|
|
5090
|
+
args.push(target);
|
|
5091
|
+
args.push("--");
|
|
5092
|
+
args.push(this.config.remoteCommand);
|
|
5093
|
+
return args;
|
|
5094
|
+
}
|
|
5095
|
+
async startSession(params) {
|
|
5096
|
+
const session = {
|
|
5097
|
+
sessionId: `ssh-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5098
|
+
workspacePath: params.workspacePath,
|
|
5099
|
+
backendName: this.name,
|
|
5100
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5101
|
+
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
5102
|
+
};
|
|
5103
|
+
return (0, import_types16.Ok)(session);
|
|
5104
|
+
}
|
|
5105
|
+
async *runTurn(session, params) {
|
|
5106
|
+
const child = this.spawnImpl(this.config.sshBinary, this.buildSshArgs(), {
|
|
5107
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5108
|
+
});
|
|
5109
|
+
const payload = JSON.stringify({
|
|
5110
|
+
kind: "turn",
|
|
5111
|
+
prompt: params.prompt,
|
|
5112
|
+
isContinuation: params.isContinuation,
|
|
5113
|
+
systemPrompt: session.systemPrompt
|
|
5114
|
+
});
|
|
5115
|
+
try {
|
|
5116
|
+
child.stdin.write(payload + "\n");
|
|
5117
|
+
child.stdin.end();
|
|
5118
|
+
} catch (err) {
|
|
5119
|
+
const message = err instanceof Error ? err.message : "failed to write to ssh stdin";
|
|
5120
|
+
try {
|
|
5121
|
+
child.kill("SIGTERM");
|
|
5122
|
+
} catch {
|
|
5123
|
+
}
|
|
5124
|
+
return errResult(session.sessionId, message);
|
|
5125
|
+
}
|
|
5126
|
+
const timeout = setTimeout(() => {
|
|
5127
|
+
try {
|
|
5128
|
+
child.kill("SIGTERM");
|
|
5129
|
+
} catch {
|
|
5130
|
+
}
|
|
5131
|
+
}, this.config.timeoutMs);
|
|
5132
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5133
|
+
let success = true;
|
|
5134
|
+
let lastError;
|
|
5135
|
+
try {
|
|
5136
|
+
for await (const line of readLines(child.stdout)) {
|
|
5137
|
+
let event;
|
|
5138
|
+
try {
|
|
5139
|
+
event = parseEvent(line, session.sessionId);
|
|
5140
|
+
} catch (err) {
|
|
5141
|
+
const message = err instanceof Error ? err.message : "unparseable ssh event";
|
|
5142
|
+
success = false;
|
|
5143
|
+
lastError = message;
|
|
5144
|
+
break;
|
|
5145
|
+
}
|
|
5146
|
+
if (!event) continue;
|
|
5147
|
+
if (event.usage) finalUsage = event.usage;
|
|
5148
|
+
if (event.type === "error" && typeof event.content === "string") {
|
|
5149
|
+
lastError = event.content;
|
|
5150
|
+
success = false;
|
|
5151
|
+
}
|
|
5152
|
+
yield event;
|
|
5153
|
+
}
|
|
5154
|
+
const exitCode = await waitForExit(child);
|
|
5155
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
5156
|
+
success = false;
|
|
5157
|
+
lastError = lastError ?? `ssh exited with code ${exitCode}`;
|
|
5158
|
+
}
|
|
5159
|
+
} finally {
|
|
5160
|
+
clearTimeout(timeout);
|
|
5161
|
+
}
|
|
5162
|
+
return {
|
|
5163
|
+
success,
|
|
5164
|
+
sessionId: session.sessionId,
|
|
5165
|
+
usage: finalUsage,
|
|
5166
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5167
|
+
};
|
|
5168
|
+
}
|
|
5169
|
+
async stopSession(_session) {
|
|
5170
|
+
return (0, import_types16.Ok)(void 0);
|
|
5171
|
+
}
|
|
5172
|
+
async healthCheck() {
|
|
5173
|
+
const args = [...this.buildSshArgs()];
|
|
5174
|
+
args[args.length - 1] = "true";
|
|
5175
|
+
return new Promise((resolve7) => {
|
|
5176
|
+
let child;
|
|
5177
|
+
try {
|
|
5178
|
+
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5179
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
5180
|
+
});
|
|
5181
|
+
} catch (err) {
|
|
5182
|
+
resolve7(
|
|
5183
|
+
(0, import_types16.Err)({
|
|
5184
|
+
category: "agent_not_found",
|
|
5185
|
+
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
5186
|
+
})
|
|
5187
|
+
);
|
|
5188
|
+
return;
|
|
5189
|
+
}
|
|
5190
|
+
let stderr = "";
|
|
5191
|
+
child.stderr?.on("data", (chunk) => {
|
|
5192
|
+
stderr += chunk.toString();
|
|
5193
|
+
});
|
|
5194
|
+
const timer = setTimeout(() => {
|
|
5195
|
+
try {
|
|
5196
|
+
child.kill("SIGTERM");
|
|
5197
|
+
} catch {
|
|
5198
|
+
}
|
|
5199
|
+
}, this.config.timeoutMs);
|
|
5200
|
+
child.on("close", (code) => {
|
|
5201
|
+
clearTimeout(timer);
|
|
5202
|
+
if (code === 0) {
|
|
5203
|
+
resolve7((0, import_types16.Ok)(void 0));
|
|
5204
|
+
} else {
|
|
5205
|
+
resolve7(
|
|
5206
|
+
(0, import_types16.Err)({
|
|
5207
|
+
category: "agent_not_found",
|
|
5208
|
+
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
5209
|
+
})
|
|
5210
|
+
);
|
|
5211
|
+
}
|
|
5212
|
+
});
|
|
5213
|
+
child.on("error", (err) => {
|
|
5214
|
+
clearTimeout(timer);
|
|
5215
|
+
resolve7((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
|
|
5216
|
+
});
|
|
5217
|
+
});
|
|
5218
|
+
}
|
|
5219
|
+
};
|
|
5220
|
+
function errResult(sessionId, message) {
|
|
5221
|
+
return {
|
|
5222
|
+
success: false,
|
|
5223
|
+
sessionId,
|
|
5224
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5225
|
+
error: message
|
|
5226
|
+
};
|
|
5227
|
+
}
|
|
5228
|
+
function parseEvent(line, sessionId) {
|
|
5229
|
+
const trimmed = line.trim();
|
|
5230
|
+
if (trimmed.length === 0) return null;
|
|
5231
|
+
const raw = JSON.parse(trimmed);
|
|
5232
|
+
if (typeof raw.type !== "string") {
|
|
5233
|
+
throw new Error(`ssh event missing 'type': ${trimmed.slice(0, 200)}`);
|
|
5234
|
+
}
|
|
5235
|
+
const ev = {
|
|
5236
|
+
type: raw.type,
|
|
5237
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5238
|
+
sessionId
|
|
5239
|
+
};
|
|
5240
|
+
if (typeof raw.subtype === "string") ev.subtype = raw.subtype;
|
|
5241
|
+
if (raw.content !== void 0) ev.content = raw.content;
|
|
5242
|
+
if (isUsage(raw.usage)) ev.usage = raw.usage;
|
|
5243
|
+
return ev;
|
|
5244
|
+
}
|
|
5245
|
+
function isUsage(u) {
|
|
5246
|
+
if (!u || typeof u !== "object") return false;
|
|
5247
|
+
const o = u;
|
|
5248
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5249
|
+
}
|
|
5250
|
+
async function* readLines(stream) {
|
|
5251
|
+
let buffer = "";
|
|
5252
|
+
for await (const chunk of stream) {
|
|
5253
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5254
|
+
let idx;
|
|
5255
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5256
|
+
yield buffer.slice(0, idx);
|
|
5257
|
+
buffer = buffer.slice(idx + 1);
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
if (buffer.length > 0) yield buffer;
|
|
5261
|
+
}
|
|
5262
|
+
function waitForExit(child) {
|
|
5263
|
+
return new Promise((resolve7) => {
|
|
5264
|
+
if (child.exitCode !== null) {
|
|
5265
|
+
resolve7(child.exitCode);
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
child.once("close", (code) => resolve7(code));
|
|
5269
|
+
child.once("error", () => resolve7(null));
|
|
5270
|
+
});
|
|
5271
|
+
}
|
|
5272
|
+
|
|
5273
|
+
// src/agent/backends/serverless.ts
|
|
5274
|
+
var import_node_child_process6 = require("child_process");
|
|
5275
|
+
var import_types17 = require("@harness-engineering/types");
|
|
5276
|
+
var ServerlessBackend = class {
|
|
5277
|
+
handles = /* @__PURE__ */ new Map();
|
|
5278
|
+
async startSession(params) {
|
|
5279
|
+
const start = await this.coldStart(params);
|
|
5280
|
+
if (!start.ok) return start;
|
|
5281
|
+
const session = {
|
|
5282
|
+
sessionId: `${this.name}-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5283
|
+
workspacePath: params.workspacePath,
|
|
5284
|
+
backendName: this.name,
|
|
5285
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5286
|
+
};
|
|
5287
|
+
this.handles.set(session.sessionId, start.value);
|
|
5288
|
+
return (0, import_types17.Ok)(session);
|
|
5289
|
+
}
|
|
5290
|
+
async *runTurn(session, params) {
|
|
5291
|
+
const handle = this.handles.get(session.sessionId);
|
|
5292
|
+
if (!handle) {
|
|
5293
|
+
return {
|
|
5294
|
+
success: false,
|
|
5295
|
+
sessionId: session.sessionId,
|
|
5296
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5297
|
+
error: `no serverless handle for session ${session.sessionId}`
|
|
5298
|
+
};
|
|
5299
|
+
}
|
|
5300
|
+
return yield* this.runOnHandle(handle, params, session);
|
|
5301
|
+
}
|
|
5302
|
+
async stopSession(session) {
|
|
5303
|
+
const handle = this.handles.get(session.sessionId);
|
|
5304
|
+
if (!handle) return (0, import_types17.Ok)(void 0);
|
|
5305
|
+
this.handles.delete(session.sessionId);
|
|
5306
|
+
return this.teardown(handle);
|
|
5307
|
+
}
|
|
5308
|
+
};
|
|
5309
|
+
var FORBIDDEN_IMAGE_CHARS = /[;&|`$()\n\r<>]/;
|
|
5310
|
+
var BLOCKED_DOCKER_FLAGS = [
|
|
5311
|
+
"--privileged",
|
|
5312
|
+
"--cap-add",
|
|
5313
|
+
"--security-opt",
|
|
5314
|
+
"--pid",
|
|
5315
|
+
"--ipc",
|
|
5316
|
+
"--userns"
|
|
5317
|
+
];
|
|
5318
|
+
var DEFAULT_OCI_TIMEOUT_MS = 9e4;
|
|
5319
|
+
var OciServerlessBackend = class extends ServerlessBackend {
|
|
5320
|
+
name = "serverless:oci";
|
|
5321
|
+
config;
|
|
5322
|
+
spawnImpl;
|
|
5323
|
+
envSource;
|
|
5324
|
+
constructor(config) {
|
|
5325
|
+
super();
|
|
5326
|
+
if (!config.image || typeof config.image !== "string") {
|
|
5327
|
+
throw new Error("OciServerlessBackend: `image` is required");
|
|
5328
|
+
}
|
|
5329
|
+
if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
|
|
5330
|
+
throw new Error(
|
|
5331
|
+
`OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
|
|
5332
|
+
);
|
|
5333
|
+
}
|
|
5334
|
+
this.config = {
|
|
5335
|
+
image: config.image,
|
|
5336
|
+
pullPolicy: config.pullPolicy ?? "if-not-present",
|
|
5337
|
+
runtime: config.runtime ?? "docker",
|
|
5338
|
+
envPassthrough: config.envPassthrough ?? [],
|
|
5339
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
|
|
5340
|
+
extraArgs: sanitizeExtraArgs(config.extraArgs),
|
|
5341
|
+
...config.registry !== void 0 ? { registry: config.registry } : {}
|
|
5342
|
+
};
|
|
5343
|
+
this.spawnImpl = config.spawnImpl ?? import_node_child_process6.spawn;
|
|
5344
|
+
this.envSource = config.envSource ?? process.env;
|
|
5345
|
+
}
|
|
5346
|
+
/** Builds the argv for `docker run -d ...`. Exposed for tests. */
|
|
5347
|
+
buildRunArgs() {
|
|
5348
|
+
const env = this.collectEnv();
|
|
5349
|
+
const args = ["run", "-d", "--rm"];
|
|
5350
|
+
for (const [k, v] of Object.entries(env)) {
|
|
5351
|
+
args.push("-e", `${k}=${v}`);
|
|
5352
|
+
}
|
|
5353
|
+
for (const ea of this.config.extraArgs) {
|
|
5354
|
+
args.push(ea);
|
|
5355
|
+
}
|
|
5356
|
+
args.push("--");
|
|
5357
|
+
args.push(this.config.image);
|
|
5358
|
+
return args;
|
|
5359
|
+
}
|
|
5360
|
+
/** Builds the argv for `docker exec <id> -- agent`. Exposed for tests. */
|
|
5361
|
+
buildExecArgs(handleId) {
|
|
5362
|
+
return ["exec", "-i", handleId, "/agent"];
|
|
5363
|
+
}
|
|
5364
|
+
async coldStart(_params) {
|
|
5365
|
+
if (this.config.pullPolicy === "always") {
|
|
5366
|
+
const pull = await this.runOneShot(this.config.runtime, ["pull", this.config.image]);
|
|
5367
|
+
if (!pull.ok) return pull;
|
|
5368
|
+
}
|
|
5369
|
+
const result = await this.runOneShot(this.config.runtime, this.buildRunArgs());
|
|
5370
|
+
if (!result.ok) return result;
|
|
5371
|
+
const id = result.value.trim().split(/\s+/)[0] ?? "";
|
|
5372
|
+
if (!id) {
|
|
5373
|
+
return (0, import_types17.Err)({
|
|
5374
|
+
category: "response_error",
|
|
5375
|
+
message: "OciServerlessBackend: empty container id from runtime"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
return (0, import_types17.Ok)({ id, adapter: this.name });
|
|
5379
|
+
}
|
|
5380
|
+
async *runOnHandle(handle, params, session) {
|
|
5381
|
+
const child = this.spawnImpl(this.config.runtime, this.buildExecArgs(handle.id), {
|
|
5382
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5383
|
+
});
|
|
5384
|
+
const payload = JSON.stringify({
|
|
5385
|
+
kind: "turn",
|
|
5386
|
+
prompt: params.prompt,
|
|
5387
|
+
isContinuation: params.isContinuation
|
|
5388
|
+
});
|
|
5389
|
+
try {
|
|
5390
|
+
child.stdin.write(payload + "\n");
|
|
5391
|
+
child.stdin.end();
|
|
5392
|
+
} catch (err) {
|
|
5393
|
+
const message = err instanceof Error ? err.message : "failed to write to docker stdin";
|
|
5394
|
+
return turnFailure(session.sessionId, message);
|
|
5395
|
+
}
|
|
5396
|
+
const timeout = setTimeout(() => {
|
|
5397
|
+
try {
|
|
5398
|
+
child.kill("SIGTERM");
|
|
5399
|
+
} catch {
|
|
5400
|
+
}
|
|
5401
|
+
}, this.config.timeoutMs);
|
|
5402
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5403
|
+
let success = true;
|
|
5404
|
+
let lastError;
|
|
5405
|
+
try {
|
|
5406
|
+
for await (const line of readLines2(child.stdout)) {
|
|
5407
|
+
const ev = tryParseEvent(line, session.sessionId);
|
|
5408
|
+
if (!ev) continue;
|
|
5409
|
+
if (ev.usage) finalUsage = ev.usage;
|
|
5410
|
+
if (ev.type === "error" && typeof ev.content === "string") {
|
|
5411
|
+
success = false;
|
|
5412
|
+
lastError = ev.content;
|
|
5413
|
+
}
|
|
5414
|
+
yield ev;
|
|
5415
|
+
}
|
|
5416
|
+
const code = await waitForExit2(child);
|
|
5417
|
+
if (code !== 0 && code !== null) {
|
|
5418
|
+
success = false;
|
|
5419
|
+
lastError = lastError ?? `runtime exec exited with code ${code}`;
|
|
5420
|
+
}
|
|
5421
|
+
} finally {
|
|
5422
|
+
clearTimeout(timeout);
|
|
5423
|
+
}
|
|
5424
|
+
return {
|
|
5425
|
+
success,
|
|
5426
|
+
sessionId: session.sessionId,
|
|
5427
|
+
usage: finalUsage,
|
|
5428
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5429
|
+
};
|
|
5430
|
+
}
|
|
5431
|
+
async teardown(handle) {
|
|
5432
|
+
if (handle.adapter !== this.name) {
|
|
5433
|
+
return (0, import_types17.Err)({
|
|
5434
|
+
category: "response_error",
|
|
5435
|
+
message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
|
|
5436
|
+
});
|
|
5437
|
+
}
|
|
5438
|
+
const stop = await this.runOneShot(this.config.runtime, ["stop", handle.id]);
|
|
5439
|
+
if (!stop.ok) return stop;
|
|
5440
|
+
return (0, import_types17.Ok)(void 0);
|
|
5441
|
+
}
|
|
5442
|
+
async healthCheck() {
|
|
5443
|
+
return mapOk(
|
|
5444
|
+
await this.runOneShot(this.config.runtime, ["version", "--format", "{{.Server.Version}}"])
|
|
5445
|
+
);
|
|
5446
|
+
}
|
|
5447
|
+
collectEnv() {
|
|
5448
|
+
const out = {};
|
|
5449
|
+
for (const key of this.config.envPassthrough) {
|
|
5450
|
+
const val = this.envSource[key];
|
|
5451
|
+
if (typeof val === "string") out[key] = val;
|
|
5452
|
+
}
|
|
5453
|
+
return out;
|
|
5454
|
+
}
|
|
5455
|
+
runOneShot(binary, args) {
|
|
5456
|
+
return new Promise((resolve7) => {
|
|
5457
|
+
let child;
|
|
5458
|
+
try {
|
|
5459
|
+
child = this.spawnImpl(binary, args, {
|
|
5460
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5461
|
+
});
|
|
5462
|
+
} catch (err) {
|
|
5463
|
+
resolve7(
|
|
5464
|
+
(0, import_types17.Err)({
|
|
5465
|
+
category: "agent_not_found",
|
|
5466
|
+
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
5467
|
+
})
|
|
5468
|
+
);
|
|
5469
|
+
return;
|
|
5470
|
+
}
|
|
5471
|
+
let stdout = "";
|
|
5472
|
+
let stderr = "";
|
|
5473
|
+
child.stdout?.on("data", (chunk) => {
|
|
5474
|
+
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5475
|
+
});
|
|
5476
|
+
child.stderr?.on("data", (chunk) => {
|
|
5477
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5478
|
+
});
|
|
5479
|
+
const timer = setTimeout(() => {
|
|
5480
|
+
try {
|
|
5481
|
+
child.kill("SIGTERM");
|
|
5482
|
+
} catch {
|
|
5483
|
+
}
|
|
5484
|
+
}, this.config.timeoutMs);
|
|
5485
|
+
child.on("close", (code) => {
|
|
5486
|
+
clearTimeout(timer);
|
|
5487
|
+
if (code === 0) {
|
|
5488
|
+
resolve7((0, import_types17.Ok)(stdout));
|
|
5489
|
+
} else {
|
|
5490
|
+
resolve7(
|
|
5491
|
+
(0, import_types17.Err)({
|
|
5492
|
+
category: "response_error",
|
|
5493
|
+
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
5494
|
+
})
|
|
5495
|
+
);
|
|
5496
|
+
}
|
|
5497
|
+
});
|
|
5498
|
+
child.on("error", (err) => {
|
|
5499
|
+
clearTimeout(timer);
|
|
5500
|
+
resolve7((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
|
|
5501
|
+
});
|
|
5502
|
+
});
|
|
5503
|
+
}
|
|
5504
|
+
};
|
|
5505
|
+
function sanitizeExtraArgs(extraArgs) {
|
|
5506
|
+
if (!extraArgs) return [];
|
|
5507
|
+
return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5508
|
+
}
|
|
5509
|
+
function mapOk(r) {
|
|
5510
|
+
return r.ok ? (0, import_types17.Ok)(void 0) : r;
|
|
5511
|
+
}
|
|
5512
|
+
function turnFailure(sessionId, message) {
|
|
5513
|
+
return {
|
|
5514
|
+
success: false,
|
|
5515
|
+
sessionId,
|
|
5516
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5517
|
+
error: message
|
|
5518
|
+
};
|
|
5519
|
+
}
|
|
5520
|
+
function tryParseEvent(line, sessionId) {
|
|
5521
|
+
const trimmed = line.trim();
|
|
5522
|
+
if (!trimmed) return null;
|
|
5523
|
+
let raw;
|
|
5524
|
+
try {
|
|
5525
|
+
raw = JSON.parse(trimmed);
|
|
5526
|
+
} catch {
|
|
5527
|
+
return null;
|
|
5528
|
+
}
|
|
5529
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5530
|
+
const o = raw;
|
|
5531
|
+
if (typeof o.type !== "string") return null;
|
|
5532
|
+
const ev = {
|
|
5533
|
+
type: o.type,
|
|
5534
|
+
timestamp: typeof o.timestamp === "string" ? o.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5535
|
+
sessionId
|
|
5536
|
+
};
|
|
5537
|
+
if (typeof o.subtype === "string") ev.subtype = o.subtype;
|
|
5538
|
+
if (o.content !== void 0) ev.content = o.content;
|
|
5539
|
+
if (isUsage2(o.usage)) ev.usage = o.usage;
|
|
5540
|
+
return ev;
|
|
5541
|
+
}
|
|
5542
|
+
function isUsage2(u) {
|
|
5543
|
+
if (!u || typeof u !== "object") return false;
|
|
5544
|
+
const o = u;
|
|
5545
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5546
|
+
}
|
|
5547
|
+
async function* readLines2(stream) {
|
|
5548
|
+
let buffer = "";
|
|
5549
|
+
for await (const chunk of stream) {
|
|
5550
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5551
|
+
let idx;
|
|
5552
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5553
|
+
yield buffer.slice(0, idx);
|
|
5554
|
+
buffer = buffer.slice(idx + 1);
|
|
5555
|
+
}
|
|
5556
|
+
}
|
|
5557
|
+
if (buffer.length > 0) yield buffer;
|
|
5558
|
+
}
|
|
5559
|
+
function waitForExit2(child) {
|
|
5560
|
+
return new Promise((resolve7) => {
|
|
5561
|
+
if (child.exitCode !== null) {
|
|
5562
|
+
resolve7(child.exitCode);
|
|
5563
|
+
return;
|
|
5564
|
+
}
|
|
5565
|
+
child.once("close", (code) => resolve7(code));
|
|
5566
|
+
child.once("error", () => resolve7(null));
|
|
5567
|
+
});
|
|
5568
|
+
}
|
|
5569
|
+
|
|
5001
5570
|
// src/agent/backend-factory.ts
|
|
5002
5571
|
function makeGetModel(model) {
|
|
5003
5572
|
if (typeof model === "string") return () => model;
|
|
@@ -5047,6 +5616,35 @@ function createBackend(def, options = {}) {
|
|
|
5047
5616
|
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5048
5617
|
});
|
|
5049
5618
|
}
|
|
5619
|
+
case "ssh": {
|
|
5620
|
+
return new SshBackend({
|
|
5621
|
+
host: def.host,
|
|
5622
|
+
remoteCommand: def.remoteCommand,
|
|
5623
|
+
...def.user !== void 0 ? { user: def.user } : {},
|
|
5624
|
+
...def.port !== void 0 ? { port: def.port } : {},
|
|
5625
|
+
...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
|
|
5626
|
+
...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
|
|
5627
|
+
...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
|
|
5628
|
+
});
|
|
5629
|
+
}
|
|
5630
|
+
case "serverless": {
|
|
5631
|
+
switch (def.adapter) {
|
|
5632
|
+
case "oci":
|
|
5633
|
+
return new OciServerlessBackend({
|
|
5634
|
+
image: def.image,
|
|
5635
|
+
...def.registry !== void 0 ? { registry: def.registry } : {},
|
|
5636
|
+
...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
|
|
5637
|
+
...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
|
|
5638
|
+
...def.runtime !== void 0 ? { runtime: def.runtime } : {}
|
|
5639
|
+
});
|
|
5640
|
+
default: {
|
|
5641
|
+
const exhaustive = def.adapter;
|
|
5642
|
+
throw new Error(
|
|
5643
|
+
`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
|
|
5644
|
+
);
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
5050
5648
|
default: {
|
|
5051
5649
|
const exhaustive = def;
|
|
5052
5650
|
throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
|
|
@@ -5055,12 +5653,12 @@ function createBackend(def, options = {}) {
|
|
|
5055
5653
|
}
|
|
5056
5654
|
|
|
5057
5655
|
// src/agent/backends/container.ts
|
|
5058
|
-
var
|
|
5656
|
+
var import_types18 = require("@harness-engineering/types");
|
|
5059
5657
|
function toAgentError(message, details) {
|
|
5060
5658
|
return { category: "response_error", message, details };
|
|
5061
5659
|
}
|
|
5062
5660
|
var BLOCKED_FLAGS = ["--privileged", "--cap-add", "--security-opt", "--pid", "--ipc", "--userns"];
|
|
5063
|
-
function
|
|
5661
|
+
function sanitizeExtraArgs2(extraArgs) {
|
|
5064
5662
|
if (!extraArgs) return [];
|
|
5065
5663
|
return extraArgs.filter((arg) => !BLOCKED_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5066
5664
|
}
|
|
@@ -5086,7 +5684,7 @@ var ContainerBackend = class {
|
|
|
5086
5684
|
}
|
|
5087
5685
|
const result = await this.secretBackend.resolveSecrets(this.secretKeys);
|
|
5088
5686
|
if (!result.ok) {
|
|
5089
|
-
return (0,
|
|
5687
|
+
return (0, import_types18.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
|
|
5090
5688
|
}
|
|
5091
5689
|
return { ok: true, value: result.value };
|
|
5092
5690
|
}
|
|
@@ -5099,7 +5697,7 @@ var ContainerBackend = class {
|
|
|
5099
5697
|
network: this.containerConfig.network ?? "none",
|
|
5100
5698
|
env
|
|
5101
5699
|
};
|
|
5102
|
-
const sanitized =
|
|
5700
|
+
const sanitized = sanitizeExtraArgs2(this.containerConfig.extraArgs);
|
|
5103
5701
|
if (sanitized.length > 0) {
|
|
5104
5702
|
opts.extraArgs = sanitized;
|
|
5105
5703
|
}
|
|
@@ -5111,7 +5709,7 @@ var ContainerBackend = class {
|
|
|
5111
5709
|
const createOpts = this.buildCreateOpts(params, envResult.value);
|
|
5112
5710
|
const containerResult = await this.runtime.createContainer(createOpts);
|
|
5113
5711
|
if (!containerResult.ok) {
|
|
5114
|
-
return (0,
|
|
5712
|
+
return (0, import_types18.Err)(
|
|
5115
5713
|
toAgentError(
|
|
5116
5714
|
`Container creation failed: ${containerResult.error.message}`,
|
|
5117
5715
|
containerResult.error
|
|
@@ -5136,7 +5734,7 @@ var ContainerBackend = class {
|
|
|
5136
5734
|
this.containerHandles.delete(session.sessionId);
|
|
5137
5735
|
const removeResult = await this.runtime.removeContainer(handle);
|
|
5138
5736
|
if (!removeResult.ok) {
|
|
5139
|
-
return (0,
|
|
5737
|
+
return (0, import_types18.Err)(
|
|
5140
5738
|
toAgentError(
|
|
5141
5739
|
`Container removal failed: ${removeResult.error.message}`,
|
|
5142
5740
|
removeResult.error
|
|
@@ -5149,7 +5747,7 @@ var ContainerBackend = class {
|
|
|
5149
5747
|
async healthCheck() {
|
|
5150
5748
|
const runtimeResult = await this.runtime.healthCheck();
|
|
5151
5749
|
if (!runtimeResult.ok) {
|
|
5152
|
-
return (0,
|
|
5750
|
+
return (0, import_types18.Err)({
|
|
5153
5751
|
category: "agent_not_found",
|
|
5154
5752
|
message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
|
|
5155
5753
|
details: runtimeResult.error
|
|
@@ -5160,16 +5758,16 @@ var ContainerBackend = class {
|
|
|
5160
5758
|
};
|
|
5161
5759
|
|
|
5162
5760
|
// src/agent/runtime/docker.ts
|
|
5163
|
-
var
|
|
5164
|
-
var
|
|
5761
|
+
var import_node_child_process7 = require("child_process");
|
|
5762
|
+
var import_types19 = require("@harness-engineering/types");
|
|
5165
5763
|
function dockerExec(args) {
|
|
5166
|
-
return new Promise((
|
|
5167
|
-
(0,
|
|
5764
|
+
return new Promise((resolve7, reject) => {
|
|
5765
|
+
(0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
|
|
5168
5766
|
if (error) {
|
|
5169
5767
|
reject(error);
|
|
5170
5768
|
return;
|
|
5171
5769
|
}
|
|
5172
|
-
|
|
5770
|
+
resolve7(stdout.trim());
|
|
5173
5771
|
});
|
|
5174
5772
|
});
|
|
5175
5773
|
}
|
|
@@ -5194,9 +5792,9 @@ var DockerRuntime = class {
|
|
|
5194
5792
|
args.push(opts.image);
|
|
5195
5793
|
args.push("sleep", "infinity");
|
|
5196
5794
|
const containerId = await dockerExec(args);
|
|
5197
|
-
return (0,
|
|
5795
|
+
return (0, import_types19.Ok)({ containerId, runtime: this.name });
|
|
5198
5796
|
} catch (error) {
|
|
5199
|
-
return (0,
|
|
5797
|
+
return (0, import_types19.Err)({
|
|
5200
5798
|
category: "container_create_failed",
|
|
5201
5799
|
message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5202
5800
|
details: error
|
|
@@ -5218,7 +5816,7 @@ var DockerRuntime = class {
|
|
|
5218
5816
|
}
|
|
5219
5817
|
}
|
|
5220
5818
|
execArgs.push(handle.containerId, ...cmd);
|
|
5221
|
-
const child = (0,
|
|
5819
|
+
const child = (0, import_node_child_process7.spawn)("docker", execArgs);
|
|
5222
5820
|
const readline3 = await import("readline");
|
|
5223
5821
|
const rl = readline3.createInterface({ input: child.stdout, terminal: false });
|
|
5224
5822
|
try {
|
|
@@ -5228,11 +5826,11 @@ var DockerRuntime = class {
|
|
|
5228
5826
|
} finally {
|
|
5229
5827
|
rl.close();
|
|
5230
5828
|
}
|
|
5231
|
-
const exitCode = await new Promise((
|
|
5829
|
+
const exitCode = await new Promise((resolve7) => {
|
|
5232
5830
|
if (child.exitCode !== null) {
|
|
5233
|
-
|
|
5831
|
+
resolve7(child.exitCode);
|
|
5234
5832
|
} else {
|
|
5235
|
-
child.on("exit", (code) =>
|
|
5833
|
+
child.on("exit", (code) => resolve7(code ?? 1));
|
|
5236
5834
|
}
|
|
5237
5835
|
});
|
|
5238
5836
|
return exitCode;
|
|
@@ -5240,9 +5838,9 @@ var DockerRuntime = class {
|
|
|
5240
5838
|
async removeContainer(handle) {
|
|
5241
5839
|
try {
|
|
5242
5840
|
await dockerExec(["rm", "-f", handle.containerId]);
|
|
5243
|
-
return (0,
|
|
5841
|
+
return (0, import_types19.Ok)(void 0);
|
|
5244
5842
|
} catch (error) {
|
|
5245
|
-
return (0,
|
|
5843
|
+
return (0, import_types19.Err)({
|
|
5246
5844
|
category: "container_remove_failed",
|
|
5247
5845
|
message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5248
5846
|
details: error
|
|
@@ -5252,9 +5850,9 @@ var DockerRuntime = class {
|
|
|
5252
5850
|
async healthCheck() {
|
|
5253
5851
|
try {
|
|
5254
5852
|
await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
|
|
5255
|
-
return (0,
|
|
5853
|
+
return (0, import_types19.Ok)(void 0);
|
|
5256
5854
|
} catch (error) {
|
|
5257
|
-
return (0,
|
|
5855
|
+
return (0, import_types19.Err)({
|
|
5258
5856
|
category: "runtime_not_found",
|
|
5259
5857
|
message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
5260
5858
|
details: error
|
|
@@ -5264,7 +5862,7 @@ var DockerRuntime = class {
|
|
|
5264
5862
|
};
|
|
5265
5863
|
|
|
5266
5864
|
// src/agent/secrets/env.ts
|
|
5267
|
-
var
|
|
5865
|
+
var import_types20 = require("@harness-engineering/types");
|
|
5268
5866
|
var EnvSecretBackend = class {
|
|
5269
5867
|
name = "env";
|
|
5270
5868
|
async resolveSecrets(keys) {
|
|
@@ -5272,7 +5870,7 @@ var EnvSecretBackend = class {
|
|
|
5272
5870
|
for (const key of keys) {
|
|
5273
5871
|
const value = process.env[key];
|
|
5274
5872
|
if (value === void 0) {
|
|
5275
|
-
return (0,
|
|
5873
|
+
return (0, import_types20.Err)({
|
|
5276
5874
|
category: "secret_not_found",
|
|
5277
5875
|
message: `Environment variable '${key}' is not set`,
|
|
5278
5876
|
key
|
|
@@ -5280,24 +5878,24 @@ var EnvSecretBackend = class {
|
|
|
5280
5878
|
}
|
|
5281
5879
|
secrets[key] = value;
|
|
5282
5880
|
}
|
|
5283
|
-
return (0,
|
|
5881
|
+
return (0, import_types20.Ok)(secrets);
|
|
5284
5882
|
}
|
|
5285
5883
|
async healthCheck() {
|
|
5286
|
-
return (0,
|
|
5884
|
+
return (0, import_types20.Ok)(void 0);
|
|
5287
5885
|
}
|
|
5288
5886
|
};
|
|
5289
5887
|
|
|
5290
5888
|
// src/agent/secrets/onepassword.ts
|
|
5291
|
-
var
|
|
5292
|
-
var
|
|
5889
|
+
var import_node_child_process8 = require("child_process");
|
|
5890
|
+
var import_types21 = require("@harness-engineering/types");
|
|
5293
5891
|
function opExec(args) {
|
|
5294
|
-
return new Promise((
|
|
5295
|
-
(0,
|
|
5892
|
+
return new Promise((resolve7, reject) => {
|
|
5893
|
+
(0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
|
|
5296
5894
|
if (error) {
|
|
5297
5895
|
reject(error);
|
|
5298
5896
|
return;
|
|
5299
5897
|
}
|
|
5300
|
-
|
|
5898
|
+
resolve7(stdout.trim());
|
|
5301
5899
|
});
|
|
5302
5900
|
});
|
|
5303
5901
|
}
|
|
@@ -5314,21 +5912,21 @@ var OnePasswordSecretBackend = class {
|
|
|
5314
5912
|
const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
|
|
5315
5913
|
secrets[key] = value;
|
|
5316
5914
|
} catch (error) {
|
|
5317
|
-
return (0,
|
|
5915
|
+
return (0, import_types21.Err)({
|
|
5318
5916
|
category: "access_denied",
|
|
5319
5917
|
message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
|
|
5320
5918
|
key
|
|
5321
5919
|
});
|
|
5322
5920
|
}
|
|
5323
5921
|
}
|
|
5324
|
-
return (0,
|
|
5922
|
+
return (0, import_types21.Ok)(secrets);
|
|
5325
5923
|
}
|
|
5326
5924
|
async healthCheck() {
|
|
5327
5925
|
try {
|
|
5328
5926
|
await opExec(["--version"]);
|
|
5329
|
-
return (0,
|
|
5927
|
+
return (0, import_types21.Ok)(void 0);
|
|
5330
5928
|
} catch (error) {
|
|
5331
|
-
return (0,
|
|
5929
|
+
return (0, import_types21.Err)({
|
|
5332
5930
|
category: "provider_unavailable",
|
|
5333
5931
|
message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5334
5932
|
});
|
|
@@ -5337,16 +5935,16 @@ var OnePasswordSecretBackend = class {
|
|
|
5337
5935
|
};
|
|
5338
5936
|
|
|
5339
5937
|
// src/agent/secrets/vault.ts
|
|
5340
|
-
var
|
|
5341
|
-
var
|
|
5938
|
+
var import_node_child_process9 = require("child_process");
|
|
5939
|
+
var import_types22 = require("@harness-engineering/types");
|
|
5342
5940
|
function vaultExec(args, env) {
|
|
5343
|
-
return new Promise((
|
|
5344
|
-
(0,
|
|
5941
|
+
return new Promise((resolve7, reject) => {
|
|
5942
|
+
(0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5345
5943
|
if (error) {
|
|
5346
5944
|
reject(error);
|
|
5347
5945
|
return;
|
|
5348
5946
|
}
|
|
5349
|
-
|
|
5947
|
+
resolve7(stdout.trim());
|
|
5350
5948
|
});
|
|
5351
5949
|
});
|
|
5352
5950
|
}
|
|
@@ -5369,11 +5967,11 @@ var VaultSecretBackend = class {
|
|
|
5369
5967
|
} catch (error) {
|
|
5370
5968
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5371
5969
|
const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
|
|
5372
|
-
return (0,
|
|
5970
|
+
return (0, import_types22.Err)({ category, message: `Failed to read from Vault: ${msg}` });
|
|
5373
5971
|
}
|
|
5374
5972
|
const missing = keys.find((k) => !(k in data));
|
|
5375
5973
|
if (missing) {
|
|
5376
|
-
return (0,
|
|
5974
|
+
return (0, import_types22.Err)({
|
|
5377
5975
|
category: "secret_not_found",
|
|
5378
5976
|
message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
|
|
5379
5977
|
key: missing
|
|
@@ -5381,14 +5979,14 @@ var VaultSecretBackend = class {
|
|
|
5381
5979
|
}
|
|
5382
5980
|
const secrets = {};
|
|
5383
5981
|
for (const key of keys) secrets[key] = data[key];
|
|
5384
|
-
return (0,
|
|
5982
|
+
return (0, import_types22.Ok)(secrets);
|
|
5385
5983
|
}
|
|
5386
5984
|
async healthCheck() {
|
|
5387
5985
|
try {
|
|
5388
5986
|
await vaultExec(["version"]);
|
|
5389
|
-
return (0,
|
|
5987
|
+
return (0, import_types22.Ok)(void 0);
|
|
5390
5988
|
} catch (error) {
|
|
5391
|
-
return (0,
|
|
5989
|
+
return (0, import_types22.Err)({
|
|
5392
5990
|
category: "provider_unavailable",
|
|
5393
5991
|
message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5394
5992
|
});
|
|
@@ -5525,6 +6123,8 @@ function buildAnalysisProvider(args) {
|
|
|
5525
6123
|
return buildClaudeCliProvider(def, args, layerModel);
|
|
5526
6124
|
case "mock":
|
|
5527
6125
|
case "gemini":
|
|
6126
|
+
case "ssh":
|
|
6127
|
+
case "serverless":
|
|
5528
6128
|
logger.warn(
|
|
5529
6129
|
`Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
|
|
5530
6130
|
);
|
|
@@ -5707,8 +6307,8 @@ function buildExplicitProvider(provider, selModel, config) {
|
|
|
5707
6307
|
|
|
5708
6308
|
// src/server/http.ts
|
|
5709
6309
|
var http = __toESM(require("http"));
|
|
5710
|
-
var
|
|
5711
|
-
var
|
|
6310
|
+
var path15 = __toESM(require("path"));
|
|
6311
|
+
var import_core11 = require("@harness-engineering/core");
|
|
5712
6312
|
|
|
5713
6313
|
// src/server/websocket.ts
|
|
5714
6314
|
var import_ws = require("ws");
|
|
@@ -5770,7 +6370,7 @@ var import_zod3 = require("zod");
|
|
|
5770
6370
|
// src/server/utils.ts
|
|
5771
6371
|
var DEFAULT_MAX_BYTES = 1048576;
|
|
5772
6372
|
function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
5773
|
-
return new Promise((
|
|
6373
|
+
return new Promise((resolve7, reject) => {
|
|
5774
6374
|
let body = "";
|
|
5775
6375
|
let bytes = 0;
|
|
5776
6376
|
req.on("data", (chunk) => {
|
|
@@ -5782,7 +6382,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
5782
6382
|
}
|
|
5783
6383
|
body += String(chunk);
|
|
5784
6384
|
});
|
|
5785
|
-
req.on("end", () =>
|
|
6385
|
+
req.on("end", () => resolve7(body));
|
|
5786
6386
|
req.on("error", reject);
|
|
5787
6387
|
});
|
|
5788
6388
|
}
|
|
@@ -5948,7 +6548,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
5948
6548
|
}
|
|
5949
6549
|
|
|
5950
6550
|
// src/server/routes/chat-proxy.ts
|
|
5951
|
-
var
|
|
6551
|
+
var import_node_child_process10 = require("child_process");
|
|
5952
6552
|
var import_node_crypto5 = require("crypto");
|
|
5953
6553
|
var readline2 = __toESM(require("readline"));
|
|
5954
6554
|
var import_zod6 = require("zod");
|
|
@@ -6034,7 +6634,7 @@ async function handleChatRequest(req, res, command) {
|
|
|
6034
6634
|
});
|
|
6035
6635
|
emit(res, { type: "session", sessionId });
|
|
6036
6636
|
const args = buildArgs(parsed.prompt, sessionId, isFirstTurn, parsed.system);
|
|
6037
|
-
child = (0,
|
|
6637
|
+
child = (0, import_node_child_process10.spawn)(command, args, { env: buildChildEnv(), stdio: "pipe" });
|
|
6038
6638
|
child.stdin?.end();
|
|
6039
6639
|
let clientDisconnected = false;
|
|
6040
6640
|
res.on("close", () => {
|
|
@@ -6762,7 +7362,7 @@ function isPrivateHost(hostname) {
|
|
|
6762
7362
|
}
|
|
6763
7363
|
|
|
6764
7364
|
// src/server/routes/v1/webhooks.ts
|
|
6765
|
-
var
|
|
7365
|
+
var import_types23 = require("@harness-engineering/types");
|
|
6766
7366
|
function isAdminAuth(authContext) {
|
|
6767
7367
|
if (!authContext) return false;
|
|
6768
7368
|
if (authContext.scopes.includes("admin")) return true;
|
|
@@ -6809,7 +7409,7 @@ function handleV1WebhooksRoute(req, res, deps) {
|
|
|
6809
7409
|
const subs = await deps.store.list();
|
|
6810
7410
|
const authContext = getAuthContext(req);
|
|
6811
7411
|
const visible = isAdminAuth(authContext) ? subs : subs.filter((s) => s.tokenId === authContext?.id);
|
|
6812
|
-
const publicView = visible.map((s) =>
|
|
7412
|
+
const publicView = visible.map((s) => import_types23.WebhookSubscriptionPublicSchema.parse(s));
|
|
6813
7413
|
sendJSON6(res, 200, publicView);
|
|
6814
7414
|
})();
|
|
6815
7415
|
return true;
|
|
@@ -6910,35 +7510,561 @@ function handleV1TelemetryRoute(req, res, deps) {
|
|
|
6910
7510
|
return false;
|
|
6911
7511
|
}
|
|
6912
7512
|
|
|
6913
|
-
// src/server/routes/
|
|
6914
|
-
var fs11 = __toESM(require("fs/promises"));
|
|
6915
|
-
var path11 = __toESM(require("path"));
|
|
7513
|
+
// src/server/routes/v1/proposals.ts
|
|
6916
7514
|
var import_zod13 = require("zod");
|
|
6917
|
-
var
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
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)
|
|
8045
|
+
}).passthrough();
|
|
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;
|
|
8047
|
+
function isSafeId(id) {
|
|
8048
|
+
return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
|
|
8049
|
+
}
|
|
6924
8050
|
function jsonResponse(res, status, data) {
|
|
6925
8051
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
6926
8052
|
res.end(JSON.stringify(data));
|
|
6927
8053
|
}
|
|
6928
8054
|
function extractSessionId(url) {
|
|
6929
|
-
const segments = new URL(url, "http://localhost").pathname.split(
|
|
8055
|
+
const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
|
|
6930
8056
|
const id = segments.pop();
|
|
6931
8057
|
return id && id !== "sessions" ? id : null;
|
|
6932
8058
|
}
|
|
6933
|
-
async function
|
|
8059
|
+
async function handleList2(res, sessionsDir) {
|
|
6934
8060
|
try {
|
|
6935
|
-
const entries = await
|
|
8061
|
+
const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
|
|
6936
8062
|
const sessions = [];
|
|
6937
8063
|
for (const entry of entries) {
|
|
6938
8064
|
if (!entry.isDirectory()) continue;
|
|
6939
8065
|
try {
|
|
6940
|
-
const content = await
|
|
6941
|
-
|
|
8066
|
+
const content = await fs12.readFile(
|
|
8067
|
+
path12.join(sessionsDir, entry.name, "session.json"),
|
|
6942
8068
|
"utf-8"
|
|
6943
8069
|
);
|
|
6944
8070
|
sessions.push(JSON.parse(content));
|
|
@@ -6957,13 +8083,13 @@ async function handleList(res, sessionsDir) {
|
|
|
6957
8083
|
jsonResponse(res, 500, { error: "Failed to list sessions" });
|
|
6958
8084
|
}
|
|
6959
8085
|
}
|
|
6960
|
-
async function
|
|
8086
|
+
async function handleGet2(res, id, sessionsDir) {
|
|
6961
8087
|
if (!isSafeId(id)) {
|
|
6962
8088
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6963
8089
|
return;
|
|
6964
8090
|
}
|
|
6965
8091
|
try {
|
|
6966
|
-
const content = await
|
|
8092
|
+
const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
|
|
6967
8093
|
jsonResponse(res, 200, JSON.parse(content));
|
|
6968
8094
|
} catch (err) {
|
|
6969
8095
|
if (err.code === "ENOENT") {
|
|
@@ -6986,9 +8112,9 @@ async function handleCreate(req, res, sessionsDir) {
|
|
|
6986
8112
|
jsonResponse(res, 400, { error: "Invalid sessionId" });
|
|
6987
8113
|
return;
|
|
6988
8114
|
}
|
|
6989
|
-
const sessionDir =
|
|
6990
|
-
await
|
|
6991
|
-
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));
|
|
6992
8118
|
jsonResponse(res, 200, { ok: true });
|
|
6993
8119
|
} catch {
|
|
6994
8120
|
jsonResponse(res, 500, { error: "Failed to save session" });
|
|
@@ -7002,10 +8128,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
|
|
|
7002
8128
|
return;
|
|
7003
8129
|
}
|
|
7004
8130
|
const body = await readBody(req);
|
|
7005
|
-
const updates =
|
|
7006
|
-
const sessionFilePath =
|
|
7007
|
-
const current = JSON.parse(await
|
|
7008
|
-
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));
|
|
7009
8135
|
jsonResponse(res, 200, { ok: true });
|
|
7010
8136
|
} catch {
|
|
7011
8137
|
jsonResponse(res, 500, { error: "Failed to update session" });
|
|
@@ -7018,7 +8144,7 @@ async function handleDelete(res, url, sessionsDir) {
|
|
|
7018
8144
|
jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
|
|
7019
8145
|
return;
|
|
7020
8146
|
}
|
|
7021
|
-
await
|
|
8147
|
+
await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
|
|
7022
8148
|
jsonResponse(res, 200, { ok: true });
|
|
7023
8149
|
} catch {
|
|
7024
8150
|
jsonResponse(res, 500, { error: "Failed to delete session" });
|
|
@@ -7031,8 +8157,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
|
|
|
7031
8157
|
switch (method) {
|
|
7032
8158
|
case "GET": {
|
|
7033
8159
|
const id = extractSessionId(url);
|
|
7034
|
-
if (id) void
|
|
7035
|
-
else void
|
|
8160
|
+
if (id) void handleGet2(res, id, sessionsDir);
|
|
8161
|
+
else void handleList2(res, sessionsDir);
|
|
7036
8162
|
return true;
|
|
7037
8163
|
}
|
|
7038
8164
|
case "POST":
|
|
@@ -7122,16 +8248,16 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7122
8248
|
}
|
|
7123
8249
|
|
|
7124
8250
|
// src/server/routes/auth.ts
|
|
7125
|
-
var
|
|
7126
|
-
var
|
|
7127
|
-
var CreateBodySchema =
|
|
7128
|
-
name:
|
|
7129
|
-
scopes:
|
|
7130
|
-
bridgeKind:
|
|
7131
|
-
tenantId:
|
|
7132
|
-
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()
|
|
7133
8259
|
});
|
|
7134
|
-
function
|
|
8260
|
+
function sendJSON9(res, status, body) {
|
|
7135
8261
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7136
8262
|
res.end(JSON.stringify(body));
|
|
7137
8263
|
}
|
|
@@ -7141,19 +8267,19 @@ async function handlePost(req, res, store) {
|
|
|
7141
8267
|
raw = await readBody(req);
|
|
7142
8268
|
} catch (err) {
|
|
7143
8269
|
const msg = err instanceof Error ? err.message : "Failed to read body";
|
|
7144
|
-
|
|
8270
|
+
sendJSON9(res, 413, { error: msg });
|
|
7145
8271
|
return;
|
|
7146
8272
|
}
|
|
7147
8273
|
let json;
|
|
7148
8274
|
try {
|
|
7149
8275
|
json = JSON.parse(raw);
|
|
7150
8276
|
} catch {
|
|
7151
|
-
|
|
8277
|
+
sendJSON9(res, 400, { error: "Invalid JSON body" });
|
|
7152
8278
|
return;
|
|
7153
8279
|
}
|
|
7154
8280
|
const parsed = CreateBodySchema.safeParse(json);
|
|
7155
8281
|
if (!parsed.success) {
|
|
7156
|
-
|
|
8282
|
+
sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
|
|
7157
8283
|
return;
|
|
7158
8284
|
}
|
|
7159
8285
|
try {
|
|
@@ -7165,38 +8291,38 @@ async function handlePost(req, res, store) {
|
|
|
7165
8291
|
if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
|
|
7166
8292
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7167
8293
|
const result = await store.create(input);
|
|
7168
|
-
const publicRecord =
|
|
7169
|
-
|
|
8294
|
+
const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
|
|
8295
|
+
sendJSON9(res, 200, {
|
|
7170
8296
|
...publicRecord,
|
|
7171
8297
|
token: result.token
|
|
7172
8298
|
});
|
|
7173
8299
|
} catch (err) {
|
|
7174
8300
|
const msg = err instanceof Error ? err.message : "Failed to create token";
|
|
7175
8301
|
if (msg.includes("already exists")) {
|
|
7176
|
-
|
|
8302
|
+
sendJSON9(res, 409, { error: msg });
|
|
7177
8303
|
return;
|
|
7178
8304
|
}
|
|
7179
|
-
|
|
8305
|
+
sendJSON9(res, 500, { error: "Internal error creating token" });
|
|
7180
8306
|
}
|
|
7181
8307
|
}
|
|
7182
|
-
async function
|
|
8308
|
+
async function handleList3(res, store) {
|
|
7183
8309
|
try {
|
|
7184
8310
|
const list = await store.list();
|
|
7185
|
-
|
|
8311
|
+
sendJSON9(res, 200, list);
|
|
7186
8312
|
} catch {
|
|
7187
|
-
|
|
8313
|
+
sendJSON9(res, 500, { error: "Internal error listing tokens" });
|
|
7188
8314
|
}
|
|
7189
8315
|
}
|
|
7190
8316
|
async function handleDelete2(res, store, id) {
|
|
7191
8317
|
try {
|
|
7192
8318
|
const ok = await store.revoke(id);
|
|
7193
8319
|
if (!ok) {
|
|
7194
|
-
|
|
8320
|
+
sendJSON9(res, 404, { error: "Token not found" });
|
|
7195
8321
|
return;
|
|
7196
8322
|
}
|
|
7197
|
-
|
|
8323
|
+
sendJSON9(res, 200, { deleted: true });
|
|
7198
8324
|
} catch {
|
|
7199
|
-
|
|
8325
|
+
sendJSON9(res, 500, { error: "Internal error revoking token" });
|
|
7200
8326
|
}
|
|
7201
8327
|
}
|
|
7202
8328
|
var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
|
|
@@ -7210,7 +8336,7 @@ function handleAuthRoute(req, res, store) {
|
|
|
7210
8336
|
return true;
|
|
7211
8337
|
}
|
|
7212
8338
|
if (method === "GET" && pathname === "/api/v1/auth/tokens") {
|
|
7213
|
-
void
|
|
8339
|
+
void handleList3(res, store);
|
|
7214
8340
|
return true;
|
|
7215
8341
|
}
|
|
7216
8342
|
if (method === "DELETE") {
|
|
@@ -7221,12 +8347,12 @@ function handleAuthRoute(req, res, store) {
|
|
|
7221
8347
|
return true;
|
|
7222
8348
|
}
|
|
7223
8349
|
}
|
|
7224
|
-
|
|
8350
|
+
sendJSON9(res, 405, { error: "Method not allowed" });
|
|
7225
8351
|
return true;
|
|
7226
8352
|
}
|
|
7227
8353
|
|
|
7228
8354
|
// src/server/routes/local-model.ts
|
|
7229
|
-
function
|
|
8355
|
+
function sendJSON10(res, status, body) {
|
|
7230
8356
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
7231
8357
|
res.end(JSON.stringify(body));
|
|
7232
8358
|
}
|
|
@@ -7234,36 +8360,36 @@ function handleLocalModelRoute(req, res, getStatus) {
|
|
|
7234
8360
|
const { method, url } = req;
|
|
7235
8361
|
if (url !== "/api/v1/local-model/status") return false;
|
|
7236
8362
|
if (method !== "GET") {
|
|
7237
|
-
|
|
8363
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7238
8364
|
return true;
|
|
7239
8365
|
}
|
|
7240
8366
|
if (!getStatus) {
|
|
7241
|
-
|
|
8367
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7242
8368
|
return true;
|
|
7243
8369
|
}
|
|
7244
8370
|
const status = getStatus();
|
|
7245
8371
|
if (!status) {
|
|
7246
|
-
|
|
8372
|
+
sendJSON10(res, 503, { error: "Local backend not configured" });
|
|
7247
8373
|
return true;
|
|
7248
8374
|
}
|
|
7249
|
-
|
|
8375
|
+
sendJSON10(res, 200, status);
|
|
7250
8376
|
return true;
|
|
7251
8377
|
}
|
|
7252
8378
|
function handleLocalModelsRoute(req, res, getStatuses) {
|
|
7253
8379
|
const { method, url } = req;
|
|
7254
8380
|
if (url !== "/api/v1/local-models/status") return false;
|
|
7255
8381
|
if (method !== "GET") {
|
|
7256
|
-
|
|
8382
|
+
sendJSON10(res, 405, { error: "Method not allowed" });
|
|
7257
8383
|
return true;
|
|
7258
8384
|
}
|
|
7259
8385
|
const statuses = getStatuses ? getStatuses() : [];
|
|
7260
|
-
|
|
8386
|
+
sendJSON10(res, 200, statuses);
|
|
7261
8387
|
return true;
|
|
7262
8388
|
}
|
|
7263
8389
|
|
|
7264
8390
|
// src/server/static.ts
|
|
7265
|
-
var
|
|
7266
|
-
var
|
|
8391
|
+
var fs13 = __toESM(require("fs"));
|
|
8392
|
+
var path13 = __toESM(require("path"));
|
|
7267
8393
|
var MIME_TYPES = {
|
|
7268
8394
|
".html": "text/html; charset=utf-8",
|
|
7269
8395
|
".js": "application/javascript; charset=utf-8",
|
|
@@ -7283,29 +8409,29 @@ var MIME_TYPES = {
|
|
|
7283
8409
|
function handleStaticFile(req, res, dashboardDir) {
|
|
7284
8410
|
const { method, url } = req;
|
|
7285
8411
|
if (method !== "GET") return false;
|
|
7286
|
-
const apiPrefix =
|
|
7287
|
-
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");
|
|
7288
8414
|
if (url?.startsWith(apiPrefix) || url === wsPath) return false;
|
|
7289
8415
|
const urlPath = new URL(url ?? "/", "http://localhost").pathname;
|
|
7290
|
-
const requestedPath =
|
|
7291
|
-
const resolved =
|
|
7292
|
-
if (!resolved.startsWith(
|
|
7293
|
-
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);
|
|
7294
8420
|
}
|
|
7295
|
-
if (
|
|
8421
|
+
if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
|
|
7296
8422
|
return serveFile(resolved, res);
|
|
7297
8423
|
}
|
|
7298
|
-
const indexPath =
|
|
7299
|
-
if (
|
|
8424
|
+
const indexPath = path13.join(dashboardDir, "index.html");
|
|
8425
|
+
if (fs13.existsSync(indexPath)) {
|
|
7300
8426
|
return serveFile(indexPath, res);
|
|
7301
8427
|
}
|
|
7302
8428
|
return false;
|
|
7303
8429
|
}
|
|
7304
8430
|
function serveFile(filePath, res) {
|
|
7305
|
-
const ext =
|
|
8431
|
+
const ext = path13.extname(filePath).toLowerCase();
|
|
7306
8432
|
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
7307
8433
|
try {
|
|
7308
|
-
const content =
|
|
8434
|
+
const content = fs13.readFileSync(filePath);
|
|
7309
8435
|
res.writeHead(200, { "Content-Type": contentType });
|
|
7310
8436
|
res.end(content);
|
|
7311
8437
|
return true;
|
|
@@ -7315,8 +8441,8 @@ function serveFile(filePath, res) {
|
|
|
7315
8441
|
}
|
|
7316
8442
|
|
|
7317
8443
|
// src/server/plan-watcher.ts
|
|
7318
|
-
var
|
|
7319
|
-
var
|
|
8444
|
+
var fs14 = __toESM(require("fs"));
|
|
8445
|
+
var path14 = __toESM(require("path"));
|
|
7320
8446
|
var PlanWatcher = class {
|
|
7321
8447
|
plansDir;
|
|
7322
8448
|
queue;
|
|
@@ -7330,11 +8456,11 @@ var PlanWatcher = class {
|
|
|
7330
8456
|
* Creates the directory if it does not exist.
|
|
7331
8457
|
*/
|
|
7332
8458
|
start() {
|
|
7333
|
-
|
|
7334
|
-
this.watcher =
|
|
8459
|
+
fs14.mkdirSync(this.plansDir, { recursive: true });
|
|
8460
|
+
this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
|
|
7335
8461
|
if (eventType === "rename" && filename && filename.endsWith(".md")) {
|
|
7336
|
-
const filePath =
|
|
7337
|
-
if (
|
|
8462
|
+
const filePath = path14.join(this.plansDir, filename);
|
|
8463
|
+
if (fs14.existsSync(filePath)) {
|
|
7338
8464
|
void this.handleNewPlan(filename);
|
|
7339
8465
|
}
|
|
7340
8466
|
}
|
|
@@ -7369,7 +8495,7 @@ var import_node_crypto9 = require("crypto");
|
|
|
7369
8495
|
var import_promises = require("fs/promises");
|
|
7370
8496
|
var import_node_path = require("path");
|
|
7371
8497
|
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
7372
|
-
var
|
|
8498
|
+
var import_types26 = require("@harness-engineering/types");
|
|
7373
8499
|
var BCRYPT_ROUNDS = 12;
|
|
7374
8500
|
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
7375
8501
|
function genId() {
|
|
@@ -7384,8 +8510,8 @@ function parseToken(raw) {
|
|
|
7384
8510
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7385
8511
|
}
|
|
7386
8512
|
var TokenStore = class {
|
|
7387
|
-
constructor(
|
|
7388
|
-
this.path =
|
|
8513
|
+
constructor(path22) {
|
|
8514
|
+
this.path = path22;
|
|
7389
8515
|
}
|
|
7390
8516
|
path;
|
|
7391
8517
|
cache = null;
|
|
@@ -7396,7 +8522,7 @@ var TokenStore = class {
|
|
|
7396
8522
|
const parsed = JSON.parse(raw);
|
|
7397
8523
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
7398
8524
|
this.cache = list.map((entry) => {
|
|
7399
|
-
const r =
|
|
8525
|
+
const r = import_types26.AuthTokenSchema.safeParse(entry);
|
|
7400
8526
|
return r.success ? r.data : null;
|
|
7401
8527
|
}).filter((x) => x !== null);
|
|
7402
8528
|
} catch (err) {
|
|
@@ -7458,7 +8584,7 @@ var TokenStore = class {
|
|
|
7458
8584
|
}
|
|
7459
8585
|
async list() {
|
|
7460
8586
|
const records = await this.load();
|
|
7461
|
-
return records.map((r) =>
|
|
8587
|
+
return records.map((r) => import_types26.AuthTokenPublicSchema.parse(r));
|
|
7462
8588
|
}
|
|
7463
8589
|
async revoke(id) {
|
|
7464
8590
|
const records = await this.load();
|
|
@@ -7490,10 +8616,10 @@ var TokenStore = class {
|
|
|
7490
8616
|
// src/auth/audit.ts
|
|
7491
8617
|
var import_promises2 = require("fs/promises");
|
|
7492
8618
|
var import_node_path2 = require("path");
|
|
7493
|
-
var
|
|
8619
|
+
var import_types27 = require("@harness-engineering/types");
|
|
7494
8620
|
var AuditLogger = class {
|
|
7495
|
-
constructor(
|
|
7496
|
-
this.path =
|
|
8621
|
+
constructor(path22, opts = {}) {
|
|
8622
|
+
this.path = path22;
|
|
7497
8623
|
this.opts = opts;
|
|
7498
8624
|
}
|
|
7499
8625
|
path;
|
|
@@ -7501,7 +8627,7 @@ var AuditLogger = class {
|
|
|
7501
8627
|
queue = Promise.resolve();
|
|
7502
8628
|
dirEnsured = false;
|
|
7503
8629
|
async append(input) {
|
|
7504
|
-
const entry =
|
|
8630
|
+
const entry = import_types27.AuthAuditEntrySchema.parse({
|
|
7505
8631
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7506
8632
|
tokenId: input.tokenId,
|
|
7507
8633
|
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
@@ -7577,6 +8703,43 @@ var V1_BRIDGE_ROUTES = [
|
|
|
7577
8703
|
scope: "subscribe-webhook",
|
|
7578
8704
|
description: "Webhook delivery queue depth + DLQ stats."
|
|
7579
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
|
+
},
|
|
7580
8743
|
// ── Phase 5 bridge primitives ──
|
|
7581
8744
|
{
|
|
7582
8745
|
method: "GET",
|
|
@@ -7588,9 +8751,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
7588
8751
|
function isV1Bridge(method, url) {
|
|
7589
8752
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
7590
8753
|
}
|
|
7591
|
-
function requiredBridgeScope(method,
|
|
8754
|
+
function requiredBridgeScope(method, path22) {
|
|
7592
8755
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
7593
|
-
if (r.method === method && r.pattern.test(
|
|
8756
|
+
if (r.method === method && r.pattern.test(path22)) return r.scope;
|
|
7594
8757
|
}
|
|
7595
8758
|
return null;
|
|
7596
8759
|
}
|
|
@@ -7600,24 +8763,24 @@ function hasScope(held, required) {
|
|
|
7600
8763
|
if (held.includes("admin")) return true;
|
|
7601
8764
|
return held.includes(required);
|
|
7602
8765
|
}
|
|
7603
|
-
function requiredScopeForRoute(method,
|
|
7604
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
8766
|
+
function requiredScopeForRoute(method, path22) {
|
|
8767
|
+
const bridgeScope = requiredBridgeScope(method, path22);
|
|
7605
8768
|
if (bridgeScope) return bridgeScope;
|
|
7606
|
-
if (
|
|
7607
|
-
if (
|
|
7608
|
-
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(
|
|
7609
|
-
if ((
|
|
7610
|
-
if (
|
|
7611
|
-
if (
|
|
7612
|
-
if (
|
|
7613
|
-
if (
|
|
7614
|
-
if (
|
|
7615
|
-
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"))
|
|
7616
8779
|
return "read-status";
|
|
7617
|
-
if (
|
|
7618
|
-
if (
|
|
7619
|
-
if (
|
|
7620
|
-
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";
|
|
7621
8784
|
return null;
|
|
7622
8785
|
}
|
|
7623
8786
|
|
|
@@ -7671,6 +8834,11 @@ var OrchestratorServer = class {
|
|
|
7671
8834
|
roadmapPath;
|
|
7672
8835
|
dispatchAdHoc;
|
|
7673
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;
|
|
7674
8842
|
maintenanceDeps = null;
|
|
7675
8843
|
getLocalModelStatus = null;
|
|
7676
8844
|
getLocalModelStatuses = null;
|
|
@@ -7688,8 +8856,8 @@ var OrchestratorServer = class {
|
|
|
7688
8856
|
this.orchestrator = orchestrator;
|
|
7689
8857
|
this.port = port;
|
|
7690
8858
|
this.initDependencies(deps);
|
|
7691
|
-
const tokensPath = process.env["HARNESS_TOKENS_PATH"] ??
|
|
7692
|
-
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");
|
|
7693
8861
|
this.tokenStore = new TokenStore(tokensPath);
|
|
7694
8862
|
this.auditLogger = new AuditLogger(auditPath);
|
|
7695
8863
|
this.httpServer = http.createServer(this.handleRequest.bind(this));
|
|
@@ -7702,14 +8870,15 @@ var OrchestratorServer = class {
|
|
|
7702
8870
|
}
|
|
7703
8871
|
initDependencies(deps) {
|
|
7704
8872
|
this.interactionQueue = deps?.interactionQueue;
|
|
7705
|
-
this.plansDir = deps?.plansDir ??
|
|
7706
|
-
this.dashboardDir = deps?.dashboardDir ??
|
|
8873
|
+
this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
|
|
8874
|
+
this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
|
|
7707
8875
|
this.claudeCommand = deps?.claudeCommand ?? "claude";
|
|
7708
8876
|
this.pipeline = deps?.pipeline ?? null;
|
|
7709
8877
|
this.analysisArchive = deps?.analysisArchive;
|
|
7710
8878
|
this.roadmapPath = deps?.roadmapPath ?? null;
|
|
7711
8879
|
this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
|
|
7712
|
-
this.sessionsDir = deps?.sessionsDir ??
|
|
8880
|
+
this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
|
|
8881
|
+
this.projectPath = deps?.projectPath ?? process.cwd();
|
|
7713
8882
|
this.maintenanceDeps = deps?.maintenanceDeps ?? null;
|
|
7714
8883
|
this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
|
|
7715
8884
|
this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
|
|
@@ -7880,6 +9049,15 @@ var OrchestratorServer = class {
|
|
|
7880
9049
|
(req, res) => handleV1TelemetryRoute(req, res, {
|
|
7881
9050
|
...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
|
|
7882
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
|
+
}),
|
|
7883
9061
|
// Chat proxy route (spawns Claude Code CLI — no API key required)
|
|
7884
9062
|
(req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
|
|
7885
9063
|
];
|
|
@@ -7962,16 +9140,16 @@ var OrchestratorServer = class {
|
|
|
7962
9140
|
return this.broadcaster.clientCount;
|
|
7963
9141
|
}
|
|
7964
9142
|
async start() {
|
|
7965
|
-
(0,
|
|
9143
|
+
(0, import_core11.assertPortUsable)(this.port, "orchestrator");
|
|
7966
9144
|
if (this.interactionQueue) {
|
|
7967
9145
|
this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
|
|
7968
9146
|
this.planWatcher.start();
|
|
7969
9147
|
}
|
|
7970
|
-
return new Promise((
|
|
9148
|
+
return new Promise((resolve7) => {
|
|
7971
9149
|
const host = getBindHost();
|
|
7972
9150
|
this.httpServer.listen(this.port, host, () => {
|
|
7973
9151
|
console.log(`Orchestrator API listening on ${host}:${this.port}`);
|
|
7974
|
-
|
|
9152
|
+
resolve7();
|
|
7975
9153
|
});
|
|
7976
9154
|
});
|
|
7977
9155
|
}
|
|
@@ -7991,7 +9169,7 @@ var OrchestratorServer = class {
|
|
|
7991
9169
|
var import_node_crypto11 = require("crypto");
|
|
7992
9170
|
var import_promises3 = require("fs/promises");
|
|
7993
9171
|
var import_node_path3 = require("path");
|
|
7994
|
-
var
|
|
9172
|
+
var import_types28 = require("@harness-engineering/types");
|
|
7995
9173
|
|
|
7996
9174
|
// src/gateway/webhooks/signer.ts
|
|
7997
9175
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8021,8 +9199,8 @@ function genSecret2() {
|
|
|
8021
9199
|
return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
|
|
8022
9200
|
}
|
|
8023
9201
|
var WebhookStore = class {
|
|
8024
|
-
constructor(
|
|
8025
|
-
this.path =
|
|
9202
|
+
constructor(path22) {
|
|
9203
|
+
this.path = path22;
|
|
8026
9204
|
}
|
|
8027
9205
|
path;
|
|
8028
9206
|
cache = null;
|
|
@@ -8033,7 +9211,7 @@ var WebhookStore = class {
|
|
|
8033
9211
|
const parsed = JSON.parse(raw);
|
|
8034
9212
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
8035
9213
|
this.cache = list.map((entry) => {
|
|
8036
|
-
const r =
|
|
9214
|
+
const r = import_types28.WebhookSubscriptionSchema.safeParse(entry);
|
|
8037
9215
|
return r.success ? r.data : null;
|
|
8038
9216
|
}).filter((x) => x !== null);
|
|
8039
9217
|
} catch (err) {
|
|
@@ -8413,7 +9591,12 @@ var WEBHOOK_TOPICS = [
|
|
|
8413
9591
|
"maintenance:completed",
|
|
8414
9592
|
"maintenance:error",
|
|
8415
9593
|
"webhook.subscription.created",
|
|
8416
|
-
"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"
|
|
8417
9600
|
];
|
|
8418
9601
|
function newEventId2() {
|
|
8419
9602
|
return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
|
|
@@ -8447,7 +9630,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
|
|
|
8447
9630
|
|
|
8448
9631
|
// src/gateway/telemetry/fanout.ts
|
|
8449
9632
|
var import_node_crypto14 = require("crypto");
|
|
8450
|
-
var
|
|
9633
|
+
var import_core12 = require("@harness-engineering/core");
|
|
8451
9634
|
var TOPICS = {
|
|
8452
9635
|
MAINTENANCE_STARTED: "maintenance:started",
|
|
8453
9636
|
MAINTENANCE_COMPLETED: "maintenance:completed",
|
|
@@ -8582,7 +9765,7 @@ function wireTelemetryFanout(params) {
|
|
|
8582
9765
|
spanId,
|
|
8583
9766
|
...parentSpanId !== void 0 ? { parentSpanId } : {},
|
|
8584
9767
|
name: SPAN_NAME[topic],
|
|
8585
|
-
kind:
|
|
9768
|
+
kind: import_core12.SpanKind.INTERNAL,
|
|
8586
9769
|
startTimeNs: startNs,
|
|
8587
9770
|
endTimeNs: startNs,
|
|
8588
9771
|
attributes: buildAttributes(payload, { "harness.topic": topic }),
|
|
@@ -8608,18 +9791,378 @@ function wireTelemetryFanout(params) {
|
|
|
8608
9791
|
};
|
|
8609
9792
|
}
|
|
8610
9793
|
|
|
8611
|
-
// src/
|
|
8612
|
-
var
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
9794
|
+
// src/notifications/slack-sink.ts
|
|
9795
|
+
var SEVERITY_PREFIX = {
|
|
9796
|
+
info: ":information_source:",
|
|
9797
|
+
success: ":white_check_mark:",
|
|
9798
|
+
warning: ":warning:",
|
|
9799
|
+
error: ":x:"
|
|
9800
|
+
};
|
|
9801
|
+
var SlackSink = class {
|
|
9802
|
+
kind = "slack";
|
|
9803
|
+
id;
|
|
9804
|
+
webhookUrl;
|
|
9805
|
+
fetchImpl;
|
|
9806
|
+
timeoutMs;
|
|
9807
|
+
constructor(opts) {
|
|
9808
|
+
this.id = opts.id;
|
|
9809
|
+
this.webhookUrl = opts.webhookUrl;
|
|
9810
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
9811
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
8618
9812
|
}
|
|
8619
|
-
|
|
8620
|
-
this.
|
|
9813
|
+
async deliver(input) {
|
|
9814
|
+
const body = input.wrapped ? this.renderEnvelope(input.payload) : this.renderRawEvent(input.payload);
|
|
9815
|
+
const ctrl = new AbortController();
|
|
9816
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
9817
|
+
try {
|
|
9818
|
+
const res = await this.fetchImpl(this.webhookUrl, {
|
|
9819
|
+
method: "POST",
|
|
9820
|
+
headers: { "Content-Type": "application/json" },
|
|
9821
|
+
body: JSON.stringify(body),
|
|
9822
|
+
signal: ctrl.signal
|
|
9823
|
+
});
|
|
9824
|
+
if (res.ok) {
|
|
9825
|
+
return { ok: true, deliveredAt: Date.now() };
|
|
9826
|
+
}
|
|
9827
|
+
return { ok: false, error: `HTTP ${res.status}`, httpStatus: res.status };
|
|
9828
|
+
} catch (err) {
|
|
9829
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9830
|
+
return { ok: false, error: ctrl.signal.aborted ? "timeout" : msg };
|
|
9831
|
+
} finally {
|
|
9832
|
+
clearTimeout(timer);
|
|
9833
|
+
}
|
|
8621
9834
|
}
|
|
8622
|
-
|
|
9835
|
+
renderEnvelope(env) {
|
|
9836
|
+
const prefix = SEVERITY_PREFIX[env.severity] ?? "";
|
|
9837
|
+
const headline = `${prefix} ${env.title}`.trim();
|
|
9838
|
+
const blocks = [
|
|
9839
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${headline}*
|
|
9840
|
+
${env.summary}` } }
|
|
9841
|
+
];
|
|
9842
|
+
if (env.actions && env.actions.length > 0) {
|
|
9843
|
+
blocks.push({
|
|
9844
|
+
type: "actions",
|
|
9845
|
+
elements: env.actions.map((a) => ({
|
|
9846
|
+
type: "button",
|
|
9847
|
+
text: { type: "plain_text", text: a.label },
|
|
9848
|
+
url: a.url
|
|
9849
|
+
}))
|
|
9850
|
+
});
|
|
9851
|
+
}
|
|
9852
|
+
if (env.permalink) {
|
|
9853
|
+
blocks.push({
|
|
9854
|
+
type: "section",
|
|
9855
|
+
text: { type: "mrkdwn", text: `<${env.permalink}|View details>` }
|
|
9856
|
+
});
|
|
9857
|
+
}
|
|
9858
|
+
return { text: headline, blocks };
|
|
9859
|
+
}
|
|
9860
|
+
renderRawEvent(event) {
|
|
9861
|
+
const dump = (() => {
|
|
9862
|
+
try {
|
|
9863
|
+
return JSON.stringify(event.data, null, 2);
|
|
9864
|
+
} catch {
|
|
9865
|
+
return String(event.data);
|
|
9866
|
+
}
|
|
9867
|
+
})();
|
|
9868
|
+
const text = `harness event: \`${event.type}\``;
|
|
9869
|
+
return {
|
|
9870
|
+
text,
|
|
9871
|
+
blocks: [
|
|
9872
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${text}*
|
|
9873
|
+
\`\`\`
|
|
9874
|
+
${dump}
|
|
9875
|
+
\`\`\`` } }
|
|
9876
|
+
]
|
|
9877
|
+
};
|
|
9878
|
+
}
|
|
9879
|
+
};
|
|
9880
|
+
|
|
9881
|
+
// src/notifications/registry.ts
|
|
9882
|
+
var SinkConfigError = class extends Error {
|
|
9883
|
+
constructor(sinkId, message) {
|
|
9884
|
+
super(`[sink:${sinkId}] ${message}`);
|
|
9885
|
+
this.sinkId = sinkId;
|
|
9886
|
+
this.name = "SinkConfigError";
|
|
9887
|
+
}
|
|
9888
|
+
sinkId;
|
|
9889
|
+
};
|
|
9890
|
+
var SinkRegistry = class _SinkRegistry {
|
|
9891
|
+
entries;
|
|
9892
|
+
constructor(entries) {
|
|
9893
|
+
this.entries = entries;
|
|
9894
|
+
}
|
|
9895
|
+
static fromConfig(config, options) {
|
|
9896
|
+
const entries = [];
|
|
9897
|
+
for (const sinkConfig of config.sinks) {
|
|
9898
|
+
entries.push({
|
|
9899
|
+
config: sinkConfig,
|
|
9900
|
+
adapter: buildSink(sinkConfig, options)
|
|
9901
|
+
});
|
|
9902
|
+
}
|
|
9903
|
+
return new _SinkRegistry(entries);
|
|
9904
|
+
}
|
|
9905
|
+
list() {
|
|
9906
|
+
return this.entries;
|
|
9907
|
+
}
|
|
9908
|
+
get(id) {
|
|
9909
|
+
return this.entries.find((e) => e.config.id === id) ?? null;
|
|
9910
|
+
}
|
|
9911
|
+
ids() {
|
|
9912
|
+
return this.entries.map((e) => e.config.id);
|
|
9913
|
+
}
|
|
9914
|
+
async dispose() {
|
|
9915
|
+
for (const entry of this.entries) {
|
|
9916
|
+
if (entry.adapter.dispose) {
|
|
9917
|
+
await entry.adapter.dispose();
|
|
9918
|
+
}
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
};
|
|
9922
|
+
function buildSink(config, options) {
|
|
9923
|
+
const kind = config.kind;
|
|
9924
|
+
switch (kind) {
|
|
9925
|
+
case "slack":
|
|
9926
|
+
return buildSlackSink(config, options);
|
|
9927
|
+
default: {
|
|
9928
|
+
const _exhaustive = kind;
|
|
9929
|
+
throw new SinkConfigError(config.id, `unknown sink kind '${String(_exhaustive)}'`);
|
|
9930
|
+
}
|
|
9931
|
+
}
|
|
9932
|
+
}
|
|
9933
|
+
function buildSlackSink(config, options) {
|
|
9934
|
+
const rawConfig = config.config;
|
|
9935
|
+
const envKey = typeof rawConfig.webhookUrlEnv === "string" ? rawConfig.webhookUrlEnv : null;
|
|
9936
|
+
const inlineUrl = typeof rawConfig.webhookUrl === "string" ? rawConfig.webhookUrl : null;
|
|
9937
|
+
let url;
|
|
9938
|
+
if (envKey) {
|
|
9939
|
+
const v = options.env[envKey];
|
|
9940
|
+
if (!v) {
|
|
9941
|
+
throw new SinkConfigError(
|
|
9942
|
+
config.id,
|
|
9943
|
+
`Slack webhook env var '${envKey}' is not set in the environment`
|
|
9944
|
+
);
|
|
9945
|
+
}
|
|
9946
|
+
url = v;
|
|
9947
|
+
} else if (inlineUrl) {
|
|
9948
|
+
url = inlineUrl;
|
|
9949
|
+
} else {
|
|
9950
|
+
throw new SinkConfigError(
|
|
9951
|
+
config.id,
|
|
9952
|
+
`Slack sink requires 'config.webhookUrlEnv' (preferred) or 'config.webhookUrl'`
|
|
9953
|
+
);
|
|
9954
|
+
}
|
|
9955
|
+
if (!/^https:\/\/hooks\.slack\.com\//.test(url)) {
|
|
9956
|
+
throw new SinkConfigError(
|
|
9957
|
+
config.id,
|
|
9958
|
+
`Slack webhook URL must be an https://hooks.slack.com/ URL`
|
|
9959
|
+
);
|
|
9960
|
+
}
|
|
9961
|
+
const sinkOpts = {
|
|
9962
|
+
id: config.id,
|
|
9963
|
+
webhookUrl: url
|
|
9964
|
+
};
|
|
9965
|
+
if (options.fetchImpl) sinkOpts.fetchImpl = options.fetchImpl;
|
|
9966
|
+
return new SlackSink(sinkOpts);
|
|
9967
|
+
}
|
|
9968
|
+
|
|
9969
|
+
// src/notifications/events.ts
|
|
9970
|
+
var import_node_crypto15 = require("crypto");
|
|
9971
|
+
|
|
9972
|
+
// src/notifications/envelope.ts
|
|
9973
|
+
function asObj(data) {
|
|
9974
|
+
return typeof data === "object" && data !== null ? data : {};
|
|
9975
|
+
}
|
|
9976
|
+
var ENVELOPE_DERIVERS = {
|
|
9977
|
+
"maintenance.started": (event) => {
|
|
9978
|
+
const data = asObj(event.data);
|
|
9979
|
+
return {
|
|
9980
|
+
title: `Maintenance started: ${data.taskId ?? "(unknown task)"}`,
|
|
9981
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` is running.`,
|
|
9982
|
+
severity: "info"
|
|
9983
|
+
};
|
|
9984
|
+
},
|
|
9985
|
+
"maintenance.completed": (event) => {
|
|
9986
|
+
const data = asObj(event.data);
|
|
9987
|
+
return {
|
|
9988
|
+
title: `Maintenance done: ${data.taskId ?? "(unknown task)"}`,
|
|
9989
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` completed successfully.`,
|
|
9990
|
+
severity: "success"
|
|
9991
|
+
};
|
|
9992
|
+
},
|
|
9993
|
+
"maintenance.error": (event) => {
|
|
9994
|
+
const data = asObj(event.data);
|
|
9995
|
+
return {
|
|
9996
|
+
title: `Maintenance failed: ${data.taskId ?? "(unknown task)"}`,
|
|
9997
|
+
summary: data.error ?? "No error message provided.",
|
|
9998
|
+
severity: "error"
|
|
9999
|
+
};
|
|
10000
|
+
},
|
|
10001
|
+
"interaction.created": (event) => {
|
|
10002
|
+
const data = asObj(event.data);
|
|
10003
|
+
return {
|
|
10004
|
+
title: `Action required: ${truncate(data.question ?? "pending interaction", 80)}`,
|
|
10005
|
+
summary: data.question ?? "(no question text)",
|
|
10006
|
+
severity: "warning"
|
|
10007
|
+
};
|
|
10008
|
+
},
|
|
10009
|
+
"interaction.resolved": (event) => {
|
|
10010
|
+
const data = asObj(event.data);
|
|
10011
|
+
return {
|
|
10012
|
+
title: `Interaction resolved`,
|
|
10013
|
+
summary: data.resolution ?? "(no resolution text)",
|
|
10014
|
+
severity: "info"
|
|
10015
|
+
};
|
|
10016
|
+
},
|
|
10017
|
+
"notification.test": (event) => {
|
|
10018
|
+
const data = asObj(event.data);
|
|
10019
|
+
return {
|
|
10020
|
+
title: "Test notification from harness",
|
|
10021
|
+
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
10022
|
+
severity: "info"
|
|
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
|
+
};
|
|
10051
|
+
}
|
|
10052
|
+
};
|
|
10053
|
+
function truncate(s, max) {
|
|
10054
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
10055
|
+
}
|
|
10056
|
+
function fallbackTitle(event) {
|
|
10057
|
+
return event.type;
|
|
10058
|
+
}
|
|
10059
|
+
function fallbackSummary(event) {
|
|
10060
|
+
try {
|
|
10061
|
+
return "```\n" + JSON.stringify(event.data, null, 2) + "\n```";
|
|
10062
|
+
} catch {
|
|
10063
|
+
return String(event.data);
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
function severityFromType(type) {
|
|
10067
|
+
if (type.endsWith(".error") || type.endsWith(".failed")) return "error";
|
|
10068
|
+
if (type.endsWith(".completed") || type.endsWith(".resolved")) return "success";
|
|
10069
|
+
if (type.endsWith(".created") || type.startsWith("interaction.")) return "warning";
|
|
10070
|
+
return "info";
|
|
10071
|
+
}
|
|
10072
|
+
function backfillEnvelope(event, partial) {
|
|
10073
|
+
return {
|
|
10074
|
+
title: truncate(partial.title ?? fallbackTitle(event), 280),
|
|
10075
|
+
summary: partial.summary ?? fallbackSummary(event),
|
|
10076
|
+
severity: partial.severity ?? severityFromType(event.type)
|
|
10077
|
+
};
|
|
10078
|
+
}
|
|
10079
|
+
function wrapAsEnvelope(event) {
|
|
10080
|
+
const deriver = ENVELOPE_DERIVERS[event.type];
|
|
10081
|
+
const partial = deriver ? deriver(event) : {};
|
|
10082
|
+
const envelope = backfillEnvelope(event, partial);
|
|
10083
|
+
if (partial.actions) envelope.actions = partial.actions;
|
|
10084
|
+
if (partial.permalink) envelope.permalink = partial.permalink;
|
|
10085
|
+
if (event.correlationId) envelope.correlationId = event.correlationId;
|
|
10086
|
+
return envelope;
|
|
10087
|
+
}
|
|
10088
|
+
|
|
10089
|
+
// src/notifications/events.ts
|
|
10090
|
+
var NOTIFICATION_TOPICS = [
|
|
10091
|
+
"interaction.created",
|
|
10092
|
+
"interaction.resolved",
|
|
10093
|
+
"maintenance:started",
|
|
10094
|
+
"maintenance:completed",
|
|
10095
|
+
"maintenance:error",
|
|
10096
|
+
// Hermes Phase 4 — skill proposal lifecycle.
|
|
10097
|
+
"proposal.created",
|
|
10098
|
+
"proposal.approved",
|
|
10099
|
+
"proposal.rejected"
|
|
10100
|
+
];
|
|
10101
|
+
function newEventId4() {
|
|
10102
|
+
return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
|
|
10103
|
+
}
|
|
10104
|
+
function dispatchToEntry(bus, entry, event) {
|
|
10105
|
+
const eventType = event.type;
|
|
10106
|
+
const matches = entry.config.events.some((p) => eventMatches(p, eventType));
|
|
10107
|
+
if (!matches) return;
|
|
10108
|
+
const payload = entry.config.wrap_response ? wrapAsEnvelope(event) : event;
|
|
10109
|
+
const summaryBase = {
|
|
10110
|
+
sinkId: entry.adapter.id,
|
|
10111
|
+
kind: entry.adapter.kind,
|
|
10112
|
+
eventType,
|
|
10113
|
+
eventId: event.id
|
|
10114
|
+
};
|
|
10115
|
+
void entry.adapter.deliver({ payload, wrapped: entry.config.wrap_response }).then((result) => {
|
|
10116
|
+
bus.emit("notification.delivery.attempted", { ...summaryBase, ok: result.ok });
|
|
10117
|
+
if (!result.ok) {
|
|
10118
|
+
bus.emit("notification.delivery.failed", {
|
|
10119
|
+
...summaryBase,
|
|
10120
|
+
ok: false,
|
|
10121
|
+
error: result.error
|
|
10122
|
+
});
|
|
10123
|
+
}
|
|
10124
|
+
}).catch((err) => {
|
|
10125
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10126
|
+
bus.emit("notification.delivery.failed", { ...summaryBase, ok: false, error: msg });
|
|
10127
|
+
});
|
|
10128
|
+
}
|
|
10129
|
+
function wireNotificationSinks({ bus, registry }) {
|
|
10130
|
+
const handlers = [];
|
|
10131
|
+
for (const topic of NOTIFICATION_TOPICS) {
|
|
10132
|
+
const eventType = topic.replace(":", ".");
|
|
10133
|
+
const fn = (data) => {
|
|
10134
|
+
const entries = registry.list();
|
|
10135
|
+
if (entries.length === 0) return;
|
|
10136
|
+
const event = {
|
|
10137
|
+
id: newEventId4(),
|
|
10138
|
+
type: eventType,
|
|
10139
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10140
|
+
data
|
|
10141
|
+
};
|
|
10142
|
+
for (const entry of entries) {
|
|
10143
|
+
dispatchToEntry(bus, entry, event);
|
|
10144
|
+
}
|
|
10145
|
+
};
|
|
10146
|
+
bus.on(topic, fn);
|
|
10147
|
+
handlers.push({ topic, fn });
|
|
10148
|
+
}
|
|
10149
|
+
return () => {
|
|
10150
|
+
for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
|
|
10151
|
+
};
|
|
10152
|
+
}
|
|
10153
|
+
|
|
10154
|
+
// src/orchestrator.ts
|
|
10155
|
+
var import_core16 = require("@harness-engineering/core");
|
|
10156
|
+
|
|
10157
|
+
// src/logging/logger.ts
|
|
10158
|
+
var StructuredLogger = class {
|
|
10159
|
+
debug(message, context) {
|
|
10160
|
+
this.log("debug", message, context);
|
|
10161
|
+
}
|
|
10162
|
+
info(message, context) {
|
|
10163
|
+
this.log("info", message, context);
|
|
10164
|
+
}
|
|
10165
|
+
warn(message, context) {
|
|
8623
10166
|
this.log("warn", message, context);
|
|
8624
10167
|
}
|
|
8625
10168
|
error(message, context) {
|
|
@@ -8651,7 +10194,7 @@ var StructuredLogger = class {
|
|
|
8651
10194
|
// src/workspace/config-scanner.ts
|
|
8652
10195
|
var import_node_fs = require("fs");
|
|
8653
10196
|
var import_node_path4 = require("path");
|
|
8654
|
-
var
|
|
10197
|
+
var import_core13 = require("@harness-engineering/core");
|
|
8655
10198
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
|
|
8656
10199
|
var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
|
|
8657
10200
|
var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
|
|
@@ -8673,25 +10216,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
|
|
|
8673
10216
|
} catch {
|
|
8674
10217
|
return null;
|
|
8675
10218
|
}
|
|
8676
|
-
const injectionFindings = (0,
|
|
8677
|
-
const findings = (0,
|
|
10219
|
+
const injectionFindings = (0, import_core13.scanForInjection)(content);
|
|
10220
|
+
const findings = (0, import_core13.mapInjectionFindings)(injectionFindings);
|
|
8678
10221
|
const secFindings = await scanner.scanFile(filePath);
|
|
8679
|
-
findings.push(...(0,
|
|
10222
|
+
findings.push(...(0, import_core13.mapSecurityFindings)(secFindings, findings));
|
|
8680
10223
|
const adjusted = adjustFindingSeverity(findings);
|
|
8681
10224
|
return {
|
|
8682
10225
|
file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
|
|
8683
10226
|
findings: adjusted,
|
|
8684
|
-
overallSeverity: (0,
|
|
10227
|
+
overallSeverity: (0, import_core13.computeOverallSeverity)(adjusted)
|
|
8685
10228
|
};
|
|
8686
10229
|
}
|
|
8687
10230
|
async function scanWorkspaceConfig(workspacePath) {
|
|
8688
|
-
const scanner = new
|
|
10231
|
+
const scanner = new import_core13.SecurityScanner((0, import_core13.parseSecurityConfig)({}));
|
|
8689
10232
|
const results = [];
|
|
8690
10233
|
for (const configFile of CONFIG_FILES) {
|
|
8691
10234
|
const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
|
|
8692
10235
|
if (result) results.push(result);
|
|
8693
10236
|
}
|
|
8694
|
-
return { exitCode: (0,
|
|
10237
|
+
return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
|
|
8695
10238
|
}
|
|
8696
10239
|
|
|
8697
10240
|
// src/maintenance/task-registry.ts
|
|
@@ -8874,6 +10417,19 @@ var BUILT_IN_TASKS = [
|
|
|
8874
10417
|
schedule: "*/15 * * * *",
|
|
8875
10418
|
branch: null,
|
|
8876
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"]
|
|
8877
10433
|
}
|
|
8878
10434
|
];
|
|
8879
10435
|
|
|
@@ -8966,24 +10522,49 @@ var MaintenanceScheduler = class {
|
|
|
8966
10522
|
this.resolvedTasks = this.resolveTasks();
|
|
8967
10523
|
}
|
|
8968
10524
|
/**
|
|
8969
|
-
* Merge built-in task definitions with config overrides
|
|
8970
|
-
*
|
|
8971
|
-
*
|
|
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.
|
|
8972
10529
|
*/
|
|
8973
10530
|
resolveTasks() {
|
|
8974
10531
|
const overrides = this.config.tasks ?? {};
|
|
8975
|
-
|
|
10532
|
+
const customs = this.config.customTasks ?? {};
|
|
10533
|
+
const merged = [];
|
|
10534
|
+
for (const task of BUILT_IN_TASKS) {
|
|
8976
10535
|
const override = overrides[task.id];
|
|
8977
|
-
if (override?.enabled === false)
|
|
8978
|
-
|
|
8979
|
-
}).map((task) => {
|
|
8980
|
-
const override = overrides[task.id];
|
|
8981
|
-
if (!override) return { ...task };
|
|
8982
|
-
return {
|
|
10536
|
+
if (override?.enabled === false) continue;
|
|
10537
|
+
merged.push({
|
|
8983
10538
|
...task,
|
|
8984
|
-
...override
|
|
8985
|
-
};
|
|
8986
|
-
}
|
|
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;
|
|
8987
10568
|
}
|
|
8988
10569
|
/** Returns the resolved (merged) task list. Useful for testing and dashboard. */
|
|
8989
10570
|
getResolvedTasks() {
|
|
@@ -9156,27 +10737,27 @@ var MaintenanceScheduler = class {
|
|
|
9156
10737
|
};
|
|
9157
10738
|
|
|
9158
10739
|
// src/maintenance/leader-elector.ts
|
|
9159
|
-
var
|
|
10740
|
+
var import_types29 = require("@harness-engineering/types");
|
|
9160
10741
|
var SingleProcessLeaderElector = class {
|
|
9161
10742
|
async electLeader() {
|
|
9162
|
-
return (0,
|
|
10743
|
+
return (0, import_types29.Ok)("claimed");
|
|
9163
10744
|
}
|
|
9164
10745
|
};
|
|
9165
10746
|
|
|
9166
10747
|
// src/maintenance/reporter.ts
|
|
9167
|
-
var
|
|
9168
|
-
var
|
|
9169
|
-
var
|
|
9170
|
-
var RunResultSchema =
|
|
9171
|
-
taskId:
|
|
9172
|
-
startedAt:
|
|
9173
|
-
completedAt:
|
|
9174
|
-
status:
|
|
9175
|
-
findings:
|
|
9176
|
-
fixed:
|
|
9177
|
-
prUrl:
|
|
9178
|
-
prUpdated:
|
|
9179
|
-
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()
|
|
9180
10761
|
});
|
|
9181
10762
|
var MAX_HISTORY = 500;
|
|
9182
10763
|
var fallbackLogger = {
|
|
@@ -9200,10 +10781,10 @@ var MaintenanceReporter = class {
|
|
|
9200
10781
|
*/
|
|
9201
10782
|
async load() {
|
|
9202
10783
|
try {
|
|
9203
|
-
await
|
|
9204
|
-
const filePath =
|
|
9205
|
-
const data = await
|
|
9206
|
-
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));
|
|
9207
10788
|
if (parsed.success) {
|
|
9208
10789
|
this.history = parsed.data.slice(0, MAX_HISTORY);
|
|
9209
10790
|
}
|
|
@@ -9236,9 +10817,9 @@ var MaintenanceReporter = class {
|
|
|
9236
10817
|
*/
|
|
9237
10818
|
async persist() {
|
|
9238
10819
|
try {
|
|
9239
|
-
await
|
|
9240
|
-
const filePath =
|
|
9241
|
-
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");
|
|
9242
10823
|
} catch (err) {
|
|
9243
10824
|
this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
|
|
9244
10825
|
}
|
|
@@ -9254,6 +10835,9 @@ var TaskRunner = class {
|
|
|
9254
10835
|
cwd;
|
|
9255
10836
|
prManager;
|
|
9256
10837
|
baseBranch;
|
|
10838
|
+
checkScriptRunner;
|
|
10839
|
+
contextResolver;
|
|
10840
|
+
outputStore;
|
|
9257
10841
|
constructor(options) {
|
|
9258
10842
|
this.config = options.config;
|
|
9259
10843
|
this.checkRunner = options.checkRunner;
|
|
@@ -9262,27 +10846,49 @@ var TaskRunner = class {
|
|
|
9262
10846
|
this.cwd = options.cwd;
|
|
9263
10847
|
this.prManager = options.prManager ?? null;
|
|
9264
10848
|
this.baseBranch = options.baseBranch ?? "main";
|
|
10849
|
+
this.checkScriptRunner = options.checkScriptRunner ?? null;
|
|
10850
|
+
this.contextResolver = options.contextResolver ?? null;
|
|
10851
|
+
this.outputStore = options.outputStore ?? null;
|
|
9265
10852
|
}
|
|
9266
10853
|
/**
|
|
9267
10854
|
* Run a maintenance task and return the result.
|
|
9268
10855
|
* Dispatches to the appropriate execution path based on task type.
|
|
9269
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.
|
|
9270
10861
|
*/
|
|
9271
|
-
async run(task) {
|
|
10862
|
+
async run(task, origin = "cron") {
|
|
9272
10863
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10864
|
+
let result;
|
|
10865
|
+
let captured;
|
|
9273
10866
|
try {
|
|
9274
10867
|
switch (task.type) {
|
|
9275
|
-
case "mechanical-ai":
|
|
9276
|
-
|
|
10868
|
+
case "mechanical-ai": {
|
|
10869
|
+
const out = await this.runMechanicalAI(task, startedAt);
|
|
10870
|
+
result = out.result;
|
|
10871
|
+
captured = out.captured;
|
|
10872
|
+
break;
|
|
10873
|
+
}
|
|
9277
10874
|
case "pure-ai":
|
|
9278
|
-
|
|
9279
|
-
|
|
9280
|
-
|
|
9281
|
-
|
|
9282
|
-
|
|
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
|
+
}
|
|
9283
10889
|
default: {
|
|
9284
10890
|
const _exhaustive = task.type;
|
|
9285
|
-
|
|
10891
|
+
result = this.failureResult(
|
|
9286
10892
|
task.id,
|
|
9287
10893
|
startedAt,
|
|
9288
10894
|
`Unknown task type: ${String(_exhaustive)}`
|
|
@@ -9290,69 +10896,174 @@ var TaskRunner = class {
|
|
|
9290
10896
|
}
|
|
9291
10897
|
}
|
|
9292
10898
|
} catch (err) {
|
|
9293
|
-
|
|
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 {
|
|
9294
10927
|
}
|
|
9295
10928
|
}
|
|
9296
10929
|
/**
|
|
9297
|
-
*
|
|
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.
|
|
9298
10934
|
*/
|
|
9299
|
-
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
|
+
}
|
|
9300
10951
|
if (!task.checkCommand || task.checkCommand.length === 0) {
|
|
9301
|
-
|
|
10952
|
+
throw new Error(`task '${task.id}' is missing checkCommand`);
|
|
9302
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) {
|
|
9303
10986
|
if (!task.fixSkill) {
|
|
9304
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
|
|
10987
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
|
|
9305
10988
|
}
|
|
9306
10989
|
if (!task.branch) {
|
|
9307
|
-
return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
|
|
10990
|
+
return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
|
|
10991
|
+
}
|
|
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
|
+
);
|
|
9308
11000
|
}
|
|
9309
|
-
|
|
9310
|
-
|
|
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) {
|
|
9311
11016
|
return {
|
|
9312
|
-
|
|
9313
|
-
|
|
9314
|
-
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
|
|
9318
|
-
|
|
9319
|
-
|
|
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
|
|
9320
11028
|
};
|
|
9321
11029
|
}
|
|
9322
11030
|
if (this.prManager) {
|
|
9323
11031
|
try {
|
|
9324
11032
|
await this.prManager.ensureBranch(task.branch, this.baseBranch);
|
|
9325
11033
|
} catch (err) {
|
|
9326
|
-
return
|
|
11034
|
+
return wrap(
|
|
11035
|
+
this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
|
|
11036
|
+
baseCaptured
|
|
11037
|
+
);
|
|
9327
11038
|
}
|
|
9328
11039
|
}
|
|
9329
11040
|
const backendName = this.resolveBackend(task.id);
|
|
9330
11041
|
let agentResult;
|
|
9331
11042
|
try {
|
|
9332
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
9333
|
-
|
|
9334
|
-
|
|
9335
|
-
backendName,
|
|
9336
|
-
this.cwd
|
|
9337
|
-
);
|
|
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);
|
|
9338
11046
|
} catch (err) {
|
|
9339
11047
|
return {
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
|
|
9343
|
-
|
|
9344
|
-
|
|
9345
|
-
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
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
|
|
9349
11060
|
};
|
|
9350
11061
|
}
|
|
9351
11062
|
let prUrl = null;
|
|
9352
11063
|
let prUpdated = false;
|
|
9353
11064
|
if (this.prManager && agentResult.producedCommits) {
|
|
9354
11065
|
try {
|
|
9355
|
-
const summary = `Findings: ${
|
|
11066
|
+
const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
|
|
9356
11067
|
const prResult = await this.prManager.ensurePR(task, summary);
|
|
9357
11068
|
prUrl = prResult.prUrl;
|
|
9358
11069
|
prUpdated = prResult.prUpdated;
|
|
@@ -9361,14 +11072,17 @@ var TaskRunner = class {
|
|
|
9361
11072
|
}
|
|
9362
11073
|
}
|
|
9363
11074
|
return {
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
|
|
9371
|
-
|
|
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
|
|
9372
11086
|
};
|
|
9373
11087
|
}
|
|
9374
11088
|
/**
|
|
@@ -9388,15 +11102,13 @@ var TaskRunner = class {
|
|
|
9388
11102
|
return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
|
|
9389
11103
|
}
|
|
9390
11104
|
}
|
|
11105
|
+
const promptContext = await this.composePromptContext(task);
|
|
9391
11106
|
const backendName = this.resolveBackend(task.id);
|
|
9392
11107
|
let agentResult;
|
|
9393
11108
|
try {
|
|
9394
|
-
agentResult = await this.agentDispatcher.dispatch(
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
backendName,
|
|
9398
|
-
this.cwd
|
|
9399
|
-
);
|
|
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);
|
|
9400
11112
|
} catch (err) {
|
|
9401
11113
|
return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
|
|
9402
11114
|
}
|
|
@@ -9424,7 +11136,7 @@ var TaskRunner = class {
|
|
|
9424
11136
|
};
|
|
9425
11137
|
}
|
|
9426
11138
|
/**
|
|
9427
|
-
* Report-only: run check
|
|
11139
|
+
* Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
|
|
9428
11140
|
*
|
|
9429
11141
|
* Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
|
|
9430
11142
|
* and `harness compound scan-candidates` in `--non-interactive` mode):
|
|
@@ -9434,122 +11146,715 @@ var TaskRunner = class {
|
|
|
9434
11146
|
* Legacy report-only tasks emit free-form output and fall through to 'success'.
|
|
9435
11147
|
*/
|
|
9436
11148
|
async runReportOnly(task, startedAt) {
|
|
9437
|
-
if (!task.checkCommand
|
|
9438
|
-
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)));
|
|
11163
|
+
}
|
|
11164
|
+
const parsed = parseStatusLine(check.stdout);
|
|
11165
|
+
const status = parsed?.status ?? "success";
|
|
11166
|
+
const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
|
|
11167
|
+
const result = {
|
|
11168
|
+
taskId: task.id,
|
|
11169
|
+
startedAt,
|
|
11170
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11171
|
+
status,
|
|
11172
|
+
findings,
|
|
11173
|
+
fixed: 0,
|
|
11174
|
+
prUrl: null,
|
|
11175
|
+
prUpdated: false
|
|
11176
|
+
};
|
|
11177
|
+
if (parsed?.error) {
|
|
11178
|
+
result.error = parsed.error;
|
|
11179
|
+
}
|
|
11180
|
+
return {
|
|
11181
|
+
result,
|
|
11182
|
+
captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
|
|
11183
|
+
};
|
|
11184
|
+
}
|
|
11185
|
+
/**
|
|
11186
|
+
* Housekeeping: run command directly, no AI, no PR.
|
|
11187
|
+
*
|
|
11188
|
+
* Captures stdout and parses a trailing JSON status line if present.
|
|
11189
|
+
* Recognized contracts:
|
|
11190
|
+
* - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
|
|
11191
|
+
* - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
|
|
11192
|
+
* Legacy housekeeping commands that emit no JSON keep the prior behavior:
|
|
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.
|
|
11197
|
+
*/
|
|
11198
|
+
async runHousekeeping(task, startedAt) {
|
|
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
|
+
);
|
|
11207
|
+
}
|
|
11208
|
+
let stdout;
|
|
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
|
+
}
|
|
11227
|
+
}
|
|
11228
|
+
const parsed = parseStatusLine(stdout);
|
|
11229
|
+
const status = parsed?.status ?? "success";
|
|
11230
|
+
const result = {
|
|
11231
|
+
taskId: task.id,
|
|
11232
|
+
startedAt,
|
|
11233
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11234
|
+
status,
|
|
11235
|
+
findings: 0,
|
|
11236
|
+
fixed: 0,
|
|
11237
|
+
prUrl: null,
|
|
11238
|
+
prUpdated: false
|
|
11239
|
+
};
|
|
11240
|
+
if (parsed?.error) result.error = parsed.error;
|
|
11241
|
+
return { result, captured: { stdout, stderr, structured } };
|
|
11242
|
+
}
|
|
11243
|
+
/**
|
|
11244
|
+
* Resolve which AI backend name to use for a given task.
|
|
11245
|
+
* Priority: per-task override > global config > 'local' default.
|
|
11246
|
+
*/
|
|
11247
|
+
resolveBackend(taskId) {
|
|
11248
|
+
const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
|
|
11249
|
+
if (taskOverride) return taskOverride;
|
|
11250
|
+
return this.config.aiBackend ?? "local";
|
|
11251
|
+
}
|
|
11252
|
+
failureResult(taskId, startedAt, error) {
|
|
11253
|
+
return {
|
|
11254
|
+
taskId,
|
|
11255
|
+
startedAt,
|
|
11256
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11257
|
+
status: "failure",
|
|
11258
|
+
findings: 0,
|
|
11259
|
+
fixed: 0,
|
|
11260
|
+
prUrl: null,
|
|
11261
|
+
prUpdated: false,
|
|
11262
|
+
error
|
|
11263
|
+
};
|
|
11264
|
+
}
|
|
11265
|
+
};
|
|
11266
|
+
function wrap(result, captured) {
|
|
11267
|
+
return captured ? { result, captured } : { result };
|
|
11268
|
+
}
|
|
11269
|
+
function parseStatusLine(output) {
|
|
11270
|
+
const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
11271
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
11272
|
+
const line = lines[i];
|
|
11273
|
+
if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
|
|
11274
|
+
try {
|
|
11275
|
+
const obj = JSON.parse(line);
|
|
11276
|
+
const s = obj.status;
|
|
11277
|
+
if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
|
|
11278
|
+
const parsed = { status: s, rawStatus: s };
|
|
11279
|
+
if (typeof obj.candidatesFound === "number") {
|
|
11280
|
+
parsed.candidatesFound = obj.candidatesFound;
|
|
11281
|
+
}
|
|
11282
|
+
if (typeof obj.error === "string") {
|
|
11283
|
+
parsed.error = obj.error;
|
|
11284
|
+
}
|
|
11285
|
+
if (typeof obj.reason === "string") {
|
|
11286
|
+
parsed.reason = obj.reason;
|
|
11287
|
+
}
|
|
11288
|
+
if (typeof obj.detail === "string" && !parsed.error) {
|
|
11289
|
+
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
11290
|
+
}
|
|
11291
|
+
return parsed;
|
|
11292
|
+
}
|
|
11293
|
+
if (s === "updated" || s === "no-op") {
|
|
11294
|
+
return { status: "success", rawStatus: s };
|
|
11295
|
+
}
|
|
11296
|
+
if (s === "error") {
|
|
11297
|
+
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
11298
|
+
return { status: "failure", error: message, rawStatus: "error" };
|
|
11299
|
+
}
|
|
11300
|
+
} catch {
|
|
11301
|
+
}
|
|
11302
|
+
}
|
|
11303
|
+
return null;
|
|
11304
|
+
}
|
|
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" });
|
|
9439
11724
|
}
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
taskId: task.id,
|
|
9446
|
-
startedAt,
|
|
9447
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9448
|
-
status,
|
|
9449
|
-
findings,
|
|
9450
|
-
fixed: 0,
|
|
9451
|
-
prUrl: null,
|
|
9452
|
-
prUpdated: false
|
|
9453
|
-
};
|
|
9454
|
-
if (parsed?.error) {
|
|
9455
|
-
result.error = parsed.error;
|
|
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
|
+
});
|
|
9456
11730
|
}
|
|
9457
|
-
return result;
|
|
9458
11731
|
}
|
|
9459
|
-
|
|
9460
|
-
|
|
9461
|
-
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9468
|
-
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
|
|
9473
|
-
let stdout;
|
|
9474
|
-
try {
|
|
9475
|
-
const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
|
|
9476
|
-
stdout = out.stdout ?? "";
|
|
9477
|
-
} catch (err) {
|
|
9478
|
-
return this.failureResult(task.id, startedAt, String(err));
|
|
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
|
+
});
|
|
9479
11746
|
}
|
|
9480
|
-
const parsed = parseStatusLine(stdout);
|
|
9481
|
-
const status = parsed?.status ?? "success";
|
|
9482
|
-
const result = {
|
|
9483
|
-
taskId: task.id,
|
|
9484
|
-
startedAt,
|
|
9485
|
-
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9486
|
-
status,
|
|
9487
|
-
findings: 0,
|
|
9488
|
-
fixed: 0,
|
|
9489
|
-
prUrl: null,
|
|
9490
|
-
prUpdated: false
|
|
9491
|
-
};
|
|
9492
|
-
if (parsed?.error) result.error = parsed.error;
|
|
9493
|
-
return result;
|
|
9494
11747
|
}
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
|
|
9501
|
-
if (taskOverride) return taskOverride;
|
|
9502
|
-
return this.config.aiBackend ?? "local";
|
|
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
|
+
});
|
|
9503
11753
|
}
|
|
9504
|
-
|
|
9505
|
-
|
|
9506
|
-
|
|
9507
|
-
|
|
9508
|
-
|
|
9509
|
-
|
|
9510
|
-
|
|
9511
|
-
fixed: 0,
|
|
9512
|
-
prUrl: null,
|
|
9513
|
-
prUpdated: false,
|
|
9514
|
-
error
|
|
9515
|
-
};
|
|
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
|
+
});
|
|
9516
11761
|
}
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
}
|
|
9531
|
-
|
|
9532
|
-
parsed.error = obj.error;
|
|
9533
|
-
}
|
|
9534
|
-
if (typeof obj.reason === "string") {
|
|
9535
|
-
parsed.reason = obj.reason;
|
|
9536
|
-
}
|
|
9537
|
-
if (typeof obj.detail === "string" && !parsed.error) {
|
|
9538
|
-
parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
|
|
9539
|
-
}
|
|
9540
|
-
return parsed;
|
|
9541
|
-
}
|
|
9542
|
-
if (s === "updated" || s === "no-op") {
|
|
9543
|
-
return { status: "success", rawStatus: s };
|
|
9544
|
-
}
|
|
9545
|
-
if (s === "error") {
|
|
9546
|
-
const message = typeof obj.message === "string" ? obj.message : "unknown error";
|
|
9547
|
-
return { status: "failure", error: message, rawStatus: "error" };
|
|
9548
|
-
}
|
|
9549
|
-
} catch {
|
|
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
|
+
});
|
|
9550
11777
|
}
|
|
9551
11778
|
}
|
|
9552
|
-
|
|
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
|
+
});
|
|
9553
11858
|
}
|
|
9554
11859
|
|
|
9555
11860
|
// src/orchestrator.ts
|
|
@@ -9639,13 +11944,20 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9639
11944
|
cacheMetrics;
|
|
9640
11945
|
otlpExporter;
|
|
9641
11946
|
telemetryFanoutOff;
|
|
11947
|
+
// Hermes Phase 3: in-process notification sinks subscribe to the same
|
|
11948
|
+
// event bus (`this`) that webhook fanout uses, applying envelope
|
|
11949
|
+
// formatting before delivering to Slack/etc. The registry + unwire
|
|
11950
|
+
// handle are kept on the instance so stop() can detach listeners and
|
|
11951
|
+
// call adapter dispose() in deterministic order.
|
|
11952
|
+
notificationsRegistry;
|
|
11953
|
+
notificationFanoutOff;
|
|
9642
11954
|
orchestratorIdPromise;
|
|
9643
11955
|
recorder;
|
|
9644
11956
|
intelligenceRunner;
|
|
9645
11957
|
completionHandler;
|
|
9646
11958
|
/** Project root directory, derived from workspace root. */
|
|
9647
11959
|
get projectRoot() {
|
|
9648
|
-
return
|
|
11960
|
+
return path19.resolve(this.config.workspace.root, "..", "..");
|
|
9649
11961
|
}
|
|
9650
11962
|
enrichedSpecsByIssue = /* @__PURE__ */ new Map();
|
|
9651
11963
|
/** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
|
|
@@ -9700,10 +12012,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9700
12012
|
this.renderer = new PromptRenderer();
|
|
9701
12013
|
this.overrideBackend = overrides?.backend ?? null;
|
|
9702
12014
|
this.interactionQueue = new InteractionQueue(
|
|
9703
|
-
|
|
12015
|
+
path19.join(config.workspace.root, "..", "interactions"),
|
|
9704
12016
|
this
|
|
9705
12017
|
);
|
|
9706
|
-
this.analysisArchive = new AnalysisArchive(
|
|
12018
|
+
this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
|
|
9707
12019
|
const backendsMap = this.config.agent.backends ?? {};
|
|
9708
12020
|
for (const [name, def] of Object.entries(backendsMap)) {
|
|
9709
12021
|
if (def.type === "local" || def.type === "pi") {
|
|
@@ -9717,7 +12029,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9717
12029
|
this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
|
|
9718
12030
|
}
|
|
9719
12031
|
}
|
|
9720
|
-
this.cacheMetrics = new
|
|
12032
|
+
this.cacheMetrics = new import_core16.CacheMetricsRecorder();
|
|
9721
12033
|
if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
|
|
9722
12034
|
const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
|
|
9723
12035
|
const firstBackendName = Object.keys(this.config.agent.backends)[0];
|
|
@@ -9747,7 +12059,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9747
12059
|
...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
|
|
9748
12060
|
});
|
|
9749
12061
|
this.recorder = new StreamRecorder(
|
|
9750
|
-
|
|
12062
|
+
path19.resolve(config.workspace.root, "..", "streams"),
|
|
9751
12063
|
this.logger
|
|
9752
12064
|
);
|
|
9753
12065
|
const self = this;
|
|
@@ -9778,10 +12090,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9778
12090
|
this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
|
|
9779
12091
|
if (config.server?.port) {
|
|
9780
12092
|
const webhookStore = new WebhookStore(
|
|
9781
|
-
|
|
12093
|
+
path19.join(this.projectRoot, ".harness", "webhooks.json")
|
|
9782
12094
|
);
|
|
9783
12095
|
this.webhookQueue = new WebhookQueue(
|
|
9784
|
-
|
|
12096
|
+
path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
|
|
9785
12097
|
);
|
|
9786
12098
|
const webhookDelivery = new WebhookDelivery({
|
|
9787
12099
|
queue: this.webhookQueue,
|
|
@@ -9794,9 +12106,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9794
12106
|
delivery: webhookDelivery
|
|
9795
12107
|
});
|
|
9796
12108
|
webhookDelivery.start();
|
|
12109
|
+
this.setupNotifications(config.notifications);
|
|
9797
12110
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
9798
12111
|
if (otlpCfg) {
|
|
9799
|
-
this.otlpExporter = new
|
|
12112
|
+
this.otlpExporter = new import_core16.OTLPExporter({
|
|
9800
12113
|
endpoint: otlpCfg.endpoint,
|
|
9801
12114
|
...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
|
|
9802
12115
|
...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
|
|
@@ -9818,7 +12131,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9818
12131
|
queue: this.webhookQueue
|
|
9819
12132
|
},
|
|
9820
12133
|
cacheMetrics: this.cacheMetrics,
|
|
9821
|
-
plansDir:
|
|
12134
|
+
plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
|
|
9822
12135
|
pipeline: this.pipeline,
|
|
9823
12136
|
analysisArchive: this.analysisArchive,
|
|
9824
12137
|
roadmapPath: config.tracker.filePath ?? null,
|
|
@@ -9856,7 +12169,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9856
12169
|
...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
|
|
9857
12170
|
...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
|
|
9858
12171
|
};
|
|
9859
|
-
const clientResult = (0,
|
|
12172
|
+
const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
|
|
9860
12173
|
if (!clientResult.ok) throw clientResult.error;
|
|
9861
12174
|
return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
|
|
9862
12175
|
}
|
|
@@ -9874,13 +12187,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9874
12187
|
const logger = this.logger;
|
|
9875
12188
|
const checkRunner = {
|
|
9876
12189
|
run: async (command, cwd) => {
|
|
9877
|
-
const { execFile:
|
|
9878
|
-
const { promisify:
|
|
9879
|
-
const
|
|
12190
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12191
|
+
const { promisify: promisify5 } = await import("util");
|
|
12192
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
9880
12193
|
const [cmd, ...args] = command;
|
|
9881
12194
|
if (!cmd) return { passed: true, findings: 0, output: "" };
|
|
9882
12195
|
try {
|
|
9883
|
-
const { stdout } = await
|
|
12196
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
9884
12197
|
const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
|
|
9885
12198
|
const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
|
|
9886
12199
|
return { passed: findings === 0, findings, output: stdout };
|
|
@@ -9909,13 +12222,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9909
12222
|
};
|
|
9910
12223
|
const commandExecutor = {
|
|
9911
12224
|
exec: async (command, cwd) => {
|
|
9912
|
-
const { execFile:
|
|
9913
|
-
const { promisify:
|
|
9914
|
-
const
|
|
12225
|
+
const { execFile: execFile7 } = await import("child_process");
|
|
12226
|
+
const { promisify: promisify5 } = await import("util");
|
|
12227
|
+
const execFileAsync2 = promisify5(execFile7);
|
|
9915
12228
|
const [cmd, ...args] = command;
|
|
9916
12229
|
if (!cmd) return { stdout: "" };
|
|
9917
12230
|
try {
|
|
9918
|
-
const { stdout } = await
|
|
12231
|
+
const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
|
|
9919
12232
|
return { stdout: String(stdout) };
|
|
9920
12233
|
} catch (err) {
|
|
9921
12234
|
logger.warn("Maintenance command execution failed", {
|
|
@@ -9927,12 +12240,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9927
12240
|
}
|
|
9928
12241
|
}
|
|
9929
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
|
+
});
|
|
9930
12259
|
return new TaskRunner({
|
|
9931
12260
|
config: maintenanceConfig,
|
|
9932
12261
|
checkRunner,
|
|
9933
12262
|
agentDispatcher,
|
|
9934
12263
|
commandExecutor,
|
|
9935
|
-
cwd: this.projectRoot
|
|
12264
|
+
cwd: this.projectRoot,
|
|
12265
|
+
checkScriptRunner,
|
|
12266
|
+
contextResolver,
|
|
12267
|
+
outputStore
|
|
9936
12268
|
});
|
|
9937
12269
|
}
|
|
9938
12270
|
/**
|
|
@@ -9940,8 +12272,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9940
12272
|
* Extracted from start() to keep function length under threshold.
|
|
9941
12273
|
*/
|
|
9942
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
|
+
}
|
|
9943
12284
|
this.maintenanceReporter = new MaintenanceReporter({
|
|
9944
|
-
persistDir:
|
|
12285
|
+
persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
|
|
9945
12286
|
logger: this.logger
|
|
9946
12287
|
});
|
|
9947
12288
|
await this.maintenanceReporter.load();
|
|
@@ -10208,7 +12549,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10208
12549
|
{ issueId }
|
|
10209
12550
|
);
|
|
10210
12551
|
await this.interactionQueue.push({
|
|
10211
|
-
id: `interaction-${(0,
|
|
12552
|
+
id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
|
|
10212
12553
|
issueId,
|
|
10213
12554
|
type: "needs-human",
|
|
10214
12555
|
reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
|
|
@@ -10284,7 +12625,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10284
12625
|
{ issueId: effect.issueId }
|
|
10285
12626
|
);
|
|
10286
12627
|
await this.interactionQueue.push({
|
|
10287
|
-
id: `interaction-${(0,
|
|
12628
|
+
id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
|
|
10288
12629
|
issueId: effect.issueId,
|
|
10289
12630
|
type: "needs-human",
|
|
10290
12631
|
reasons: effect.reasons,
|
|
@@ -10380,12 +12721,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10380
12721
|
async postLifecycleComment(identifier, externalId, event) {
|
|
10381
12722
|
try {
|
|
10382
12723
|
if (!externalId) return;
|
|
10383
|
-
const trackerConfig = (0,
|
|
12724
|
+
const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
|
|
10384
12725
|
if (!trackerConfig) return;
|
|
10385
12726
|
const token = process.env.GITHUB_TOKEN;
|
|
10386
12727
|
if (!token) return;
|
|
10387
12728
|
const orchestratorId = await this.orchestratorIdPromise;
|
|
10388
|
-
const adapter = new
|
|
12729
|
+
const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
|
|
10389
12730
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
|
|
10390
12731
|
const actionMap = {
|
|
10391
12732
|
claimed: "Dispatching agent for autonomous execution",
|
|
@@ -10458,7 +12799,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10458
12799
|
...f.line !== void 0 ? { line: f.line } : {}
|
|
10459
12800
|
}))
|
|
10460
12801
|
);
|
|
10461
|
-
(0,
|
|
12802
|
+
(0, import_core14.writeTaint)(
|
|
10462
12803
|
workspacePath,
|
|
10463
12804
|
issue.id,
|
|
10464
12805
|
"Medium-severity injection patterns found in workspace config files",
|
|
@@ -10629,6 +12970,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10629
12970
|
);
|
|
10630
12971
|
this.emit("state_change", this.getSnapshot());
|
|
10631
12972
|
}
|
|
12973
|
+
/**
|
|
12974
|
+
* Hermes Phase 3: wire in-process notification sinks against the
|
|
12975
|
+
* orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
|
|
12976
|
+
* missing env var) logs + skips rather than breaking startup — the
|
|
12977
|
+
* hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
|
|
12978
|
+
* to the same topics as `wireWebhookFanout`; a slow Slack call cannot
|
|
12979
|
+
* block webhook delivery because the two paths fan out independently.
|
|
12980
|
+
*/
|
|
12981
|
+
setupNotifications(notifConfig) {
|
|
12982
|
+
if (!notifConfig || !notifConfig.sinks || notifConfig.sinks.length === 0) return;
|
|
12983
|
+
try {
|
|
12984
|
+
this.notificationsRegistry = SinkRegistry.fromConfig(notifConfig, {
|
|
12985
|
+
env: process.env
|
|
12986
|
+
});
|
|
12987
|
+
this.notificationFanoutOff = wireNotificationSinks({
|
|
12988
|
+
bus: this,
|
|
12989
|
+
registry: this.notificationsRegistry
|
|
12990
|
+
});
|
|
12991
|
+
} catch (err) {
|
|
12992
|
+
this.logger.warn(
|
|
12993
|
+
`notifications sink registry failed: ${err instanceof Error ? err.message : String(err)}; sinks disabled`
|
|
12994
|
+
);
|
|
12995
|
+
delete this.notificationsRegistry;
|
|
12996
|
+
}
|
|
12997
|
+
}
|
|
10632
12998
|
/**
|
|
10633
12999
|
* Stops execution for a specific issue.
|
|
10634
13000
|
*
|
|
@@ -10796,6 +13162,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10796
13162
|
this.webhookFanoutOff();
|
|
10797
13163
|
delete this.webhookFanoutOff;
|
|
10798
13164
|
}
|
|
13165
|
+
if (this.notificationFanoutOff) {
|
|
13166
|
+
this.notificationFanoutOff();
|
|
13167
|
+
delete this.notificationFanoutOff;
|
|
13168
|
+
}
|
|
13169
|
+
if (this.notificationsRegistry) {
|
|
13170
|
+
await this.notificationsRegistry.dispose();
|
|
13171
|
+
delete this.notificationsRegistry;
|
|
13172
|
+
}
|
|
10799
13173
|
if (this.telemetryFanoutOff) {
|
|
10800
13174
|
this.telemetryFanoutOff();
|
|
10801
13175
|
delete this.telemetryFanoutOff;
|
|
@@ -11084,11 +13458,11 @@ function launchTUI(orchestrator) {
|
|
|
11084
13458
|
}
|
|
11085
13459
|
|
|
11086
13460
|
// src/maintenance/sync-main.ts
|
|
11087
|
-
var
|
|
11088
|
-
var
|
|
11089
|
-
var
|
|
13461
|
+
var import_node_child_process12 = require("child_process");
|
|
13462
|
+
var import_node_util4 = require("util");
|
|
13463
|
+
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
11090
13464
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
11091
|
-
const exec = (0,
|
|
13465
|
+
const exec = (0, import_node_util4.promisify)(execFileFn);
|
|
11092
13466
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
11093
13467
|
return { stdout: String(stdout), stderr: String(stderr) };
|
|
11094
13468
|
}
|
|
@@ -11150,8 +13524,8 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
11150
13524
|
}
|
|
11151
13525
|
}
|
|
11152
13526
|
async function syncMain(repoRoot, opts = {}) {
|
|
11153
|
-
const execFileFn = opts.execFileFn ??
|
|
11154
|
-
const timeoutMs = opts.timeoutMs ??
|
|
13527
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
|
|
13528
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
11155
13529
|
try {
|
|
11156
13530
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
11157
13531
|
if (!originRef) {
|
|
@@ -11226,11 +13600,471 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11226
13600
|
};
|
|
11227
13601
|
}
|
|
11228
13602
|
}
|
|
13603
|
+
|
|
13604
|
+
// src/sessions/search-index.ts
|
|
13605
|
+
var fs17 = __toESM(require("fs"));
|
|
13606
|
+
var path20 = __toESM(require("path"));
|
|
13607
|
+
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
13608
|
+
var import_types31 = require("@harness-engineering/types");
|
|
13609
|
+
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
13610
|
+
var SCHEMA_SQL2 = `
|
|
13611
|
+
CREATE TABLE IF NOT EXISTS session_docs (
|
|
13612
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13613
|
+
session_id TEXT NOT NULL,
|
|
13614
|
+
archived INTEGER NOT NULL,
|
|
13615
|
+
file_kind TEXT NOT NULL,
|
|
13616
|
+
path TEXT NOT NULL,
|
|
13617
|
+
mtime_ms INTEGER NOT NULL,
|
|
13618
|
+
body TEXT NOT NULL,
|
|
13619
|
+
UNIQUE (session_id, archived, file_kind)
|
|
13620
|
+
);
|
|
13621
|
+
|
|
13622
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_docs_fts USING fts5 (
|
|
13623
|
+
body,
|
|
13624
|
+
content='session_docs',
|
|
13625
|
+
content_rowid='id',
|
|
13626
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
13627
|
+
);
|
|
13628
|
+
|
|
13629
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ai
|
|
13630
|
+
AFTER INSERT ON session_docs
|
|
13631
|
+
BEGIN INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body); END;
|
|
13632
|
+
|
|
13633
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ad
|
|
13634
|
+
AFTER DELETE ON session_docs
|
|
13635
|
+
BEGIN INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body); END;
|
|
13636
|
+
|
|
13637
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_au
|
|
13638
|
+
AFTER UPDATE ON session_docs
|
|
13639
|
+
BEGIN
|
|
13640
|
+
INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body);
|
|
13641
|
+
INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body);
|
|
13642
|
+
END;
|
|
13643
|
+
`;
|
|
13644
|
+
var DEFAULT_LIMIT = 20;
|
|
13645
|
+
function normalizeFts5Query(query) {
|
|
13646
|
+
const advancedSyntax = /["()*^+]|\bAND\b|\bOR\b|\bNOT\b|[A-Za-z_]+:/;
|
|
13647
|
+
if (advancedSyntax.test(query)) return query;
|
|
13648
|
+
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
13649
|
+
}
|
|
13650
|
+
function searchIndexPath(projectPath) {
|
|
13651
|
+
return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
13652
|
+
}
|
|
13653
|
+
var FILE_KIND_TO_FILENAME = {
|
|
13654
|
+
summary: "summary.md",
|
|
13655
|
+
learnings: "learnings.md",
|
|
13656
|
+
failures: "failures.md",
|
|
13657
|
+
sections: "session-sections.md",
|
|
13658
|
+
llm_summary: "llm-summary.md"
|
|
13659
|
+
};
|
|
13660
|
+
var SqliteSearchIndex = class {
|
|
13661
|
+
db;
|
|
13662
|
+
upsertStmt;
|
|
13663
|
+
removeSessionStmt;
|
|
13664
|
+
totalStmt;
|
|
13665
|
+
constructor(dbPath) {
|
|
13666
|
+
fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
|
|
13667
|
+
this.db = new import_better_sqlite32.default(dbPath);
|
|
13668
|
+
this.db.pragma("journal_mode = WAL");
|
|
13669
|
+
this.db.pragma("synchronous = NORMAL");
|
|
13670
|
+
this.db.exec(SCHEMA_SQL2);
|
|
13671
|
+
this.upsertStmt = this.db.prepare(
|
|
13672
|
+
`INSERT INTO session_docs (session_id, archived, file_kind, path, mtime_ms, body)
|
|
13673
|
+
VALUES (@sessionId, @archived, @fileKind, @path, @mtimeMs, @body)
|
|
13674
|
+
ON CONFLICT(session_id, archived, file_kind) DO UPDATE SET
|
|
13675
|
+
path = excluded.path,
|
|
13676
|
+
mtime_ms = excluded.mtime_ms,
|
|
13677
|
+
body = excluded.body`
|
|
13678
|
+
);
|
|
13679
|
+
this.removeSessionStmt = this.db.prepare(`DELETE FROM session_docs WHERE session_id = ?`);
|
|
13680
|
+
this.totalStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM session_docs`);
|
|
13681
|
+
}
|
|
13682
|
+
upsertSessionDoc(doc) {
|
|
13683
|
+
this.upsertStmt.run({
|
|
13684
|
+
sessionId: doc.sessionId,
|
|
13685
|
+
archived: doc.archived ? 1 : 0,
|
|
13686
|
+
fileKind: doc.fileKind,
|
|
13687
|
+
path: doc.path,
|
|
13688
|
+
mtimeMs: Math.floor(doc.mtimeMs),
|
|
13689
|
+
body: doc.body
|
|
13690
|
+
});
|
|
13691
|
+
}
|
|
13692
|
+
removeSession(sessionId) {
|
|
13693
|
+
const info = this.removeSessionStmt.run(sessionId);
|
|
13694
|
+
return info.changes;
|
|
13695
|
+
}
|
|
13696
|
+
/**
|
|
13697
|
+
* Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
|
|
13698
|
+
* re-walk. Live (archived=0) rows are preserved.
|
|
13699
|
+
*/
|
|
13700
|
+
resetArchived() {
|
|
13701
|
+
this.db.prepare(`DELETE FROM session_docs WHERE archived = 1`).run();
|
|
13702
|
+
}
|
|
13703
|
+
/** Total rows currently indexed (across both live and archived). */
|
|
13704
|
+
totalIndexed() {
|
|
13705
|
+
const row = this.totalStmt.get();
|
|
13706
|
+
return row.n;
|
|
13707
|
+
}
|
|
13708
|
+
/**
|
|
13709
|
+
* Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
|
|
13710
|
+
* FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
|
|
13711
|
+
* is therefore the user-facing language. Errors from malformed queries
|
|
13712
|
+
* surface as thrown `SqliteError` so the CLI can catch + render them.
|
|
13713
|
+
*/
|
|
13714
|
+
search(query, opts = {}) {
|
|
13715
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
13716
|
+
const filters = [];
|
|
13717
|
+
const params = { q: normalizeFts5Query(query), limit };
|
|
13718
|
+
if (opts.archivedOnly) {
|
|
13719
|
+
filters.push("d.archived = 1");
|
|
13720
|
+
}
|
|
13721
|
+
const fileKinds = opts.fileKinds && opts.fileKinds.length > 0 ? opts.fileKinds : null;
|
|
13722
|
+
if (fileKinds) {
|
|
13723
|
+
const placeholders = fileKinds.map((_, i) => `@fk${i}`).join(", ");
|
|
13724
|
+
filters.push(`d.file_kind IN (${placeholders})`);
|
|
13725
|
+
fileKinds.forEach((k, i) => {
|
|
13726
|
+
params[`fk${i}`] = k;
|
|
13727
|
+
});
|
|
13728
|
+
}
|
|
13729
|
+
const whereClause = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
|
|
13730
|
+
const sql = `
|
|
13731
|
+
SELECT
|
|
13732
|
+
d.session_id AS sessionId,
|
|
13733
|
+
d.archived AS archived,
|
|
13734
|
+
d.file_kind AS fileKind,
|
|
13735
|
+
d.path AS path,
|
|
13736
|
+
bm25(session_docs_fts) AS bm25,
|
|
13737
|
+
snippet(session_docs_fts, 0, '\u2026', '\u2026', '\u2026', 16) AS snippet
|
|
13738
|
+
FROM session_docs_fts
|
|
13739
|
+
JOIN session_docs d ON d.id = session_docs_fts.rowid
|
|
13740
|
+
WHERE session_docs_fts MATCH @q
|
|
13741
|
+
${whereClause}
|
|
13742
|
+
ORDER BY bm25 ASC
|
|
13743
|
+
LIMIT @limit
|
|
13744
|
+
`;
|
|
13745
|
+
const start = Date.now();
|
|
13746
|
+
const rows = this.db.prepare(sql).all(params);
|
|
13747
|
+
const durationMs = Date.now() - start;
|
|
13748
|
+
const matches = rows.map((r) => ({
|
|
13749
|
+
sessionId: r.sessionId,
|
|
13750
|
+
archived: r.archived === 1,
|
|
13751
|
+
fileKind: r.fileKind,
|
|
13752
|
+
path: r.path,
|
|
13753
|
+
bm25: r.bm25,
|
|
13754
|
+
snippet: r.snippet
|
|
13755
|
+
}));
|
|
13756
|
+
return { matches, durationMs, totalIndexed: this.totalIndexed() };
|
|
13757
|
+
}
|
|
13758
|
+
close() {
|
|
13759
|
+
this.db.close();
|
|
13760
|
+
}
|
|
13761
|
+
};
|
|
13762
|
+
function openSearchIndex(projectPath) {
|
|
13763
|
+
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
13764
|
+
}
|
|
13765
|
+
function indexSessionDirectory(idx, args) {
|
|
13766
|
+
const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
|
|
13767
|
+
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
13768
|
+
let docsWritten = 0;
|
|
13769
|
+
for (const kind of kinds) {
|
|
13770
|
+
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
13771
|
+
const filePath = path20.join(args.sessionDir, fileName);
|
|
13772
|
+
if (!fs17.existsSync(filePath)) continue;
|
|
13773
|
+
let body = fs17.readFileSync(filePath, "utf8");
|
|
13774
|
+
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
13775
|
+
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
13776
|
+
}
|
|
13777
|
+
const stat = fs17.statSync(filePath);
|
|
13778
|
+
const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
13779
|
+
idx.upsertSessionDoc({
|
|
13780
|
+
sessionId: args.sessionId,
|
|
13781
|
+
archived: args.archived,
|
|
13782
|
+
fileKind: kind,
|
|
13783
|
+
path: relPath,
|
|
13784
|
+
mtimeMs: stat.mtimeMs,
|
|
13785
|
+
body
|
|
13786
|
+
});
|
|
13787
|
+
docsWritten++;
|
|
13788
|
+
}
|
|
13789
|
+
return { docsWritten };
|
|
13790
|
+
}
|
|
13791
|
+
function reindexFromArchive(projectPath, opts = {}) {
|
|
13792
|
+
const start = Date.now();
|
|
13793
|
+
const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
|
|
13794
|
+
const idx = openSearchIndex(projectPath);
|
|
13795
|
+
try {
|
|
13796
|
+
idx.resetArchived();
|
|
13797
|
+
let sessionsIndexed = 0;
|
|
13798
|
+
let docsWritten = 0;
|
|
13799
|
+
if (fs17.existsSync(archiveBase)) {
|
|
13800
|
+
const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
|
|
13801
|
+
for (const entry of entries) {
|
|
13802
|
+
if (!entry.isDirectory()) continue;
|
|
13803
|
+
const sessionDir = path20.join(archiveBase, entry.name);
|
|
13804
|
+
const result = indexSessionDirectory(idx, {
|
|
13805
|
+
sessionId: entry.name,
|
|
13806
|
+
sessionDir,
|
|
13807
|
+
archived: true,
|
|
13808
|
+
projectPath,
|
|
13809
|
+
...opts.fileKinds && { fileKinds: opts.fileKinds },
|
|
13810
|
+
...opts.maxBytesPerBody !== void 0 && { maxBytesPerBody: opts.maxBytesPerBody }
|
|
13811
|
+
});
|
|
13812
|
+
if (result.docsWritten > 0) sessionsIndexed++;
|
|
13813
|
+
docsWritten += result.docsWritten;
|
|
13814
|
+
}
|
|
13815
|
+
}
|
|
13816
|
+
return { sessionsIndexed, docsWritten, durationMs: Date.now() - start };
|
|
13817
|
+
} finally {
|
|
13818
|
+
idx.close();
|
|
13819
|
+
}
|
|
13820
|
+
}
|
|
13821
|
+
|
|
13822
|
+
// src/sessions/summarize.ts
|
|
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");
|
|
13827
|
+
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
13828
|
+
var SUMMARY_INPUT_FILES = [
|
|
13829
|
+
{ filename: "summary.md", kind: "summary" },
|
|
13830
|
+
{ filename: "learnings.md", kind: "learnings" },
|
|
13831
|
+
{ filename: "failures.md", kind: "failures" },
|
|
13832
|
+
{ filename: "session-sections.md", kind: "sections" }
|
|
13833
|
+
];
|
|
13834
|
+
var DEFAULT_INPUT_BUDGET_TOKENS = 16e3;
|
|
13835
|
+
var DEFAULT_TIMEOUT_MS4 = 6e4;
|
|
13836
|
+
var CHARS_PER_TOKEN = 4;
|
|
13837
|
+
var SYSTEM_PROMPT = `You produce concise, structured retrospectives of completed harness-engineering sessions.
|
|
13838
|
+
|
|
13839
|
+
Read the session's archived markdown files and emit a JSON object that conforms exactly to the provided schema. Be specific and grounded \u2014 quote artefacts (file names, skill names, error messages) verbatim when relevant. Do not invent. If a field has no content, return an empty array.`;
|
|
13840
|
+
var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-engineering session. Produce a structured summary capturing:
|
|
13841
|
+
- headline: one-sentence retrospective (\u2264 120 chars)
|
|
13842
|
+
- keyOutcomes: concrete things that shipped / decisions made (\u2264 20 strings)
|
|
13843
|
+
- openQuestions: items still open (\u2264 20 strings)
|
|
13844
|
+
- relatedSessions: other session slugs referenced (may be empty)
|
|
13845
|
+
|
|
13846
|
+
---
|
|
13847
|
+
|
|
13848
|
+
`;
|
|
13849
|
+
function readInputCorpus(archiveDir) {
|
|
13850
|
+
const parts = [];
|
|
13851
|
+
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
13852
|
+
const p = path21.join(archiveDir, filename);
|
|
13853
|
+
if (!fs18.existsSync(p)) continue;
|
|
13854
|
+
try {
|
|
13855
|
+
const content = fs18.readFileSync(p, "utf8");
|
|
13856
|
+
if (content.trim().length === 0) continue;
|
|
13857
|
+
parts.push(`## FILE: ${kind}
|
|
13858
|
+
|
|
13859
|
+
${content.trim()}`);
|
|
13860
|
+
} catch {
|
|
13861
|
+
}
|
|
13862
|
+
}
|
|
13863
|
+
return parts.join("\n\n");
|
|
13864
|
+
}
|
|
13865
|
+
function truncateForBudget(text, inputBudgetTokens) {
|
|
13866
|
+
const cap = Math.max(0, inputBudgetTokens * CHARS_PER_TOKEN);
|
|
13867
|
+
if (text.length <= cap) return text;
|
|
13868
|
+
return text.slice(0, cap) + "\n\n[TRUNCATED \u2014 input exceeded token budget]";
|
|
13869
|
+
}
|
|
13870
|
+
function renderLlmSummaryMarkdown(summary, meta) {
|
|
13871
|
+
const lines = [
|
|
13872
|
+
"---",
|
|
13873
|
+
`generatedAt: ${meta.generatedAt}`,
|
|
13874
|
+
`model: ${meta.model}`,
|
|
13875
|
+
`inputTokens: ${meta.inputTokens}`,
|
|
13876
|
+
`outputTokens: ${meta.outputTokens}`,
|
|
13877
|
+
`schemaVersion: ${meta.schemaVersion}`,
|
|
13878
|
+
"---",
|
|
13879
|
+
"",
|
|
13880
|
+
"## Headline",
|
|
13881
|
+
summary.headline,
|
|
13882
|
+
"",
|
|
13883
|
+
"## Key outcomes"
|
|
13884
|
+
];
|
|
13885
|
+
if (summary.keyOutcomes.length === 0) {
|
|
13886
|
+
lines.push("_(none)_");
|
|
13887
|
+
} else {
|
|
13888
|
+
for (const item of summary.keyOutcomes) lines.push(`- ${item}`);
|
|
13889
|
+
}
|
|
13890
|
+
lines.push("", "## Open questions");
|
|
13891
|
+
if (summary.openQuestions.length === 0) {
|
|
13892
|
+
lines.push("_(none)_");
|
|
13893
|
+
} else {
|
|
13894
|
+
for (const item of summary.openQuestions) lines.push(`- ${item}`);
|
|
13895
|
+
}
|
|
13896
|
+
lines.push("", "## Related sessions");
|
|
13897
|
+
if (summary.relatedSessions.length === 0) {
|
|
13898
|
+
lines.push("_(none)_");
|
|
13899
|
+
} else {
|
|
13900
|
+
for (const item of summary.relatedSessions) lines.push(`- ${item}`);
|
|
13901
|
+
}
|
|
13902
|
+
lines.push("");
|
|
13903
|
+
return lines.join("\n");
|
|
13904
|
+
}
|
|
13905
|
+
function writeStubMarkdown(archiveDir, reason) {
|
|
13906
|
+
const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
|
|
13907
|
+
const body = `---
|
|
13908
|
+
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
13909
|
+
schemaVersion: 1
|
|
13910
|
+
status: failed
|
|
13911
|
+
---
|
|
13912
|
+
|
|
13913
|
+
## Summary unavailable
|
|
13914
|
+
|
|
13915
|
+
- reason: ${reason}
|
|
13916
|
+
`;
|
|
13917
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13918
|
+
return filePath;
|
|
13919
|
+
}
|
|
13920
|
+
async function summarizeArchivedSession(ctx) {
|
|
13921
|
+
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
13922
|
+
if (!fs18.existsSync(ctx.archiveDir)) {
|
|
13923
|
+
return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
13924
|
+
}
|
|
13925
|
+
const corpus = readInputCorpus(ctx.archiveDir);
|
|
13926
|
+
if (corpus.trim().length === 0) {
|
|
13927
|
+
return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
13928
|
+
}
|
|
13929
|
+
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
13930
|
+
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
13931
|
+
const prompt = USER_PROMPT_PREAMBLE + truncated;
|
|
13932
|
+
const timeoutMs = ctx.config?.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
13933
|
+
const analyzeOpts = {
|
|
13934
|
+
prompt,
|
|
13935
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
13936
|
+
responseSchema: import_types32.SessionSummarySchema,
|
|
13937
|
+
...ctx.config?.model && { model: ctx.config.model }
|
|
13938
|
+
};
|
|
13939
|
+
let response;
|
|
13940
|
+
try {
|
|
13941
|
+
response = await Promise.race([
|
|
13942
|
+
ctx.provider.analyze(analyzeOpts),
|
|
13943
|
+
new Promise(
|
|
13944
|
+
(_, reject) => setTimeout(
|
|
13945
|
+
() => reject(new Error(`provider call timed out after ${timeoutMs}ms`)),
|
|
13946
|
+
timeoutMs
|
|
13947
|
+
)
|
|
13948
|
+
)
|
|
13949
|
+
]);
|
|
13950
|
+
} catch (e) {
|
|
13951
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
13952
|
+
ctx.logger?.warn?.("session summary: provider call failed", { reason });
|
|
13953
|
+
let stubPath;
|
|
13954
|
+
if (writeStubOnError) {
|
|
13955
|
+
try {
|
|
13956
|
+
stubPath = writeStubMarkdown(ctx.archiveDir, reason);
|
|
13957
|
+
} catch {
|
|
13958
|
+
}
|
|
13959
|
+
}
|
|
13960
|
+
return (0, import_types33.Err)(
|
|
13961
|
+
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
13962
|
+
);
|
|
13963
|
+
}
|
|
13964
|
+
const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
|
|
13965
|
+
if (!parsed.success) {
|
|
13966
|
+
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
13967
|
+
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
13968
|
+
if (writeStubOnError) {
|
|
13969
|
+
try {
|
|
13970
|
+
writeStubMarkdown(ctx.archiveDir, reason);
|
|
13971
|
+
} catch {
|
|
13972
|
+
}
|
|
13973
|
+
}
|
|
13974
|
+
return (0, import_types33.Err)(new Error(reason));
|
|
13975
|
+
}
|
|
13976
|
+
const meta = {
|
|
13977
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13978
|
+
model: response.model,
|
|
13979
|
+
inputTokens: response.tokenUsage.inputTokens,
|
|
13980
|
+
outputTokens: response.tokenUsage.outputTokens,
|
|
13981
|
+
schemaVersion: 1
|
|
13982
|
+
};
|
|
13983
|
+
const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
13984
|
+
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
13985
|
+
fs18.writeFileSync(filePath, body, "utf8");
|
|
13986
|
+
return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
|
|
13987
|
+
}
|
|
13988
|
+
function isSummaryEnabled(config) {
|
|
13989
|
+
if (!config) return false;
|
|
13990
|
+
if (config.enabled === false) return false;
|
|
13991
|
+
return true;
|
|
13992
|
+
}
|
|
13993
|
+
|
|
13994
|
+
// src/sessions/archive-hooks.ts
|
|
13995
|
+
var defaultLogger = {
|
|
13996
|
+
warn: (msg, meta) => console.warn(`[sessions] ${msg}`, meta)
|
|
13997
|
+
};
|
|
13998
|
+
async function runSummaryStep(opts, logger, sessionId, archiveDir) {
|
|
13999
|
+
const enabled = isSummaryEnabled(opts.config?.summary) && opts.provider != null;
|
|
14000
|
+
if (!enabled || !opts.provider) return;
|
|
14001
|
+
const ctx = {
|
|
14002
|
+
archiveDir,
|
|
14003
|
+
provider: opts.provider,
|
|
14004
|
+
...opts.config?.summary && { config: opts.config.summary },
|
|
14005
|
+
...logger && { logger }
|
|
14006
|
+
};
|
|
14007
|
+
try {
|
|
14008
|
+
const result = await summarizeArchivedSession(ctx);
|
|
14009
|
+
if (!result.ok) {
|
|
14010
|
+
logger.warn?.("session summary: failed", {
|
|
14011
|
+
sessionId,
|
|
14012
|
+
error: result.error.message
|
|
14013
|
+
});
|
|
14014
|
+
}
|
|
14015
|
+
} catch (e) {
|
|
14016
|
+
logger.warn?.("session summary: threw", {
|
|
14017
|
+
sessionId,
|
|
14018
|
+
error: e instanceof Error ? e.message : String(e)
|
|
14019
|
+
});
|
|
14020
|
+
}
|
|
14021
|
+
}
|
|
14022
|
+
function runIndexStep(opts, logger, sessionId, archiveDir) {
|
|
14023
|
+
try {
|
|
14024
|
+
const idx = openSearchIndex(opts.projectPath);
|
|
14025
|
+
try {
|
|
14026
|
+
const result = indexSessionDirectory(idx, {
|
|
14027
|
+
sessionId,
|
|
14028
|
+
sessionDir: archiveDir,
|
|
14029
|
+
archived: true,
|
|
14030
|
+
projectPath: opts.projectPath,
|
|
14031
|
+
...opts.config?.search?.indexedFileKinds && {
|
|
14032
|
+
fileKinds: opts.config.search.indexedFileKinds
|
|
14033
|
+
},
|
|
14034
|
+
...opts.config?.search?.maxIndexBytesPerFile !== void 0 && {
|
|
14035
|
+
maxBytesPerBody: opts.config.search.maxIndexBytesPerFile
|
|
14036
|
+
}
|
|
14037
|
+
});
|
|
14038
|
+
if (result.docsWritten === 0) {
|
|
14039
|
+
logger.warn?.("session index: no docs written", { sessionId, archiveDir });
|
|
14040
|
+
}
|
|
14041
|
+
} finally {
|
|
14042
|
+
idx.close();
|
|
14043
|
+
}
|
|
14044
|
+
} catch (e) {
|
|
14045
|
+
logger.warn?.("session index: failed", {
|
|
14046
|
+
sessionId,
|
|
14047
|
+
error: e instanceof Error ? e.message : String(e)
|
|
14048
|
+
});
|
|
14049
|
+
}
|
|
14050
|
+
}
|
|
14051
|
+
function buildArchiveHooks(opts) {
|
|
14052
|
+
const logger = opts.logger ?? defaultLogger;
|
|
14053
|
+
return {
|
|
14054
|
+
async onArchived({ sessionId, archiveDir }) {
|
|
14055
|
+
await runSummaryStep(opts, logger, sessionId, archiveDir);
|
|
14056
|
+
runIndexStep(opts, logger, sessionId, archiveDir);
|
|
14057
|
+
}
|
|
14058
|
+
};
|
|
14059
|
+
}
|
|
11229
14060
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11230
14061
|
0 && (module.exports = {
|
|
11231
14062
|
AnalysisArchive,
|
|
14063
|
+
BUILT_IN_TASKS,
|
|
11232
14064
|
BackendRouter,
|
|
11233
14065
|
ClaimManager,
|
|
14066
|
+
GateNotReadyError,
|
|
14067
|
+
GateRunError,
|
|
11234
14068
|
InteractionQueue,
|
|
11235
14069
|
LinearGraphQLStub,
|
|
11236
14070
|
MAX_ATTEMPTS,
|
|
@@ -11239,10 +14073,16 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11239
14073
|
Orchestrator,
|
|
11240
14074
|
OrchestratorBackendFactory,
|
|
11241
14075
|
PRDetector,
|
|
14076
|
+
PromotionError,
|
|
11242
14077
|
PromptRenderer,
|
|
11243
14078
|
RETRY_DELAYS_MS,
|
|
11244
14079
|
RoadmapTrackerAdapter,
|
|
14080
|
+
SinkConfigError,
|
|
14081
|
+
SinkRegistry,
|
|
14082
|
+
SlackSink,
|
|
14083
|
+
SqliteSearchIndex,
|
|
11245
14084
|
StreamRecorder,
|
|
14085
|
+
TaskOutputStore,
|
|
11246
14086
|
TokenStore,
|
|
11247
14087
|
WebhookQueue,
|
|
11248
14088
|
WorkflowLoader,
|
|
@@ -11250,31 +14090,49 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11250
14090
|
WorkspaceManager,
|
|
11251
14091
|
applyEvent,
|
|
11252
14092
|
artifactPresenceFromIssue,
|
|
14093
|
+
buildArchiveHooks,
|
|
11253
14094
|
calculateRetryDelay,
|
|
11254
14095
|
canDispatch,
|
|
11255
14096
|
computeRateLimitDelay,
|
|
11256
14097
|
createBackend,
|
|
11257
14098
|
createEmptyState,
|
|
11258
14099
|
detectScopeTier,
|
|
14100
|
+
emitProposalApproved,
|
|
14101
|
+
emitProposalCreated,
|
|
14102
|
+
emitProposalRejected,
|
|
11259
14103
|
extractHighlights,
|
|
11260
14104
|
extractTitlePrefix,
|
|
11261
14105
|
getAvailableSlots,
|
|
11262
14106
|
getDefaultConfig,
|
|
11263
14107
|
getPerStateCount,
|
|
14108
|
+
indexSessionDirectory,
|
|
11264
14109
|
isEligible,
|
|
14110
|
+
isSummaryEnabled,
|
|
11265
14111
|
launchTUI,
|
|
11266
14112
|
loadPublishedIndex,
|
|
11267
14113
|
migrateAgentConfig,
|
|
14114
|
+
normalizeFts5Query,
|
|
14115
|
+
openSearchIndex,
|
|
14116
|
+
promote,
|
|
11268
14117
|
reconcile,
|
|
14118
|
+
reindexFromArchive,
|
|
11269
14119
|
renderAnalysisComment,
|
|
14120
|
+
renderLlmSummaryMarkdown,
|
|
11270
14121
|
renderPRComment,
|
|
11271
14122
|
resolveEscalationConfig,
|
|
11272
14123
|
resolveOrchestratorId,
|
|
11273
14124
|
routeIssue,
|
|
14125
|
+
runGate,
|
|
11274
14126
|
savePublishedIndex,
|
|
14127
|
+
searchIndexPath,
|
|
11275
14128
|
selectCandidates,
|
|
11276
14129
|
sortCandidates,
|
|
14130
|
+
summarizeArchivedSession,
|
|
11277
14131
|
syncMain,
|
|
11278
14132
|
triageIssue,
|
|
11279
|
-
|
|
14133
|
+
truncateForBudget,
|
|
14134
|
+
validateCustomTasks,
|
|
14135
|
+
validateWorkflowConfig,
|
|
14136
|
+
wireNotificationSinks,
|
|
14137
|
+
wrapAsEnvelope
|
|
11280
14138
|
});
|