@harness-engineering/orchestrator 0.4.5 → 0.5.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 +287 -4
- package/dist/index.d.ts +287 -4
- package/dist/index.js +1529 -97
- package/dist/index.mjs +1497 -73
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -44,6 +44,10 @@ __export(index_exports, {
|
|
|
44
44
|
PromptRenderer: () => PromptRenderer,
|
|
45
45
|
RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
|
|
46
46
|
RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
|
|
47
|
+
SinkConfigError: () => SinkConfigError,
|
|
48
|
+
SinkRegistry: () => SinkRegistry,
|
|
49
|
+
SlackSink: () => SlackSink,
|
|
50
|
+
SqliteSearchIndex: () => SqliteSearchIndex,
|
|
47
51
|
StreamRecorder: () => StreamRecorder,
|
|
48
52
|
TokenStore: () => TokenStore,
|
|
49
53
|
WebhookQueue: () => WebhookQueue,
|
|
@@ -52,6 +56,7 @@ __export(index_exports, {
|
|
|
52
56
|
WorkspaceManager: () => WorkspaceManager,
|
|
53
57
|
applyEvent: () => applyEvent,
|
|
54
58
|
artifactPresenceFromIssue: () => artifactPresenceFromIssue,
|
|
59
|
+
buildArchiveHooks: () => buildArchiveHooks,
|
|
55
60
|
calculateRetryDelay: () => calculateRetryDelay,
|
|
56
61
|
canDispatch: () => canDispatch,
|
|
57
62
|
computeRateLimitDelay: () => computeRateLimitDelay,
|
|
@@ -63,22 +68,33 @@ __export(index_exports, {
|
|
|
63
68
|
getAvailableSlots: () => getAvailableSlots,
|
|
64
69
|
getDefaultConfig: () => getDefaultConfig,
|
|
65
70
|
getPerStateCount: () => getPerStateCount,
|
|
71
|
+
indexSessionDirectory: () => indexSessionDirectory,
|
|
66
72
|
isEligible: () => isEligible,
|
|
73
|
+
isSummaryEnabled: () => isSummaryEnabled,
|
|
67
74
|
launchTUI: () => launchTUI,
|
|
68
75
|
loadPublishedIndex: () => loadPublishedIndex,
|
|
69
76
|
migrateAgentConfig: () => migrateAgentConfig,
|
|
77
|
+
normalizeFts5Query: () => normalizeFts5Query,
|
|
78
|
+
openSearchIndex: () => openSearchIndex,
|
|
70
79
|
reconcile: () => reconcile,
|
|
80
|
+
reindexFromArchive: () => reindexFromArchive,
|
|
71
81
|
renderAnalysisComment: () => renderAnalysisComment,
|
|
82
|
+
renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
|
|
72
83
|
renderPRComment: () => renderPRComment,
|
|
73
84
|
resolveEscalationConfig: () => resolveEscalationConfig,
|
|
74
85
|
resolveOrchestratorId: () => resolveOrchestratorId,
|
|
75
86
|
routeIssue: () => routeIssue,
|
|
76
87
|
savePublishedIndex: () => savePublishedIndex,
|
|
88
|
+
searchIndexPath: () => searchIndexPath,
|
|
77
89
|
selectCandidates: () => selectCandidates,
|
|
78
90
|
sortCandidates: () => sortCandidates,
|
|
91
|
+
summarizeArchivedSession: () => summarizeArchivedSession,
|
|
79
92
|
syncMain: () => syncMain,
|
|
80
93
|
triageIssue: () => triageIssue,
|
|
81
|
-
|
|
94
|
+
truncateForBudget: () => truncateForBudget,
|
|
95
|
+
validateWorkflowConfig: () => validateWorkflowConfig,
|
|
96
|
+
wireNotificationSinks: () => wireNotificationSinks,
|
|
97
|
+
wrapAsEnvelope: () => wrapAsEnvelope
|
|
82
98
|
});
|
|
83
99
|
module.exports = __toCommonJS(index_exports);
|
|
84
100
|
|
|
@@ -1951,11 +1967,11 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
|
|
|
1951
1967
|
function crossFieldRoutingIssues(backends, routing) {
|
|
1952
1968
|
const issues = [];
|
|
1953
1969
|
const names = new Set(Object.keys(backends));
|
|
1954
|
-
const checkRef = (
|
|
1970
|
+
const checkRef = (path19, name) => {
|
|
1955
1971
|
if (name !== void 0 && !names.has(name)) {
|
|
1956
1972
|
issues.push({
|
|
1957
|
-
path:
|
|
1958
|
-
message: `routing.${
|
|
1973
|
+
path: path19,
|
|
1974
|
+
message: `routing.${path19.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
|
|
1959
1975
|
});
|
|
1960
1976
|
}
|
|
1961
1977
|
};
|
|
@@ -2735,7 +2751,7 @@ var PromptRenderer = class {
|
|
|
2735
2751
|
// src/orchestrator.ts
|
|
2736
2752
|
var import_node_events = require("events");
|
|
2737
2753
|
var path16 = __toESM(require("path"));
|
|
2738
|
-
var
|
|
2754
|
+
var import_node_crypto16 = require("crypto");
|
|
2739
2755
|
var import_core11 = require("@harness-engineering/core");
|
|
2740
2756
|
|
|
2741
2757
|
// src/intelligence/pipeline-runner.ts
|
|
@@ -3701,11 +3717,11 @@ function detectLegacyFields(agent) {
|
|
|
3701
3717
|
}
|
|
3702
3718
|
function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
|
|
3703
3719
|
const warnings = [];
|
|
3704
|
-
for (const
|
|
3705
|
-
if (CASE1_ALWAYS_SUPPRESS.has(
|
|
3706
|
-
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(
|
|
3720
|
+
for (const path19 of presentLegacy) {
|
|
3721
|
+
if (CASE1_ALWAYS_SUPPRESS.has(path19)) continue;
|
|
3722
|
+
if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path19)) continue;
|
|
3707
3723
|
warnings.push(
|
|
3708
|
-
`Ignoring legacy field '${
|
|
3724
|
+
`Ignoring legacy field '${path19}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
|
|
3709
3725
|
);
|
|
3710
3726
|
}
|
|
3711
3727
|
return warnings;
|
|
@@ -3733,7 +3749,7 @@ function migrateAgentConfig(agent) {
|
|
|
3733
3749
|
}
|
|
3734
3750
|
const { backends, routing } = synthesizeBackendsAndRouting(agent);
|
|
3735
3751
|
const warnings = presentLegacy.map(
|
|
3736
|
-
(
|
|
3752
|
+
(path19) => `Deprecated config field '${path19}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
|
|
3737
3753
|
);
|
|
3738
3754
|
return {
|
|
3739
3755
|
config: { ...agent, backends, routing },
|
|
@@ -3822,6 +3838,10 @@ var BackendRouter = class {
|
|
|
3822
3838
|
const intel = this.routing.intelligence;
|
|
3823
3839
|
return intel?.[useCase.layer] ?? this.routing.default;
|
|
3824
3840
|
}
|
|
3841
|
+
case "isolation": {
|
|
3842
|
+
const iso = this.routing.isolation;
|
|
3843
|
+
return iso?.[useCase.tier] ?? this.routing.default;
|
|
3844
|
+
}
|
|
3825
3845
|
case "maintenance":
|
|
3826
3846
|
case "chat":
|
|
3827
3847
|
return this.routing.default;
|
|
@@ -3845,8 +3865,8 @@ var BackendRouter = class {
|
|
|
3845
3865
|
validateReferences() {
|
|
3846
3866
|
const known = new Set(Object.keys(this.backends));
|
|
3847
3867
|
const missing = [];
|
|
3848
|
-
const check = (
|
|
3849
|
-
if (name !== void 0 && !known.has(name)) missing.push({ path:
|
|
3868
|
+
const check = (path19, name) => {
|
|
3869
|
+
if (name !== void 0 && !known.has(name)) missing.push({ path: path19, name });
|
|
3850
3870
|
};
|
|
3851
3871
|
check("default", this.routing.default);
|
|
3852
3872
|
check("quick-fix", this.routing["quick-fix"]);
|
|
@@ -3855,8 +3875,11 @@ var BackendRouter = class {
|
|
|
3855
3875
|
check("diagnostic", this.routing.diagnostic);
|
|
3856
3876
|
check("intelligence.sel", this.routing.intelligence?.sel);
|
|
3857
3877
|
check("intelligence.pesl", this.routing.intelligence?.pesl);
|
|
3878
|
+
check("isolation.none", this.routing.isolation?.none);
|
|
3879
|
+
check("isolation.container", this.routing.isolation?.container);
|
|
3880
|
+
check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
|
|
3858
3881
|
if (missing.length > 0) {
|
|
3859
|
-
const detail = missing.map(({ path:
|
|
3882
|
+
const detail = missing.map(({ path: path19, name }) => `routing.${path19} -> '${name}'`).join("; ");
|
|
3860
3883
|
const known_ = [...known].join(", ") || "(none)";
|
|
3861
3884
|
throw new Error(
|
|
3862
3885
|
`BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
|
|
@@ -4998,6 +5021,541 @@ var PiBackend = class {
|
|
|
4998
5021
|
}
|
|
4999
5022
|
};
|
|
5000
5023
|
|
|
5024
|
+
// src/agent/backends/ssh.ts
|
|
5025
|
+
var import_node_child_process5 = require("child_process");
|
|
5026
|
+
var import_types16 = require("@harness-engineering/types");
|
|
5027
|
+
var DEFAULT_TIMEOUT_MS2 = 9e4;
|
|
5028
|
+
var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
|
|
5029
|
+
var SshBackend = class {
|
|
5030
|
+
name = "ssh";
|
|
5031
|
+
config;
|
|
5032
|
+
spawnImpl;
|
|
5033
|
+
constructor(config) {
|
|
5034
|
+
if (!config.host || typeof config.host !== "string") {
|
|
5035
|
+
throw new Error("SshBackend: `host` is required");
|
|
5036
|
+
}
|
|
5037
|
+
if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
|
|
5038
|
+
throw new Error(
|
|
5039
|
+
`SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
|
|
5040
|
+
);
|
|
5041
|
+
}
|
|
5042
|
+
if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
|
|
5043
|
+
throw new Error("SshBackend: `remoteCommand` is required");
|
|
5044
|
+
}
|
|
5045
|
+
if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
|
|
5046
|
+
throw new Error(`SshBackend: invalid user '${config.user}'`);
|
|
5047
|
+
}
|
|
5048
|
+
this.config = {
|
|
5049
|
+
host: config.host,
|
|
5050
|
+
remoteCommand: config.remoteCommand,
|
|
5051
|
+
sshBinary: config.sshBinary ?? "ssh",
|
|
5052
|
+
sshOptions: config.sshOptions ?? [],
|
|
5053
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
|
|
5054
|
+
...config.user !== void 0 ? { user: config.user } : {},
|
|
5055
|
+
...config.port !== void 0 ? { port: config.port } : {},
|
|
5056
|
+
...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
|
|
5057
|
+
};
|
|
5058
|
+
this.spawnImpl = config.spawnImpl ?? import_node_child_process5.spawn;
|
|
5059
|
+
}
|
|
5060
|
+
/**
|
|
5061
|
+
* Builds the argv passed to the `ssh` binary. Exported as a method on
|
|
5062
|
+
* the class so tests can assert the exact shape without spawning.
|
|
5063
|
+
*
|
|
5064
|
+
* Layout: `[options..., target, '--', remoteCommand]`
|
|
5065
|
+
*/
|
|
5066
|
+
buildSshArgs() {
|
|
5067
|
+
const args = [];
|
|
5068
|
+
if (this.config.identityFile) {
|
|
5069
|
+
args.push("-i", this.config.identityFile);
|
|
5070
|
+
}
|
|
5071
|
+
if (this.config.port !== void 0) {
|
|
5072
|
+
args.push("-p", String(this.config.port));
|
|
5073
|
+
}
|
|
5074
|
+
args.push("-o", "BatchMode=yes");
|
|
5075
|
+
for (const opt of this.config.sshOptions) {
|
|
5076
|
+
args.push("-o", opt);
|
|
5077
|
+
}
|
|
5078
|
+
const target = this.config.user ? `${this.config.user}@${this.config.host}` : this.config.host;
|
|
5079
|
+
args.push(target);
|
|
5080
|
+
args.push("--");
|
|
5081
|
+
args.push(this.config.remoteCommand);
|
|
5082
|
+
return args;
|
|
5083
|
+
}
|
|
5084
|
+
async startSession(params) {
|
|
5085
|
+
const session = {
|
|
5086
|
+
sessionId: `ssh-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5087
|
+
workspacePath: params.workspacePath,
|
|
5088
|
+
backendName: this.name,
|
|
5089
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5090
|
+
...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
|
|
5091
|
+
};
|
|
5092
|
+
return (0, import_types16.Ok)(session);
|
|
5093
|
+
}
|
|
5094
|
+
async *runTurn(session, params) {
|
|
5095
|
+
const child = this.spawnImpl(this.config.sshBinary, this.buildSshArgs(), {
|
|
5096
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5097
|
+
});
|
|
5098
|
+
const payload = JSON.stringify({
|
|
5099
|
+
kind: "turn",
|
|
5100
|
+
prompt: params.prompt,
|
|
5101
|
+
isContinuation: params.isContinuation,
|
|
5102
|
+
systemPrompt: session.systemPrompt
|
|
5103
|
+
});
|
|
5104
|
+
try {
|
|
5105
|
+
child.stdin.write(payload + "\n");
|
|
5106
|
+
child.stdin.end();
|
|
5107
|
+
} catch (err) {
|
|
5108
|
+
const message = err instanceof Error ? err.message : "failed to write to ssh stdin";
|
|
5109
|
+
try {
|
|
5110
|
+
child.kill("SIGTERM");
|
|
5111
|
+
} catch {
|
|
5112
|
+
}
|
|
5113
|
+
return errResult(session.sessionId, message);
|
|
5114
|
+
}
|
|
5115
|
+
const timeout = setTimeout(() => {
|
|
5116
|
+
try {
|
|
5117
|
+
child.kill("SIGTERM");
|
|
5118
|
+
} catch {
|
|
5119
|
+
}
|
|
5120
|
+
}, this.config.timeoutMs);
|
|
5121
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5122
|
+
let success = true;
|
|
5123
|
+
let lastError;
|
|
5124
|
+
try {
|
|
5125
|
+
for await (const line of readLines(child.stdout)) {
|
|
5126
|
+
let event;
|
|
5127
|
+
try {
|
|
5128
|
+
event = parseEvent(line, session.sessionId);
|
|
5129
|
+
} catch (err) {
|
|
5130
|
+
const message = err instanceof Error ? err.message : "unparseable ssh event";
|
|
5131
|
+
success = false;
|
|
5132
|
+
lastError = message;
|
|
5133
|
+
break;
|
|
5134
|
+
}
|
|
5135
|
+
if (!event) continue;
|
|
5136
|
+
if (event.usage) finalUsage = event.usage;
|
|
5137
|
+
if (event.type === "error" && typeof event.content === "string") {
|
|
5138
|
+
lastError = event.content;
|
|
5139
|
+
success = false;
|
|
5140
|
+
}
|
|
5141
|
+
yield event;
|
|
5142
|
+
}
|
|
5143
|
+
const exitCode = await waitForExit(child);
|
|
5144
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
5145
|
+
success = false;
|
|
5146
|
+
lastError = lastError ?? `ssh exited with code ${exitCode}`;
|
|
5147
|
+
}
|
|
5148
|
+
} finally {
|
|
5149
|
+
clearTimeout(timeout);
|
|
5150
|
+
}
|
|
5151
|
+
return {
|
|
5152
|
+
success,
|
|
5153
|
+
sessionId: session.sessionId,
|
|
5154
|
+
usage: finalUsage,
|
|
5155
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
async stopSession(_session) {
|
|
5159
|
+
return (0, import_types16.Ok)(void 0);
|
|
5160
|
+
}
|
|
5161
|
+
async healthCheck() {
|
|
5162
|
+
const args = [...this.buildSshArgs()];
|
|
5163
|
+
args[args.length - 1] = "true";
|
|
5164
|
+
return new Promise((resolve6) => {
|
|
5165
|
+
let child;
|
|
5166
|
+
try {
|
|
5167
|
+
child = this.spawnImpl(this.config.sshBinary, args, {
|
|
5168
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
5169
|
+
});
|
|
5170
|
+
} catch (err) {
|
|
5171
|
+
resolve6(
|
|
5172
|
+
(0, import_types16.Err)({
|
|
5173
|
+
category: "agent_not_found",
|
|
5174
|
+
message: err instanceof Error ? err.message : "failed to spawn ssh"
|
|
5175
|
+
})
|
|
5176
|
+
);
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
let stderr = "";
|
|
5180
|
+
child.stderr?.on("data", (chunk) => {
|
|
5181
|
+
stderr += chunk.toString();
|
|
5182
|
+
});
|
|
5183
|
+
const timer = setTimeout(() => {
|
|
5184
|
+
try {
|
|
5185
|
+
child.kill("SIGTERM");
|
|
5186
|
+
} catch {
|
|
5187
|
+
}
|
|
5188
|
+
}, this.config.timeoutMs);
|
|
5189
|
+
child.on("close", (code) => {
|
|
5190
|
+
clearTimeout(timer);
|
|
5191
|
+
if (code === 0) {
|
|
5192
|
+
resolve6((0, import_types16.Ok)(void 0));
|
|
5193
|
+
} else {
|
|
5194
|
+
resolve6(
|
|
5195
|
+
(0, import_types16.Err)({
|
|
5196
|
+
category: "agent_not_found",
|
|
5197
|
+
message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
|
|
5198
|
+
})
|
|
5199
|
+
);
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
5202
|
+
child.on("error", (err) => {
|
|
5203
|
+
clearTimeout(timer);
|
|
5204
|
+
resolve6((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
|
|
5205
|
+
});
|
|
5206
|
+
});
|
|
5207
|
+
}
|
|
5208
|
+
};
|
|
5209
|
+
function errResult(sessionId, message) {
|
|
5210
|
+
return {
|
|
5211
|
+
success: false,
|
|
5212
|
+
sessionId,
|
|
5213
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5214
|
+
error: message
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
function parseEvent(line, sessionId) {
|
|
5218
|
+
const trimmed = line.trim();
|
|
5219
|
+
if (trimmed.length === 0) return null;
|
|
5220
|
+
const raw = JSON.parse(trimmed);
|
|
5221
|
+
if (typeof raw.type !== "string") {
|
|
5222
|
+
throw new Error(`ssh event missing 'type': ${trimmed.slice(0, 200)}`);
|
|
5223
|
+
}
|
|
5224
|
+
const ev = {
|
|
5225
|
+
type: raw.type,
|
|
5226
|
+
timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5227
|
+
sessionId
|
|
5228
|
+
};
|
|
5229
|
+
if (typeof raw.subtype === "string") ev.subtype = raw.subtype;
|
|
5230
|
+
if (raw.content !== void 0) ev.content = raw.content;
|
|
5231
|
+
if (isUsage(raw.usage)) ev.usage = raw.usage;
|
|
5232
|
+
return ev;
|
|
5233
|
+
}
|
|
5234
|
+
function isUsage(u) {
|
|
5235
|
+
if (!u || typeof u !== "object") return false;
|
|
5236
|
+
const o = u;
|
|
5237
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5238
|
+
}
|
|
5239
|
+
async function* readLines(stream) {
|
|
5240
|
+
let buffer = "";
|
|
5241
|
+
for await (const chunk of stream) {
|
|
5242
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5243
|
+
let idx;
|
|
5244
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5245
|
+
yield buffer.slice(0, idx);
|
|
5246
|
+
buffer = buffer.slice(idx + 1);
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
if (buffer.length > 0) yield buffer;
|
|
5250
|
+
}
|
|
5251
|
+
function waitForExit(child) {
|
|
5252
|
+
return new Promise((resolve6) => {
|
|
5253
|
+
if (child.exitCode !== null) {
|
|
5254
|
+
resolve6(child.exitCode);
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
child.once("close", (code) => resolve6(code));
|
|
5258
|
+
child.once("error", () => resolve6(null));
|
|
5259
|
+
});
|
|
5260
|
+
}
|
|
5261
|
+
|
|
5262
|
+
// src/agent/backends/serverless.ts
|
|
5263
|
+
var import_node_child_process6 = require("child_process");
|
|
5264
|
+
var import_types17 = require("@harness-engineering/types");
|
|
5265
|
+
var ServerlessBackend = class {
|
|
5266
|
+
handles = /* @__PURE__ */ new Map();
|
|
5267
|
+
async startSession(params) {
|
|
5268
|
+
const start = await this.coldStart(params);
|
|
5269
|
+
if (!start.ok) return start;
|
|
5270
|
+
const session = {
|
|
5271
|
+
sessionId: `${this.name}-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
5272
|
+
workspacePath: params.workspacePath,
|
|
5273
|
+
backendName: this.name,
|
|
5274
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5275
|
+
};
|
|
5276
|
+
this.handles.set(session.sessionId, start.value);
|
|
5277
|
+
return (0, import_types17.Ok)(session);
|
|
5278
|
+
}
|
|
5279
|
+
async *runTurn(session, params) {
|
|
5280
|
+
const handle = this.handles.get(session.sessionId);
|
|
5281
|
+
if (!handle) {
|
|
5282
|
+
return {
|
|
5283
|
+
success: false,
|
|
5284
|
+
sessionId: session.sessionId,
|
|
5285
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5286
|
+
error: `no serverless handle for session ${session.sessionId}`
|
|
5287
|
+
};
|
|
5288
|
+
}
|
|
5289
|
+
return yield* this.runOnHandle(handle, params, session);
|
|
5290
|
+
}
|
|
5291
|
+
async stopSession(session) {
|
|
5292
|
+
const handle = this.handles.get(session.sessionId);
|
|
5293
|
+
if (!handle) return (0, import_types17.Ok)(void 0);
|
|
5294
|
+
this.handles.delete(session.sessionId);
|
|
5295
|
+
return this.teardown(handle);
|
|
5296
|
+
}
|
|
5297
|
+
};
|
|
5298
|
+
var FORBIDDEN_IMAGE_CHARS = /[;&|`$()\n\r<>]/;
|
|
5299
|
+
var BLOCKED_DOCKER_FLAGS = [
|
|
5300
|
+
"--privileged",
|
|
5301
|
+
"--cap-add",
|
|
5302
|
+
"--security-opt",
|
|
5303
|
+
"--pid",
|
|
5304
|
+
"--ipc",
|
|
5305
|
+
"--userns"
|
|
5306
|
+
];
|
|
5307
|
+
var DEFAULT_OCI_TIMEOUT_MS = 9e4;
|
|
5308
|
+
var OciServerlessBackend = class extends ServerlessBackend {
|
|
5309
|
+
name = "serverless:oci";
|
|
5310
|
+
config;
|
|
5311
|
+
spawnImpl;
|
|
5312
|
+
envSource;
|
|
5313
|
+
constructor(config) {
|
|
5314
|
+
super();
|
|
5315
|
+
if (!config.image || typeof config.image !== "string") {
|
|
5316
|
+
throw new Error("OciServerlessBackend: `image` is required");
|
|
5317
|
+
}
|
|
5318
|
+
if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
|
|
5319
|
+
throw new Error(
|
|
5320
|
+
`OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
|
|
5321
|
+
);
|
|
5322
|
+
}
|
|
5323
|
+
this.config = {
|
|
5324
|
+
image: config.image,
|
|
5325
|
+
pullPolicy: config.pullPolicy ?? "if-not-present",
|
|
5326
|
+
runtime: config.runtime ?? "docker",
|
|
5327
|
+
envPassthrough: config.envPassthrough ?? [],
|
|
5328
|
+
timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
|
|
5329
|
+
extraArgs: sanitizeExtraArgs(config.extraArgs),
|
|
5330
|
+
...config.registry !== void 0 ? { registry: config.registry } : {}
|
|
5331
|
+
};
|
|
5332
|
+
this.spawnImpl = config.spawnImpl ?? import_node_child_process6.spawn;
|
|
5333
|
+
this.envSource = config.envSource ?? process.env;
|
|
5334
|
+
}
|
|
5335
|
+
/** Builds the argv for `docker run -d ...`. Exposed for tests. */
|
|
5336
|
+
buildRunArgs() {
|
|
5337
|
+
const env = this.collectEnv();
|
|
5338
|
+
const args = ["run", "-d", "--rm"];
|
|
5339
|
+
for (const [k, v] of Object.entries(env)) {
|
|
5340
|
+
args.push("-e", `${k}=${v}`);
|
|
5341
|
+
}
|
|
5342
|
+
for (const ea of this.config.extraArgs) {
|
|
5343
|
+
args.push(ea);
|
|
5344
|
+
}
|
|
5345
|
+
args.push("--");
|
|
5346
|
+
args.push(this.config.image);
|
|
5347
|
+
return args;
|
|
5348
|
+
}
|
|
5349
|
+
/** Builds the argv for `docker exec <id> -- agent`. Exposed for tests. */
|
|
5350
|
+
buildExecArgs(handleId) {
|
|
5351
|
+
return ["exec", "-i", handleId, "/agent"];
|
|
5352
|
+
}
|
|
5353
|
+
async coldStart(_params) {
|
|
5354
|
+
if (this.config.pullPolicy === "always") {
|
|
5355
|
+
const pull = await this.runOneShot(this.config.runtime, ["pull", this.config.image]);
|
|
5356
|
+
if (!pull.ok) return pull;
|
|
5357
|
+
}
|
|
5358
|
+
const result = await this.runOneShot(this.config.runtime, this.buildRunArgs());
|
|
5359
|
+
if (!result.ok) return result;
|
|
5360
|
+
const id = result.value.trim().split(/\s+/)[0] ?? "";
|
|
5361
|
+
if (!id) {
|
|
5362
|
+
return (0, import_types17.Err)({
|
|
5363
|
+
category: "response_error",
|
|
5364
|
+
message: "OciServerlessBackend: empty container id from runtime"
|
|
5365
|
+
});
|
|
5366
|
+
}
|
|
5367
|
+
return (0, import_types17.Ok)({ id, adapter: this.name });
|
|
5368
|
+
}
|
|
5369
|
+
async *runOnHandle(handle, params, session) {
|
|
5370
|
+
const child = this.spawnImpl(this.config.runtime, this.buildExecArgs(handle.id), {
|
|
5371
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
5372
|
+
});
|
|
5373
|
+
const payload = JSON.stringify({
|
|
5374
|
+
kind: "turn",
|
|
5375
|
+
prompt: params.prompt,
|
|
5376
|
+
isContinuation: params.isContinuation
|
|
5377
|
+
});
|
|
5378
|
+
try {
|
|
5379
|
+
child.stdin.write(payload + "\n");
|
|
5380
|
+
child.stdin.end();
|
|
5381
|
+
} catch (err) {
|
|
5382
|
+
const message = err instanceof Error ? err.message : "failed to write to docker stdin";
|
|
5383
|
+
return turnFailure(session.sessionId, message);
|
|
5384
|
+
}
|
|
5385
|
+
const timeout = setTimeout(() => {
|
|
5386
|
+
try {
|
|
5387
|
+
child.kill("SIGTERM");
|
|
5388
|
+
} catch {
|
|
5389
|
+
}
|
|
5390
|
+
}, this.config.timeoutMs);
|
|
5391
|
+
let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
|
|
5392
|
+
let success = true;
|
|
5393
|
+
let lastError;
|
|
5394
|
+
try {
|
|
5395
|
+
for await (const line of readLines2(child.stdout)) {
|
|
5396
|
+
const ev = tryParseEvent(line, session.sessionId);
|
|
5397
|
+
if (!ev) continue;
|
|
5398
|
+
if (ev.usage) finalUsage = ev.usage;
|
|
5399
|
+
if (ev.type === "error" && typeof ev.content === "string") {
|
|
5400
|
+
success = false;
|
|
5401
|
+
lastError = ev.content;
|
|
5402
|
+
}
|
|
5403
|
+
yield ev;
|
|
5404
|
+
}
|
|
5405
|
+
const code = await waitForExit2(child);
|
|
5406
|
+
if (code !== 0 && code !== null) {
|
|
5407
|
+
success = false;
|
|
5408
|
+
lastError = lastError ?? `runtime exec exited with code ${code}`;
|
|
5409
|
+
}
|
|
5410
|
+
} finally {
|
|
5411
|
+
clearTimeout(timeout);
|
|
5412
|
+
}
|
|
5413
|
+
return {
|
|
5414
|
+
success,
|
|
5415
|
+
sessionId: session.sessionId,
|
|
5416
|
+
usage: finalUsage,
|
|
5417
|
+
...lastError !== void 0 ? { error: lastError } : {}
|
|
5418
|
+
};
|
|
5419
|
+
}
|
|
5420
|
+
async teardown(handle) {
|
|
5421
|
+
if (handle.adapter !== this.name) {
|
|
5422
|
+
return (0, import_types17.Err)({
|
|
5423
|
+
category: "response_error",
|
|
5424
|
+
message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
5427
|
+
const stop = await this.runOneShot(this.config.runtime, ["stop", handle.id]);
|
|
5428
|
+
if (!stop.ok) return stop;
|
|
5429
|
+
return (0, import_types17.Ok)(void 0);
|
|
5430
|
+
}
|
|
5431
|
+
async healthCheck() {
|
|
5432
|
+
return mapOk(
|
|
5433
|
+
await this.runOneShot(this.config.runtime, ["version", "--format", "{{.Server.Version}}"])
|
|
5434
|
+
);
|
|
5435
|
+
}
|
|
5436
|
+
collectEnv() {
|
|
5437
|
+
const out = {};
|
|
5438
|
+
for (const key of this.config.envPassthrough) {
|
|
5439
|
+
const val = this.envSource[key];
|
|
5440
|
+
if (typeof val === "string") out[key] = val;
|
|
5441
|
+
}
|
|
5442
|
+
return out;
|
|
5443
|
+
}
|
|
5444
|
+
runOneShot(binary, args) {
|
|
5445
|
+
return new Promise((resolve6) => {
|
|
5446
|
+
let child;
|
|
5447
|
+
try {
|
|
5448
|
+
child = this.spawnImpl(binary, args, {
|
|
5449
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5450
|
+
});
|
|
5451
|
+
} catch (err) {
|
|
5452
|
+
resolve6(
|
|
5453
|
+
(0, import_types17.Err)({
|
|
5454
|
+
category: "agent_not_found",
|
|
5455
|
+
message: err instanceof Error ? err.message : "failed to spawn runtime"
|
|
5456
|
+
})
|
|
5457
|
+
);
|
|
5458
|
+
return;
|
|
5459
|
+
}
|
|
5460
|
+
let stdout = "";
|
|
5461
|
+
let stderr = "";
|
|
5462
|
+
child.stdout?.on("data", (chunk) => {
|
|
5463
|
+
stdout += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5464
|
+
});
|
|
5465
|
+
child.stderr?.on("data", (chunk) => {
|
|
5466
|
+
stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5467
|
+
});
|
|
5468
|
+
const timer = setTimeout(() => {
|
|
5469
|
+
try {
|
|
5470
|
+
child.kill("SIGTERM");
|
|
5471
|
+
} catch {
|
|
5472
|
+
}
|
|
5473
|
+
}, this.config.timeoutMs);
|
|
5474
|
+
child.on("close", (code) => {
|
|
5475
|
+
clearTimeout(timer);
|
|
5476
|
+
if (code === 0) {
|
|
5477
|
+
resolve6((0, import_types17.Ok)(stdout));
|
|
5478
|
+
} else {
|
|
5479
|
+
resolve6(
|
|
5480
|
+
(0, import_types17.Err)({
|
|
5481
|
+
category: "response_error",
|
|
5482
|
+
message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
|
|
5483
|
+
})
|
|
5484
|
+
);
|
|
5485
|
+
}
|
|
5486
|
+
});
|
|
5487
|
+
child.on("error", (err) => {
|
|
5488
|
+
clearTimeout(timer);
|
|
5489
|
+
resolve6((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
|
|
5490
|
+
});
|
|
5491
|
+
});
|
|
5492
|
+
}
|
|
5493
|
+
};
|
|
5494
|
+
function sanitizeExtraArgs(extraArgs) {
|
|
5495
|
+
if (!extraArgs) return [];
|
|
5496
|
+
return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5497
|
+
}
|
|
5498
|
+
function mapOk(r) {
|
|
5499
|
+
return r.ok ? (0, import_types17.Ok)(void 0) : r;
|
|
5500
|
+
}
|
|
5501
|
+
function turnFailure(sessionId, message) {
|
|
5502
|
+
return {
|
|
5503
|
+
success: false,
|
|
5504
|
+
sessionId,
|
|
5505
|
+
usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
5506
|
+
error: message
|
|
5507
|
+
};
|
|
5508
|
+
}
|
|
5509
|
+
function tryParseEvent(line, sessionId) {
|
|
5510
|
+
const trimmed = line.trim();
|
|
5511
|
+
if (!trimmed) return null;
|
|
5512
|
+
let raw;
|
|
5513
|
+
try {
|
|
5514
|
+
raw = JSON.parse(trimmed);
|
|
5515
|
+
} catch {
|
|
5516
|
+
return null;
|
|
5517
|
+
}
|
|
5518
|
+
if (!raw || typeof raw !== "object") return null;
|
|
5519
|
+
const o = raw;
|
|
5520
|
+
if (typeof o.type !== "string") return null;
|
|
5521
|
+
const ev = {
|
|
5522
|
+
type: o.type,
|
|
5523
|
+
timestamp: typeof o.timestamp === "string" ? o.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
|
|
5524
|
+
sessionId
|
|
5525
|
+
};
|
|
5526
|
+
if (typeof o.subtype === "string") ev.subtype = o.subtype;
|
|
5527
|
+
if (o.content !== void 0) ev.content = o.content;
|
|
5528
|
+
if (isUsage2(o.usage)) ev.usage = o.usage;
|
|
5529
|
+
return ev;
|
|
5530
|
+
}
|
|
5531
|
+
function isUsage2(u) {
|
|
5532
|
+
if (!u || typeof u !== "object") return false;
|
|
5533
|
+
const o = u;
|
|
5534
|
+
return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
|
|
5535
|
+
}
|
|
5536
|
+
async function* readLines2(stream) {
|
|
5537
|
+
let buffer = "";
|
|
5538
|
+
for await (const chunk of stream) {
|
|
5539
|
+
buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
|
|
5540
|
+
let idx;
|
|
5541
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
5542
|
+
yield buffer.slice(0, idx);
|
|
5543
|
+
buffer = buffer.slice(idx + 1);
|
|
5544
|
+
}
|
|
5545
|
+
}
|
|
5546
|
+
if (buffer.length > 0) yield buffer;
|
|
5547
|
+
}
|
|
5548
|
+
function waitForExit2(child) {
|
|
5549
|
+
return new Promise((resolve6) => {
|
|
5550
|
+
if (child.exitCode !== null) {
|
|
5551
|
+
resolve6(child.exitCode);
|
|
5552
|
+
return;
|
|
5553
|
+
}
|
|
5554
|
+
child.once("close", (code) => resolve6(code));
|
|
5555
|
+
child.once("error", () => resolve6(null));
|
|
5556
|
+
});
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5001
5559
|
// src/agent/backend-factory.ts
|
|
5002
5560
|
function makeGetModel(model) {
|
|
5003
5561
|
if (typeof model === "string") return () => model;
|
|
@@ -5047,6 +5605,35 @@ function createBackend(def, options = {}) {
|
|
|
5047
5605
|
...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
|
|
5048
5606
|
});
|
|
5049
5607
|
}
|
|
5608
|
+
case "ssh": {
|
|
5609
|
+
return new SshBackend({
|
|
5610
|
+
host: def.host,
|
|
5611
|
+
remoteCommand: def.remoteCommand,
|
|
5612
|
+
...def.user !== void 0 ? { user: def.user } : {},
|
|
5613
|
+
...def.port !== void 0 ? { port: def.port } : {},
|
|
5614
|
+
...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
|
|
5615
|
+
...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
|
|
5616
|
+
...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
|
|
5617
|
+
});
|
|
5618
|
+
}
|
|
5619
|
+
case "serverless": {
|
|
5620
|
+
switch (def.adapter) {
|
|
5621
|
+
case "oci":
|
|
5622
|
+
return new OciServerlessBackend({
|
|
5623
|
+
image: def.image,
|
|
5624
|
+
...def.registry !== void 0 ? { registry: def.registry } : {},
|
|
5625
|
+
...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
|
|
5626
|
+
...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
|
|
5627
|
+
...def.runtime !== void 0 ? { runtime: def.runtime } : {}
|
|
5628
|
+
});
|
|
5629
|
+
default: {
|
|
5630
|
+
const exhaustive = def.adapter;
|
|
5631
|
+
throw new Error(
|
|
5632
|
+
`createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
|
|
5633
|
+
);
|
|
5634
|
+
}
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5050
5637
|
default: {
|
|
5051
5638
|
const exhaustive = def;
|
|
5052
5639
|
throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
|
|
@@ -5055,12 +5642,12 @@ function createBackend(def, options = {}) {
|
|
|
5055
5642
|
}
|
|
5056
5643
|
|
|
5057
5644
|
// src/agent/backends/container.ts
|
|
5058
|
-
var
|
|
5645
|
+
var import_types18 = require("@harness-engineering/types");
|
|
5059
5646
|
function toAgentError(message, details) {
|
|
5060
5647
|
return { category: "response_error", message, details };
|
|
5061
5648
|
}
|
|
5062
5649
|
var BLOCKED_FLAGS = ["--privileged", "--cap-add", "--security-opt", "--pid", "--ipc", "--userns"];
|
|
5063
|
-
function
|
|
5650
|
+
function sanitizeExtraArgs2(extraArgs) {
|
|
5064
5651
|
if (!extraArgs) return [];
|
|
5065
5652
|
return extraArgs.filter((arg) => !BLOCKED_FLAGS.some((flag) => arg.startsWith(flag)));
|
|
5066
5653
|
}
|
|
@@ -5086,7 +5673,7 @@ var ContainerBackend = class {
|
|
|
5086
5673
|
}
|
|
5087
5674
|
const result = await this.secretBackend.resolveSecrets(this.secretKeys);
|
|
5088
5675
|
if (!result.ok) {
|
|
5089
|
-
return (0,
|
|
5676
|
+
return (0, import_types18.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
|
|
5090
5677
|
}
|
|
5091
5678
|
return { ok: true, value: result.value };
|
|
5092
5679
|
}
|
|
@@ -5099,7 +5686,7 @@ var ContainerBackend = class {
|
|
|
5099
5686
|
network: this.containerConfig.network ?? "none",
|
|
5100
5687
|
env
|
|
5101
5688
|
};
|
|
5102
|
-
const sanitized =
|
|
5689
|
+
const sanitized = sanitizeExtraArgs2(this.containerConfig.extraArgs);
|
|
5103
5690
|
if (sanitized.length > 0) {
|
|
5104
5691
|
opts.extraArgs = sanitized;
|
|
5105
5692
|
}
|
|
@@ -5111,7 +5698,7 @@ var ContainerBackend = class {
|
|
|
5111
5698
|
const createOpts = this.buildCreateOpts(params, envResult.value);
|
|
5112
5699
|
const containerResult = await this.runtime.createContainer(createOpts);
|
|
5113
5700
|
if (!containerResult.ok) {
|
|
5114
|
-
return (0,
|
|
5701
|
+
return (0, import_types18.Err)(
|
|
5115
5702
|
toAgentError(
|
|
5116
5703
|
`Container creation failed: ${containerResult.error.message}`,
|
|
5117
5704
|
containerResult.error
|
|
@@ -5136,7 +5723,7 @@ var ContainerBackend = class {
|
|
|
5136
5723
|
this.containerHandles.delete(session.sessionId);
|
|
5137
5724
|
const removeResult = await this.runtime.removeContainer(handle);
|
|
5138
5725
|
if (!removeResult.ok) {
|
|
5139
|
-
return (0,
|
|
5726
|
+
return (0, import_types18.Err)(
|
|
5140
5727
|
toAgentError(
|
|
5141
5728
|
`Container removal failed: ${removeResult.error.message}`,
|
|
5142
5729
|
removeResult.error
|
|
@@ -5149,7 +5736,7 @@ var ContainerBackend = class {
|
|
|
5149
5736
|
async healthCheck() {
|
|
5150
5737
|
const runtimeResult = await this.runtime.healthCheck();
|
|
5151
5738
|
if (!runtimeResult.ok) {
|
|
5152
|
-
return (0,
|
|
5739
|
+
return (0, import_types18.Err)({
|
|
5153
5740
|
category: "agent_not_found",
|
|
5154
5741
|
message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
|
|
5155
5742
|
details: runtimeResult.error
|
|
@@ -5160,11 +5747,11 @@ var ContainerBackend = class {
|
|
|
5160
5747
|
};
|
|
5161
5748
|
|
|
5162
5749
|
// src/agent/runtime/docker.ts
|
|
5163
|
-
var
|
|
5164
|
-
var
|
|
5750
|
+
var import_node_child_process7 = require("child_process");
|
|
5751
|
+
var import_types19 = require("@harness-engineering/types");
|
|
5165
5752
|
function dockerExec(args) {
|
|
5166
5753
|
return new Promise((resolve6, reject) => {
|
|
5167
|
-
(0,
|
|
5754
|
+
(0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
|
|
5168
5755
|
if (error) {
|
|
5169
5756
|
reject(error);
|
|
5170
5757
|
return;
|
|
@@ -5194,9 +5781,9 @@ var DockerRuntime = class {
|
|
|
5194
5781
|
args.push(opts.image);
|
|
5195
5782
|
args.push("sleep", "infinity");
|
|
5196
5783
|
const containerId = await dockerExec(args);
|
|
5197
|
-
return (0,
|
|
5784
|
+
return (0, import_types19.Ok)({ containerId, runtime: this.name });
|
|
5198
5785
|
} catch (error) {
|
|
5199
|
-
return (0,
|
|
5786
|
+
return (0, import_types19.Err)({
|
|
5200
5787
|
category: "container_create_failed",
|
|
5201
5788
|
message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5202
5789
|
details: error
|
|
@@ -5218,7 +5805,7 @@ var DockerRuntime = class {
|
|
|
5218
5805
|
}
|
|
5219
5806
|
}
|
|
5220
5807
|
execArgs.push(handle.containerId, ...cmd);
|
|
5221
|
-
const child = (0,
|
|
5808
|
+
const child = (0, import_node_child_process7.spawn)("docker", execArgs);
|
|
5222
5809
|
const readline3 = await import("readline");
|
|
5223
5810
|
const rl = readline3.createInterface({ input: child.stdout, terminal: false });
|
|
5224
5811
|
try {
|
|
@@ -5240,9 +5827,9 @@ var DockerRuntime = class {
|
|
|
5240
5827
|
async removeContainer(handle) {
|
|
5241
5828
|
try {
|
|
5242
5829
|
await dockerExec(["rm", "-f", handle.containerId]);
|
|
5243
|
-
return (0,
|
|
5830
|
+
return (0, import_types19.Ok)(void 0);
|
|
5244
5831
|
} catch (error) {
|
|
5245
|
-
return (0,
|
|
5832
|
+
return (0, import_types19.Err)({
|
|
5246
5833
|
category: "container_remove_failed",
|
|
5247
5834
|
message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
|
|
5248
5835
|
details: error
|
|
@@ -5252,9 +5839,9 @@ var DockerRuntime = class {
|
|
|
5252
5839
|
async healthCheck() {
|
|
5253
5840
|
try {
|
|
5254
5841
|
await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
|
|
5255
|
-
return (0,
|
|
5842
|
+
return (0, import_types19.Ok)(void 0);
|
|
5256
5843
|
} catch (error) {
|
|
5257
|
-
return (0,
|
|
5844
|
+
return (0, import_types19.Err)({
|
|
5258
5845
|
category: "runtime_not_found",
|
|
5259
5846
|
message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
|
|
5260
5847
|
details: error
|
|
@@ -5264,7 +5851,7 @@ var DockerRuntime = class {
|
|
|
5264
5851
|
};
|
|
5265
5852
|
|
|
5266
5853
|
// src/agent/secrets/env.ts
|
|
5267
|
-
var
|
|
5854
|
+
var import_types20 = require("@harness-engineering/types");
|
|
5268
5855
|
var EnvSecretBackend = class {
|
|
5269
5856
|
name = "env";
|
|
5270
5857
|
async resolveSecrets(keys) {
|
|
@@ -5272,7 +5859,7 @@ var EnvSecretBackend = class {
|
|
|
5272
5859
|
for (const key of keys) {
|
|
5273
5860
|
const value = process.env[key];
|
|
5274
5861
|
if (value === void 0) {
|
|
5275
|
-
return (0,
|
|
5862
|
+
return (0, import_types20.Err)({
|
|
5276
5863
|
category: "secret_not_found",
|
|
5277
5864
|
message: `Environment variable '${key}' is not set`,
|
|
5278
5865
|
key
|
|
@@ -5280,19 +5867,19 @@ var EnvSecretBackend = class {
|
|
|
5280
5867
|
}
|
|
5281
5868
|
secrets[key] = value;
|
|
5282
5869
|
}
|
|
5283
|
-
return (0,
|
|
5870
|
+
return (0, import_types20.Ok)(secrets);
|
|
5284
5871
|
}
|
|
5285
5872
|
async healthCheck() {
|
|
5286
|
-
return (0,
|
|
5873
|
+
return (0, import_types20.Ok)(void 0);
|
|
5287
5874
|
}
|
|
5288
5875
|
};
|
|
5289
5876
|
|
|
5290
5877
|
// src/agent/secrets/onepassword.ts
|
|
5291
|
-
var
|
|
5292
|
-
var
|
|
5878
|
+
var import_node_child_process8 = require("child_process");
|
|
5879
|
+
var import_types21 = require("@harness-engineering/types");
|
|
5293
5880
|
function opExec(args) {
|
|
5294
5881
|
return new Promise((resolve6, reject) => {
|
|
5295
|
-
(0,
|
|
5882
|
+
(0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
|
|
5296
5883
|
if (error) {
|
|
5297
5884
|
reject(error);
|
|
5298
5885
|
return;
|
|
@@ -5314,21 +5901,21 @@ var OnePasswordSecretBackend = class {
|
|
|
5314
5901
|
const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
|
|
5315
5902
|
secrets[key] = value;
|
|
5316
5903
|
} catch (error) {
|
|
5317
|
-
return (0,
|
|
5904
|
+
return (0, import_types21.Err)({
|
|
5318
5905
|
category: "access_denied",
|
|
5319
5906
|
message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
|
|
5320
5907
|
key
|
|
5321
5908
|
});
|
|
5322
5909
|
}
|
|
5323
5910
|
}
|
|
5324
|
-
return (0,
|
|
5911
|
+
return (0, import_types21.Ok)(secrets);
|
|
5325
5912
|
}
|
|
5326
5913
|
async healthCheck() {
|
|
5327
5914
|
try {
|
|
5328
5915
|
await opExec(["--version"]);
|
|
5329
|
-
return (0,
|
|
5916
|
+
return (0, import_types21.Ok)(void 0);
|
|
5330
5917
|
} catch (error) {
|
|
5331
|
-
return (0,
|
|
5918
|
+
return (0, import_types21.Err)({
|
|
5332
5919
|
category: "provider_unavailable",
|
|
5333
5920
|
message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5334
5921
|
});
|
|
@@ -5337,11 +5924,11 @@ var OnePasswordSecretBackend = class {
|
|
|
5337
5924
|
};
|
|
5338
5925
|
|
|
5339
5926
|
// src/agent/secrets/vault.ts
|
|
5340
|
-
var
|
|
5341
|
-
var
|
|
5927
|
+
var import_node_child_process9 = require("child_process");
|
|
5928
|
+
var import_types22 = require("@harness-engineering/types");
|
|
5342
5929
|
function vaultExec(args, env) {
|
|
5343
5930
|
return new Promise((resolve6, reject) => {
|
|
5344
|
-
(0,
|
|
5931
|
+
(0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
|
|
5345
5932
|
if (error) {
|
|
5346
5933
|
reject(error);
|
|
5347
5934
|
return;
|
|
@@ -5369,11 +5956,11 @@ var VaultSecretBackend = class {
|
|
|
5369
5956
|
} catch (error) {
|
|
5370
5957
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5371
5958
|
const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
|
|
5372
|
-
return (0,
|
|
5959
|
+
return (0, import_types22.Err)({ category, message: `Failed to read from Vault: ${msg}` });
|
|
5373
5960
|
}
|
|
5374
5961
|
const missing = keys.find((k) => !(k in data));
|
|
5375
5962
|
if (missing) {
|
|
5376
|
-
return (0,
|
|
5963
|
+
return (0, import_types22.Err)({
|
|
5377
5964
|
category: "secret_not_found",
|
|
5378
5965
|
message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
|
|
5379
5966
|
key: missing
|
|
@@ -5381,14 +5968,14 @@ var VaultSecretBackend = class {
|
|
|
5381
5968
|
}
|
|
5382
5969
|
const secrets = {};
|
|
5383
5970
|
for (const key of keys) secrets[key] = data[key];
|
|
5384
|
-
return (0,
|
|
5971
|
+
return (0, import_types22.Ok)(secrets);
|
|
5385
5972
|
}
|
|
5386
5973
|
async healthCheck() {
|
|
5387
5974
|
try {
|
|
5388
5975
|
await vaultExec(["version"]);
|
|
5389
|
-
return (0,
|
|
5976
|
+
return (0, import_types22.Ok)(void 0);
|
|
5390
5977
|
} catch (error) {
|
|
5391
|
-
return (0,
|
|
5978
|
+
return (0, import_types22.Err)({
|
|
5392
5979
|
category: "provider_unavailable",
|
|
5393
5980
|
message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
|
|
5394
5981
|
});
|
|
@@ -5525,6 +6112,8 @@ function buildAnalysisProvider(args) {
|
|
|
5525
6112
|
return buildClaudeCliProvider(def, args, layerModel);
|
|
5526
6113
|
case "mock":
|
|
5527
6114
|
case "gemini":
|
|
6115
|
+
case "ssh":
|
|
6116
|
+
case "serverless":
|
|
5528
6117
|
logger.warn(
|
|
5529
6118
|
`Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
|
|
5530
6119
|
);
|
|
@@ -5948,7 +6537,7 @@ function handlePlansRoute(req, res, plansDir) {
|
|
|
5948
6537
|
}
|
|
5949
6538
|
|
|
5950
6539
|
// src/server/routes/chat-proxy.ts
|
|
5951
|
-
var
|
|
6540
|
+
var import_node_child_process10 = require("child_process");
|
|
5952
6541
|
var import_node_crypto5 = require("crypto");
|
|
5953
6542
|
var readline2 = __toESM(require("readline"));
|
|
5954
6543
|
var import_zod6 = require("zod");
|
|
@@ -6034,7 +6623,7 @@ async function handleChatRequest(req, res, command) {
|
|
|
6034
6623
|
});
|
|
6035
6624
|
emit(res, { type: "session", sessionId });
|
|
6036
6625
|
const args = buildArgs(parsed.prompt, sessionId, isFirstTurn, parsed.system);
|
|
6037
|
-
child = (0,
|
|
6626
|
+
child = (0, import_node_child_process10.spawn)(command, args, { env: buildChildEnv(), stdio: "pipe" });
|
|
6038
6627
|
child.stdin?.end();
|
|
6039
6628
|
let clientDisconnected = false;
|
|
6040
6629
|
res.on("close", () => {
|
|
@@ -6762,7 +7351,7 @@ function isPrivateHost(hostname) {
|
|
|
6762
7351
|
}
|
|
6763
7352
|
|
|
6764
7353
|
// src/server/routes/v1/webhooks.ts
|
|
6765
|
-
var
|
|
7354
|
+
var import_types23 = require("@harness-engineering/types");
|
|
6766
7355
|
function isAdminAuth(authContext) {
|
|
6767
7356
|
if (!authContext) return false;
|
|
6768
7357
|
if (authContext.scopes.includes("admin")) return true;
|
|
@@ -6809,7 +7398,7 @@ function handleV1WebhooksRoute(req, res, deps) {
|
|
|
6809
7398
|
const subs = await deps.store.list();
|
|
6810
7399
|
const authContext = getAuthContext(req);
|
|
6811
7400
|
const visible = isAdminAuth(authContext) ? subs : subs.filter((s) => s.tokenId === authContext?.id);
|
|
6812
|
-
const publicView = visible.map((s) =>
|
|
7401
|
+
const publicView = visible.map((s) => import_types23.WebhookSubscriptionPublicSchema.parse(s));
|
|
6813
7402
|
sendJSON6(res, 200, publicView);
|
|
6814
7403
|
})();
|
|
6815
7404
|
return true;
|
|
@@ -7123,11 +7712,11 @@ function handleStreamsRoute(req, res, recorder) {
|
|
|
7123
7712
|
|
|
7124
7713
|
// src/server/routes/auth.ts
|
|
7125
7714
|
var import_zod14 = require("zod");
|
|
7126
|
-
var
|
|
7715
|
+
var import_types24 = require("@harness-engineering/types");
|
|
7127
7716
|
var CreateBodySchema = import_zod14.z.object({
|
|
7128
7717
|
name: import_zod14.z.string().min(1).max(100),
|
|
7129
|
-
scopes: import_zod14.z.array(
|
|
7130
|
-
bridgeKind:
|
|
7718
|
+
scopes: import_zod14.z.array(import_types24.TokenScopeSchema).min(1),
|
|
7719
|
+
bridgeKind: import_types24.BridgeKindSchema.optional(),
|
|
7131
7720
|
tenantId: import_zod14.z.string().optional(),
|
|
7132
7721
|
expiresAt: import_zod14.z.string().datetime().optional()
|
|
7133
7722
|
});
|
|
@@ -7165,7 +7754,7 @@ async function handlePost(req, res, store) {
|
|
|
7165
7754
|
if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
|
|
7166
7755
|
if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
|
|
7167
7756
|
const result = await store.create(input);
|
|
7168
|
-
const publicRecord =
|
|
7757
|
+
const publicRecord = import_types24.AuthTokenPublicSchema.parse(result.record);
|
|
7169
7758
|
sendJSON8(res, 200, {
|
|
7170
7759
|
...publicRecord,
|
|
7171
7760
|
token: result.token
|
|
@@ -7369,7 +7958,7 @@ var import_node_crypto9 = require("crypto");
|
|
|
7369
7958
|
var import_promises = require("fs/promises");
|
|
7370
7959
|
var import_node_path = require("path");
|
|
7371
7960
|
var import_bcryptjs = __toESM(require("bcryptjs"));
|
|
7372
|
-
var
|
|
7961
|
+
var import_types25 = require("@harness-engineering/types");
|
|
7373
7962
|
var BCRYPT_ROUNDS = 12;
|
|
7374
7963
|
var LEGACY_ENV_ID = "tok_legacy_env";
|
|
7375
7964
|
function genId() {
|
|
@@ -7384,8 +7973,8 @@ function parseToken(raw) {
|
|
|
7384
7973
|
return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
|
|
7385
7974
|
}
|
|
7386
7975
|
var TokenStore = class {
|
|
7387
|
-
constructor(
|
|
7388
|
-
this.path =
|
|
7976
|
+
constructor(path19) {
|
|
7977
|
+
this.path = path19;
|
|
7389
7978
|
}
|
|
7390
7979
|
path;
|
|
7391
7980
|
cache = null;
|
|
@@ -7396,7 +7985,7 @@ var TokenStore = class {
|
|
|
7396
7985
|
const parsed = JSON.parse(raw);
|
|
7397
7986
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
7398
7987
|
this.cache = list.map((entry) => {
|
|
7399
|
-
const r =
|
|
7988
|
+
const r = import_types25.AuthTokenSchema.safeParse(entry);
|
|
7400
7989
|
return r.success ? r.data : null;
|
|
7401
7990
|
}).filter((x) => x !== null);
|
|
7402
7991
|
} catch (err) {
|
|
@@ -7458,7 +8047,7 @@ var TokenStore = class {
|
|
|
7458
8047
|
}
|
|
7459
8048
|
async list() {
|
|
7460
8049
|
const records = await this.load();
|
|
7461
|
-
return records.map((r) =>
|
|
8050
|
+
return records.map((r) => import_types25.AuthTokenPublicSchema.parse(r));
|
|
7462
8051
|
}
|
|
7463
8052
|
async revoke(id) {
|
|
7464
8053
|
const records = await this.load();
|
|
@@ -7490,10 +8079,10 @@ var TokenStore = class {
|
|
|
7490
8079
|
// src/auth/audit.ts
|
|
7491
8080
|
var import_promises2 = require("fs/promises");
|
|
7492
8081
|
var import_node_path2 = require("path");
|
|
7493
|
-
var
|
|
8082
|
+
var import_types26 = require("@harness-engineering/types");
|
|
7494
8083
|
var AuditLogger = class {
|
|
7495
|
-
constructor(
|
|
7496
|
-
this.path =
|
|
8084
|
+
constructor(path19, opts = {}) {
|
|
8085
|
+
this.path = path19;
|
|
7497
8086
|
this.opts = opts;
|
|
7498
8087
|
}
|
|
7499
8088
|
path;
|
|
@@ -7501,7 +8090,7 @@ var AuditLogger = class {
|
|
|
7501
8090
|
queue = Promise.resolve();
|
|
7502
8091
|
dirEnsured = false;
|
|
7503
8092
|
async append(input) {
|
|
7504
|
-
const entry =
|
|
8093
|
+
const entry = import_types26.AuthAuditEntrySchema.parse({
|
|
7505
8094
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7506
8095
|
tokenId: input.tokenId,
|
|
7507
8096
|
...input.tenantId ? { tenantId: input.tenantId } : {},
|
|
@@ -7588,9 +8177,9 @@ var V1_BRIDGE_ROUTES = [
|
|
|
7588
8177
|
function isV1Bridge(method, url) {
|
|
7589
8178
|
return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
|
|
7590
8179
|
}
|
|
7591
|
-
function requiredBridgeScope(method,
|
|
8180
|
+
function requiredBridgeScope(method, path19) {
|
|
7592
8181
|
for (const r of V1_BRIDGE_ROUTES) {
|
|
7593
|
-
if (r.method === method && r.pattern.test(
|
|
8182
|
+
if (r.method === method && r.pattern.test(path19)) return r.scope;
|
|
7594
8183
|
}
|
|
7595
8184
|
return null;
|
|
7596
8185
|
}
|
|
@@ -7600,24 +8189,24 @@ function hasScope(held, required) {
|
|
|
7600
8189
|
if (held.includes("admin")) return true;
|
|
7601
8190
|
return held.includes(required);
|
|
7602
8191
|
}
|
|
7603
|
-
function requiredScopeForRoute(method,
|
|
7604
|
-
const bridgeScope = requiredBridgeScope(method,
|
|
8192
|
+
function requiredScopeForRoute(method, path19) {
|
|
8193
|
+
const bridgeScope = requiredBridgeScope(method, path19);
|
|
7605
8194
|
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 (
|
|
8195
|
+
if (path19 === "/api/v1/auth/token" && method === "POST") return "admin";
|
|
8196
|
+
if (path19 === "/api/v1/auth/tokens" && method === "GET") return "admin";
|
|
8197
|
+
if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path19) && method === "DELETE") return "admin";
|
|
8198
|
+
if ((path19 === "/api/state" || path19 === "/api/v1/state") && method === "GET") return "read-status";
|
|
8199
|
+
if (path19.startsWith("/api/interactions")) return "resolve-interaction";
|
|
8200
|
+
if (path19.startsWith("/api/plans")) return "read-status";
|
|
8201
|
+
if (path19.startsWith("/api/analyze") || path19.startsWith("/api/analyses")) return "read-status";
|
|
8202
|
+
if (path19.startsWith("/api/roadmap-actions")) return "modify-roadmap";
|
|
8203
|
+
if (path19.startsWith("/api/dispatch-actions")) return "trigger-job";
|
|
8204
|
+
if (path19.startsWith("/api/local-model") || path19.startsWith("/api/local-models"))
|
|
7616
8205
|
return "read-status";
|
|
7617
|
-
if (
|
|
7618
|
-
if (
|
|
7619
|
-
if (
|
|
7620
|
-
if (
|
|
8206
|
+
if (path19.startsWith("/api/maintenance")) return "trigger-job";
|
|
8207
|
+
if (path19.startsWith("/api/streams")) return "read-status";
|
|
8208
|
+
if (path19.startsWith("/api/sessions")) return "read-status";
|
|
8209
|
+
if (path19.startsWith("/api/chat-proxy")) return "trigger-job";
|
|
7621
8210
|
return null;
|
|
7622
8211
|
}
|
|
7623
8212
|
|
|
@@ -7991,7 +8580,7 @@ var OrchestratorServer = class {
|
|
|
7991
8580
|
var import_node_crypto11 = require("crypto");
|
|
7992
8581
|
var import_promises3 = require("fs/promises");
|
|
7993
8582
|
var import_node_path3 = require("path");
|
|
7994
|
-
var
|
|
8583
|
+
var import_types27 = require("@harness-engineering/types");
|
|
7995
8584
|
|
|
7996
8585
|
// src/gateway/webhooks/signer.ts
|
|
7997
8586
|
var import_node_crypto10 = require("crypto");
|
|
@@ -8021,8 +8610,8 @@ function genSecret2() {
|
|
|
8021
8610
|
return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
|
|
8022
8611
|
}
|
|
8023
8612
|
var WebhookStore = class {
|
|
8024
|
-
constructor(
|
|
8025
|
-
this.path =
|
|
8613
|
+
constructor(path19) {
|
|
8614
|
+
this.path = path19;
|
|
8026
8615
|
}
|
|
8027
8616
|
path;
|
|
8028
8617
|
cache = null;
|
|
@@ -8033,7 +8622,7 @@ var WebhookStore = class {
|
|
|
8033
8622
|
const parsed = JSON.parse(raw);
|
|
8034
8623
|
const list = Array.isArray(parsed) ? parsed : [];
|
|
8035
8624
|
this.cache = list.map((entry) => {
|
|
8036
|
-
const r =
|
|
8625
|
+
const r = import_types27.WebhookSubscriptionSchema.safeParse(entry);
|
|
8037
8626
|
return r.success ? r.data : null;
|
|
8038
8627
|
}).filter((x) => x !== null);
|
|
8039
8628
|
} catch (err) {
|
|
@@ -8608,6 +9197,335 @@ function wireTelemetryFanout(params) {
|
|
|
8608
9197
|
};
|
|
8609
9198
|
}
|
|
8610
9199
|
|
|
9200
|
+
// src/notifications/slack-sink.ts
|
|
9201
|
+
var SEVERITY_PREFIX = {
|
|
9202
|
+
info: ":information_source:",
|
|
9203
|
+
success: ":white_check_mark:",
|
|
9204
|
+
warning: ":warning:",
|
|
9205
|
+
error: ":x:"
|
|
9206
|
+
};
|
|
9207
|
+
var SlackSink = class {
|
|
9208
|
+
kind = "slack";
|
|
9209
|
+
id;
|
|
9210
|
+
webhookUrl;
|
|
9211
|
+
fetchImpl;
|
|
9212
|
+
timeoutMs;
|
|
9213
|
+
constructor(opts) {
|
|
9214
|
+
this.id = opts.id;
|
|
9215
|
+
this.webhookUrl = opts.webhookUrl;
|
|
9216
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
9217
|
+
this.timeoutMs = opts.timeoutMs ?? 5e3;
|
|
9218
|
+
}
|
|
9219
|
+
async deliver(input) {
|
|
9220
|
+
const body = input.wrapped ? this.renderEnvelope(input.payload) : this.renderRawEvent(input.payload);
|
|
9221
|
+
const ctrl = new AbortController();
|
|
9222
|
+
const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
|
|
9223
|
+
try {
|
|
9224
|
+
const res = await this.fetchImpl(this.webhookUrl, {
|
|
9225
|
+
method: "POST",
|
|
9226
|
+
headers: { "Content-Type": "application/json" },
|
|
9227
|
+
body: JSON.stringify(body),
|
|
9228
|
+
signal: ctrl.signal
|
|
9229
|
+
});
|
|
9230
|
+
if (res.ok) {
|
|
9231
|
+
return { ok: true, deliveredAt: Date.now() };
|
|
9232
|
+
}
|
|
9233
|
+
return { ok: false, error: `HTTP ${res.status}`, httpStatus: res.status };
|
|
9234
|
+
} catch (err) {
|
|
9235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9236
|
+
return { ok: false, error: ctrl.signal.aborted ? "timeout" : msg };
|
|
9237
|
+
} finally {
|
|
9238
|
+
clearTimeout(timer);
|
|
9239
|
+
}
|
|
9240
|
+
}
|
|
9241
|
+
renderEnvelope(env) {
|
|
9242
|
+
const prefix = SEVERITY_PREFIX[env.severity] ?? "";
|
|
9243
|
+
const headline = `${prefix} ${env.title}`.trim();
|
|
9244
|
+
const blocks = [
|
|
9245
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${headline}*
|
|
9246
|
+
${env.summary}` } }
|
|
9247
|
+
];
|
|
9248
|
+
if (env.actions && env.actions.length > 0) {
|
|
9249
|
+
blocks.push({
|
|
9250
|
+
type: "actions",
|
|
9251
|
+
elements: env.actions.map((a) => ({
|
|
9252
|
+
type: "button",
|
|
9253
|
+
text: { type: "plain_text", text: a.label },
|
|
9254
|
+
url: a.url
|
|
9255
|
+
}))
|
|
9256
|
+
});
|
|
9257
|
+
}
|
|
9258
|
+
if (env.permalink) {
|
|
9259
|
+
blocks.push({
|
|
9260
|
+
type: "section",
|
|
9261
|
+
text: { type: "mrkdwn", text: `<${env.permalink}|View details>` }
|
|
9262
|
+
});
|
|
9263
|
+
}
|
|
9264
|
+
return { text: headline, blocks };
|
|
9265
|
+
}
|
|
9266
|
+
renderRawEvent(event) {
|
|
9267
|
+
const dump = (() => {
|
|
9268
|
+
try {
|
|
9269
|
+
return JSON.stringify(event.data, null, 2);
|
|
9270
|
+
} catch {
|
|
9271
|
+
return String(event.data);
|
|
9272
|
+
}
|
|
9273
|
+
})();
|
|
9274
|
+
const text = `harness event: \`${event.type}\``;
|
|
9275
|
+
return {
|
|
9276
|
+
text,
|
|
9277
|
+
blocks: [
|
|
9278
|
+
{ type: "section", text: { type: "mrkdwn", text: `*${text}*
|
|
9279
|
+
\`\`\`
|
|
9280
|
+
${dump}
|
|
9281
|
+
\`\`\`` } }
|
|
9282
|
+
]
|
|
9283
|
+
};
|
|
9284
|
+
}
|
|
9285
|
+
};
|
|
9286
|
+
|
|
9287
|
+
// src/notifications/registry.ts
|
|
9288
|
+
var SinkConfigError = class extends Error {
|
|
9289
|
+
constructor(sinkId, message) {
|
|
9290
|
+
super(`[sink:${sinkId}] ${message}`);
|
|
9291
|
+
this.sinkId = sinkId;
|
|
9292
|
+
this.name = "SinkConfigError";
|
|
9293
|
+
}
|
|
9294
|
+
sinkId;
|
|
9295
|
+
};
|
|
9296
|
+
var SinkRegistry = class _SinkRegistry {
|
|
9297
|
+
entries;
|
|
9298
|
+
constructor(entries) {
|
|
9299
|
+
this.entries = entries;
|
|
9300
|
+
}
|
|
9301
|
+
static fromConfig(config, options) {
|
|
9302
|
+
const entries = [];
|
|
9303
|
+
for (const sinkConfig of config.sinks) {
|
|
9304
|
+
entries.push({
|
|
9305
|
+
config: sinkConfig,
|
|
9306
|
+
adapter: buildSink(sinkConfig, options)
|
|
9307
|
+
});
|
|
9308
|
+
}
|
|
9309
|
+
return new _SinkRegistry(entries);
|
|
9310
|
+
}
|
|
9311
|
+
list() {
|
|
9312
|
+
return this.entries;
|
|
9313
|
+
}
|
|
9314
|
+
get(id) {
|
|
9315
|
+
return this.entries.find((e) => e.config.id === id) ?? null;
|
|
9316
|
+
}
|
|
9317
|
+
ids() {
|
|
9318
|
+
return this.entries.map((e) => e.config.id);
|
|
9319
|
+
}
|
|
9320
|
+
async dispose() {
|
|
9321
|
+
for (const entry of this.entries) {
|
|
9322
|
+
if (entry.adapter.dispose) {
|
|
9323
|
+
await entry.adapter.dispose();
|
|
9324
|
+
}
|
|
9325
|
+
}
|
|
9326
|
+
}
|
|
9327
|
+
};
|
|
9328
|
+
function buildSink(config, options) {
|
|
9329
|
+
const kind = config.kind;
|
|
9330
|
+
switch (kind) {
|
|
9331
|
+
case "slack":
|
|
9332
|
+
return buildSlackSink(config, options);
|
|
9333
|
+
default: {
|
|
9334
|
+
const _exhaustive = kind;
|
|
9335
|
+
throw new SinkConfigError(config.id, `unknown sink kind '${String(_exhaustive)}'`);
|
|
9336
|
+
}
|
|
9337
|
+
}
|
|
9338
|
+
}
|
|
9339
|
+
function buildSlackSink(config, options) {
|
|
9340
|
+
const rawConfig = config.config;
|
|
9341
|
+
const envKey = typeof rawConfig.webhookUrlEnv === "string" ? rawConfig.webhookUrlEnv : null;
|
|
9342
|
+
const inlineUrl = typeof rawConfig.webhookUrl === "string" ? rawConfig.webhookUrl : null;
|
|
9343
|
+
let url;
|
|
9344
|
+
if (envKey) {
|
|
9345
|
+
const v = options.env[envKey];
|
|
9346
|
+
if (!v) {
|
|
9347
|
+
throw new SinkConfigError(
|
|
9348
|
+
config.id,
|
|
9349
|
+
`Slack webhook env var '${envKey}' is not set in the environment`
|
|
9350
|
+
);
|
|
9351
|
+
}
|
|
9352
|
+
url = v;
|
|
9353
|
+
} else if (inlineUrl) {
|
|
9354
|
+
url = inlineUrl;
|
|
9355
|
+
} else {
|
|
9356
|
+
throw new SinkConfigError(
|
|
9357
|
+
config.id,
|
|
9358
|
+
`Slack sink requires 'config.webhookUrlEnv' (preferred) or 'config.webhookUrl'`
|
|
9359
|
+
);
|
|
9360
|
+
}
|
|
9361
|
+
if (!/^https:\/\/hooks\.slack\.com\//.test(url)) {
|
|
9362
|
+
throw new SinkConfigError(
|
|
9363
|
+
config.id,
|
|
9364
|
+
`Slack webhook URL must be an https://hooks.slack.com/ URL`
|
|
9365
|
+
);
|
|
9366
|
+
}
|
|
9367
|
+
const sinkOpts = {
|
|
9368
|
+
id: config.id,
|
|
9369
|
+
webhookUrl: url
|
|
9370
|
+
};
|
|
9371
|
+
if (options.fetchImpl) sinkOpts.fetchImpl = options.fetchImpl;
|
|
9372
|
+
return new SlackSink(sinkOpts);
|
|
9373
|
+
}
|
|
9374
|
+
|
|
9375
|
+
// src/notifications/events.ts
|
|
9376
|
+
var import_node_crypto15 = require("crypto");
|
|
9377
|
+
|
|
9378
|
+
// src/notifications/envelope.ts
|
|
9379
|
+
function asObj(data) {
|
|
9380
|
+
return typeof data === "object" && data !== null ? data : {};
|
|
9381
|
+
}
|
|
9382
|
+
var ENVELOPE_DERIVERS = {
|
|
9383
|
+
"maintenance.started": (event) => {
|
|
9384
|
+
const data = asObj(event.data);
|
|
9385
|
+
return {
|
|
9386
|
+
title: `Maintenance started: ${data.taskId ?? "(unknown task)"}`,
|
|
9387
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` is running.`,
|
|
9388
|
+
severity: "info"
|
|
9389
|
+
};
|
|
9390
|
+
},
|
|
9391
|
+
"maintenance.completed": (event) => {
|
|
9392
|
+
const data = asObj(event.data);
|
|
9393
|
+
return {
|
|
9394
|
+
title: `Maintenance done: ${data.taskId ?? "(unknown task)"}`,
|
|
9395
|
+
summary: `Task \`${data.taskId ?? "(unknown)"}\` completed successfully.`,
|
|
9396
|
+
severity: "success"
|
|
9397
|
+
};
|
|
9398
|
+
},
|
|
9399
|
+
"maintenance.error": (event) => {
|
|
9400
|
+
const data = asObj(event.data);
|
|
9401
|
+
return {
|
|
9402
|
+
title: `Maintenance failed: ${data.taskId ?? "(unknown task)"}`,
|
|
9403
|
+
summary: data.error ?? "No error message provided.",
|
|
9404
|
+
severity: "error"
|
|
9405
|
+
};
|
|
9406
|
+
},
|
|
9407
|
+
"interaction.created": (event) => {
|
|
9408
|
+
const data = asObj(event.data);
|
|
9409
|
+
return {
|
|
9410
|
+
title: `Action required: ${truncate(data.question ?? "pending interaction", 80)}`,
|
|
9411
|
+
summary: data.question ?? "(no question text)",
|
|
9412
|
+
severity: "warning"
|
|
9413
|
+
};
|
|
9414
|
+
},
|
|
9415
|
+
"interaction.resolved": (event) => {
|
|
9416
|
+
const data = asObj(event.data);
|
|
9417
|
+
return {
|
|
9418
|
+
title: `Interaction resolved`,
|
|
9419
|
+
summary: data.resolution ?? "(no resolution text)",
|
|
9420
|
+
severity: "info"
|
|
9421
|
+
};
|
|
9422
|
+
},
|
|
9423
|
+
"notification.test": (event) => {
|
|
9424
|
+
const data = asObj(event.data);
|
|
9425
|
+
return {
|
|
9426
|
+
title: "Test notification from harness",
|
|
9427
|
+
summary: data.message ?? "If you see this, your notification sink is working.",
|
|
9428
|
+
severity: "info"
|
|
9429
|
+
};
|
|
9430
|
+
}
|
|
9431
|
+
};
|
|
9432
|
+
function truncate(s, max) {
|
|
9433
|
+
return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
|
|
9434
|
+
}
|
|
9435
|
+
function fallbackTitle(event) {
|
|
9436
|
+
return event.type;
|
|
9437
|
+
}
|
|
9438
|
+
function fallbackSummary(event) {
|
|
9439
|
+
try {
|
|
9440
|
+
return "```\n" + JSON.stringify(event.data, null, 2) + "\n```";
|
|
9441
|
+
} catch {
|
|
9442
|
+
return String(event.data);
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
function severityFromType(type) {
|
|
9446
|
+
if (type.endsWith(".error") || type.endsWith(".failed")) return "error";
|
|
9447
|
+
if (type.endsWith(".completed") || type.endsWith(".resolved")) return "success";
|
|
9448
|
+
if (type.endsWith(".created") || type.startsWith("interaction.")) return "warning";
|
|
9449
|
+
return "info";
|
|
9450
|
+
}
|
|
9451
|
+
function backfillEnvelope(event, partial) {
|
|
9452
|
+
return {
|
|
9453
|
+
title: truncate(partial.title ?? fallbackTitle(event), 280),
|
|
9454
|
+
summary: partial.summary ?? fallbackSummary(event),
|
|
9455
|
+
severity: partial.severity ?? severityFromType(event.type)
|
|
9456
|
+
};
|
|
9457
|
+
}
|
|
9458
|
+
function wrapAsEnvelope(event) {
|
|
9459
|
+
const deriver = ENVELOPE_DERIVERS[event.type];
|
|
9460
|
+
const partial = deriver ? deriver(event) : {};
|
|
9461
|
+
const envelope = backfillEnvelope(event, partial);
|
|
9462
|
+
if (partial.actions) envelope.actions = partial.actions;
|
|
9463
|
+
if (partial.permalink) envelope.permalink = partial.permalink;
|
|
9464
|
+
if (event.correlationId) envelope.correlationId = event.correlationId;
|
|
9465
|
+
return envelope;
|
|
9466
|
+
}
|
|
9467
|
+
|
|
9468
|
+
// src/notifications/events.ts
|
|
9469
|
+
var NOTIFICATION_TOPICS = [
|
|
9470
|
+
"interaction.created",
|
|
9471
|
+
"interaction.resolved",
|
|
9472
|
+
"maintenance:started",
|
|
9473
|
+
"maintenance:completed",
|
|
9474
|
+
"maintenance:error"
|
|
9475
|
+
];
|
|
9476
|
+
function newEventId4() {
|
|
9477
|
+
return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
|
|
9478
|
+
}
|
|
9479
|
+
function dispatchToEntry(bus, entry, event) {
|
|
9480
|
+
const eventType = event.type;
|
|
9481
|
+
const matches = entry.config.events.some((p) => eventMatches(p, eventType));
|
|
9482
|
+
if (!matches) return;
|
|
9483
|
+
const payload = entry.config.wrap_response ? wrapAsEnvelope(event) : event;
|
|
9484
|
+
const summaryBase = {
|
|
9485
|
+
sinkId: entry.adapter.id,
|
|
9486
|
+
kind: entry.adapter.kind,
|
|
9487
|
+
eventType,
|
|
9488
|
+
eventId: event.id
|
|
9489
|
+
};
|
|
9490
|
+
void entry.adapter.deliver({ payload, wrapped: entry.config.wrap_response }).then((result) => {
|
|
9491
|
+
bus.emit("notification.delivery.attempted", { ...summaryBase, ok: result.ok });
|
|
9492
|
+
if (!result.ok) {
|
|
9493
|
+
bus.emit("notification.delivery.failed", {
|
|
9494
|
+
...summaryBase,
|
|
9495
|
+
ok: false,
|
|
9496
|
+
error: result.error
|
|
9497
|
+
});
|
|
9498
|
+
}
|
|
9499
|
+
}).catch((err) => {
|
|
9500
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9501
|
+
bus.emit("notification.delivery.failed", { ...summaryBase, ok: false, error: msg });
|
|
9502
|
+
});
|
|
9503
|
+
}
|
|
9504
|
+
function wireNotificationSinks({ bus, registry }) {
|
|
9505
|
+
const handlers = [];
|
|
9506
|
+
for (const topic of NOTIFICATION_TOPICS) {
|
|
9507
|
+
const eventType = topic.replace(":", ".");
|
|
9508
|
+
const fn = (data) => {
|
|
9509
|
+
const entries = registry.list();
|
|
9510
|
+
if (entries.length === 0) return;
|
|
9511
|
+
const event = {
|
|
9512
|
+
id: newEventId4(),
|
|
9513
|
+
type: eventType,
|
|
9514
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9515
|
+
data
|
|
9516
|
+
};
|
|
9517
|
+
for (const entry of entries) {
|
|
9518
|
+
dispatchToEntry(bus, entry, event);
|
|
9519
|
+
}
|
|
9520
|
+
};
|
|
9521
|
+
bus.on(topic, fn);
|
|
9522
|
+
handlers.push({ topic, fn });
|
|
9523
|
+
}
|
|
9524
|
+
return () => {
|
|
9525
|
+
for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
|
|
9526
|
+
};
|
|
9527
|
+
}
|
|
9528
|
+
|
|
8611
9529
|
// src/orchestrator.ts
|
|
8612
9530
|
var import_core13 = require("@harness-engineering/core");
|
|
8613
9531
|
|
|
@@ -9156,10 +10074,10 @@ var MaintenanceScheduler = class {
|
|
|
9156
10074
|
};
|
|
9157
10075
|
|
|
9158
10076
|
// src/maintenance/leader-elector.ts
|
|
9159
|
-
var
|
|
10077
|
+
var import_types28 = require("@harness-engineering/types");
|
|
9160
10078
|
var SingleProcessLeaderElector = class {
|
|
9161
10079
|
async electLeader() {
|
|
9162
|
-
return (0,
|
|
10080
|
+
return (0, import_types28.Ok)("claimed");
|
|
9163
10081
|
}
|
|
9164
10082
|
};
|
|
9165
10083
|
|
|
@@ -9639,6 +10557,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9639
10557
|
cacheMetrics;
|
|
9640
10558
|
otlpExporter;
|
|
9641
10559
|
telemetryFanoutOff;
|
|
10560
|
+
// Hermes Phase 3: in-process notification sinks subscribe to the same
|
|
10561
|
+
// event bus (`this`) that webhook fanout uses, applying envelope
|
|
10562
|
+
// formatting before delivering to Slack/etc. The registry + unwire
|
|
10563
|
+
// handle are kept on the instance so stop() can detach listeners and
|
|
10564
|
+
// call adapter dispose() in deterministic order.
|
|
10565
|
+
notificationsRegistry;
|
|
10566
|
+
notificationFanoutOff;
|
|
9642
10567
|
orchestratorIdPromise;
|
|
9643
10568
|
recorder;
|
|
9644
10569
|
intelligenceRunner;
|
|
@@ -9794,6 +10719,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
9794
10719
|
delivery: webhookDelivery
|
|
9795
10720
|
});
|
|
9796
10721
|
webhookDelivery.start();
|
|
10722
|
+
this.setupNotifications(config.notifications);
|
|
9797
10723
|
const otlpCfg = config.telemetry?.export?.otlp;
|
|
9798
10724
|
if (otlpCfg) {
|
|
9799
10725
|
this.otlpExporter = new import_core13.OTLPExporter({
|
|
@@ -10208,7 +11134,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10208
11134
|
{ issueId }
|
|
10209
11135
|
);
|
|
10210
11136
|
await this.interactionQueue.push({
|
|
10211
|
-
id: `interaction-${(0,
|
|
11137
|
+
id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
|
|
10212
11138
|
issueId,
|
|
10213
11139
|
type: "needs-human",
|
|
10214
11140
|
reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
|
|
@@ -10284,7 +11210,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10284
11210
|
{ issueId: effect.issueId }
|
|
10285
11211
|
);
|
|
10286
11212
|
await this.interactionQueue.push({
|
|
10287
|
-
id: `interaction-${(0,
|
|
11213
|
+
id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
|
|
10288
11214
|
issueId: effect.issueId,
|
|
10289
11215
|
type: "needs-human",
|
|
10290
11216
|
reasons: effect.reasons,
|
|
@@ -10629,6 +11555,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10629
11555
|
);
|
|
10630
11556
|
this.emit("state_change", this.getSnapshot());
|
|
10631
11557
|
}
|
|
11558
|
+
/**
|
|
11559
|
+
* Hermes Phase 3: wire in-process notification sinks against the
|
|
11560
|
+
* orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
|
|
11561
|
+
* missing env var) logs + skips rather than breaking startup — the
|
|
11562
|
+
* hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
|
|
11563
|
+
* to the same topics as `wireWebhookFanout`; a slow Slack call cannot
|
|
11564
|
+
* block webhook delivery because the two paths fan out independently.
|
|
11565
|
+
*/
|
|
11566
|
+
setupNotifications(notifConfig) {
|
|
11567
|
+
if (!notifConfig || !notifConfig.sinks || notifConfig.sinks.length === 0) return;
|
|
11568
|
+
try {
|
|
11569
|
+
this.notificationsRegistry = SinkRegistry.fromConfig(notifConfig, {
|
|
11570
|
+
env: process.env
|
|
11571
|
+
});
|
|
11572
|
+
this.notificationFanoutOff = wireNotificationSinks({
|
|
11573
|
+
bus: this,
|
|
11574
|
+
registry: this.notificationsRegistry
|
|
11575
|
+
});
|
|
11576
|
+
} catch (err) {
|
|
11577
|
+
this.logger.warn(
|
|
11578
|
+
`notifications sink registry failed: ${err instanceof Error ? err.message : String(err)}; sinks disabled`
|
|
11579
|
+
);
|
|
11580
|
+
delete this.notificationsRegistry;
|
|
11581
|
+
}
|
|
11582
|
+
}
|
|
10632
11583
|
/**
|
|
10633
11584
|
* Stops execution for a specific issue.
|
|
10634
11585
|
*
|
|
@@ -10796,6 +11747,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
|
|
|
10796
11747
|
this.webhookFanoutOff();
|
|
10797
11748
|
delete this.webhookFanoutOff;
|
|
10798
11749
|
}
|
|
11750
|
+
if (this.notificationFanoutOff) {
|
|
11751
|
+
this.notificationFanoutOff();
|
|
11752
|
+
delete this.notificationFanoutOff;
|
|
11753
|
+
}
|
|
11754
|
+
if (this.notificationsRegistry) {
|
|
11755
|
+
await this.notificationsRegistry.dispose();
|
|
11756
|
+
delete this.notificationsRegistry;
|
|
11757
|
+
}
|
|
10799
11758
|
if (this.telemetryFanoutOff) {
|
|
10800
11759
|
this.telemetryFanoutOff();
|
|
10801
11760
|
delete this.telemetryFanoutOff;
|
|
@@ -11084,9 +12043,9 @@ function launchTUI(orchestrator) {
|
|
|
11084
12043
|
}
|
|
11085
12044
|
|
|
11086
12045
|
// src/maintenance/sync-main.ts
|
|
11087
|
-
var
|
|
12046
|
+
var import_node_child_process11 = require("child_process");
|
|
11088
12047
|
var import_node_util3 = require("util");
|
|
11089
|
-
var
|
|
12048
|
+
var DEFAULT_TIMEOUT_MS3 = 6e4;
|
|
11090
12049
|
async function git(execFileFn, args, cwd, timeoutMs) {
|
|
11091
12050
|
const exec = (0, import_node_util3.promisify)(execFileFn);
|
|
11092
12051
|
const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
|
|
@@ -11150,8 +12109,8 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
|
|
|
11150
12109
|
}
|
|
11151
12110
|
}
|
|
11152
12111
|
async function syncMain(repoRoot, opts = {}) {
|
|
11153
|
-
const execFileFn = opts.execFileFn ??
|
|
11154
|
-
const timeoutMs = opts.timeoutMs ??
|
|
12112
|
+
const execFileFn = opts.execFileFn ?? import_node_child_process11.execFile;
|
|
12113
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
11155
12114
|
try {
|
|
11156
12115
|
const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
|
|
11157
12116
|
if (!originRef) {
|
|
@@ -11226,6 +12185,463 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11226
12185
|
};
|
|
11227
12186
|
}
|
|
11228
12187
|
}
|
|
12188
|
+
|
|
12189
|
+
// src/sessions/search-index.ts
|
|
12190
|
+
var fs15 = __toESM(require("fs"));
|
|
12191
|
+
var path17 = __toESM(require("path"));
|
|
12192
|
+
var import_better_sqlite32 = __toESM(require("better-sqlite3"));
|
|
12193
|
+
var import_types29 = require("@harness-engineering/types");
|
|
12194
|
+
var SEARCH_INDEX_FILE = "search-index.sqlite";
|
|
12195
|
+
var SCHEMA_SQL2 = `
|
|
12196
|
+
CREATE TABLE IF NOT EXISTS session_docs (
|
|
12197
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12198
|
+
session_id TEXT NOT NULL,
|
|
12199
|
+
archived INTEGER NOT NULL,
|
|
12200
|
+
file_kind TEXT NOT NULL,
|
|
12201
|
+
path TEXT NOT NULL,
|
|
12202
|
+
mtime_ms INTEGER NOT NULL,
|
|
12203
|
+
body TEXT NOT NULL,
|
|
12204
|
+
UNIQUE (session_id, archived, file_kind)
|
|
12205
|
+
);
|
|
12206
|
+
|
|
12207
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_docs_fts USING fts5 (
|
|
12208
|
+
body,
|
|
12209
|
+
content='session_docs',
|
|
12210
|
+
content_rowid='id',
|
|
12211
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
12212
|
+
);
|
|
12213
|
+
|
|
12214
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ai
|
|
12215
|
+
AFTER INSERT ON session_docs
|
|
12216
|
+
BEGIN INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body); END;
|
|
12217
|
+
|
|
12218
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_ad
|
|
12219
|
+
AFTER DELETE ON session_docs
|
|
12220
|
+
BEGIN INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body); END;
|
|
12221
|
+
|
|
12222
|
+
CREATE TRIGGER IF NOT EXISTS session_docs_au
|
|
12223
|
+
AFTER UPDATE ON session_docs
|
|
12224
|
+
BEGIN
|
|
12225
|
+
INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body);
|
|
12226
|
+
INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body);
|
|
12227
|
+
END;
|
|
12228
|
+
`;
|
|
12229
|
+
var DEFAULT_LIMIT = 20;
|
|
12230
|
+
function normalizeFts5Query(query) {
|
|
12231
|
+
const advancedSyntax = /["()*^+]|\bAND\b|\bOR\b|\bNOT\b|[A-Za-z_]+:/;
|
|
12232
|
+
if (advancedSyntax.test(query)) return query;
|
|
12233
|
+
return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
|
|
12234
|
+
}
|
|
12235
|
+
function searchIndexPath(projectPath) {
|
|
12236
|
+
return path17.join(projectPath, ".harness", SEARCH_INDEX_FILE);
|
|
12237
|
+
}
|
|
12238
|
+
var FILE_KIND_TO_FILENAME = {
|
|
12239
|
+
summary: "summary.md",
|
|
12240
|
+
learnings: "learnings.md",
|
|
12241
|
+
failures: "failures.md",
|
|
12242
|
+
sections: "session-sections.md",
|
|
12243
|
+
llm_summary: "llm-summary.md"
|
|
12244
|
+
};
|
|
12245
|
+
var SqliteSearchIndex = class {
|
|
12246
|
+
db;
|
|
12247
|
+
upsertStmt;
|
|
12248
|
+
removeSessionStmt;
|
|
12249
|
+
totalStmt;
|
|
12250
|
+
constructor(dbPath) {
|
|
12251
|
+
fs15.mkdirSync(path17.dirname(dbPath), { recursive: true });
|
|
12252
|
+
this.db = new import_better_sqlite32.default(dbPath);
|
|
12253
|
+
this.db.pragma("journal_mode = WAL");
|
|
12254
|
+
this.db.pragma("synchronous = NORMAL");
|
|
12255
|
+
this.db.exec(SCHEMA_SQL2);
|
|
12256
|
+
this.upsertStmt = this.db.prepare(
|
|
12257
|
+
`INSERT INTO session_docs (session_id, archived, file_kind, path, mtime_ms, body)
|
|
12258
|
+
VALUES (@sessionId, @archived, @fileKind, @path, @mtimeMs, @body)
|
|
12259
|
+
ON CONFLICT(session_id, archived, file_kind) DO UPDATE SET
|
|
12260
|
+
path = excluded.path,
|
|
12261
|
+
mtime_ms = excluded.mtime_ms,
|
|
12262
|
+
body = excluded.body`
|
|
12263
|
+
);
|
|
12264
|
+
this.removeSessionStmt = this.db.prepare(`DELETE FROM session_docs WHERE session_id = ?`);
|
|
12265
|
+
this.totalStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM session_docs`);
|
|
12266
|
+
}
|
|
12267
|
+
upsertSessionDoc(doc) {
|
|
12268
|
+
this.upsertStmt.run({
|
|
12269
|
+
sessionId: doc.sessionId,
|
|
12270
|
+
archived: doc.archived ? 1 : 0,
|
|
12271
|
+
fileKind: doc.fileKind,
|
|
12272
|
+
path: doc.path,
|
|
12273
|
+
mtimeMs: Math.floor(doc.mtimeMs),
|
|
12274
|
+
body: doc.body
|
|
12275
|
+
});
|
|
12276
|
+
}
|
|
12277
|
+
removeSession(sessionId) {
|
|
12278
|
+
const info = this.removeSessionStmt.run(sessionId);
|
|
12279
|
+
return info.changes;
|
|
12280
|
+
}
|
|
12281
|
+
/**
|
|
12282
|
+
* Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
|
|
12283
|
+
* re-walk. Live (archived=0) rows are preserved.
|
|
12284
|
+
*/
|
|
12285
|
+
resetArchived() {
|
|
12286
|
+
this.db.prepare(`DELETE FROM session_docs WHERE archived = 1`).run();
|
|
12287
|
+
}
|
|
12288
|
+
/** Total rows currently indexed (across both live and archived). */
|
|
12289
|
+
totalIndexed() {
|
|
12290
|
+
const row = this.totalStmt.get();
|
|
12291
|
+
return row.n;
|
|
12292
|
+
}
|
|
12293
|
+
/**
|
|
12294
|
+
* Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
|
|
12295
|
+
* FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
|
|
12296
|
+
* is therefore the user-facing language. Errors from malformed queries
|
|
12297
|
+
* surface as thrown `SqliteError` so the CLI can catch + render them.
|
|
12298
|
+
*/
|
|
12299
|
+
search(query, opts = {}) {
|
|
12300
|
+
const limit = opts.limit ?? DEFAULT_LIMIT;
|
|
12301
|
+
const filters = [];
|
|
12302
|
+
const params = { q: normalizeFts5Query(query), limit };
|
|
12303
|
+
if (opts.archivedOnly) {
|
|
12304
|
+
filters.push("d.archived = 1");
|
|
12305
|
+
}
|
|
12306
|
+
const fileKinds = opts.fileKinds && opts.fileKinds.length > 0 ? opts.fileKinds : null;
|
|
12307
|
+
if (fileKinds) {
|
|
12308
|
+
const placeholders = fileKinds.map((_, i) => `@fk${i}`).join(", ");
|
|
12309
|
+
filters.push(`d.file_kind IN (${placeholders})`);
|
|
12310
|
+
fileKinds.forEach((k, i) => {
|
|
12311
|
+
params[`fk${i}`] = k;
|
|
12312
|
+
});
|
|
12313
|
+
}
|
|
12314
|
+
const whereClause = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
|
|
12315
|
+
const sql = `
|
|
12316
|
+
SELECT
|
|
12317
|
+
d.session_id AS sessionId,
|
|
12318
|
+
d.archived AS archived,
|
|
12319
|
+
d.file_kind AS fileKind,
|
|
12320
|
+
d.path AS path,
|
|
12321
|
+
bm25(session_docs_fts) AS bm25,
|
|
12322
|
+
snippet(session_docs_fts, 0, '\u2026', '\u2026', '\u2026', 16) AS snippet
|
|
12323
|
+
FROM session_docs_fts
|
|
12324
|
+
JOIN session_docs d ON d.id = session_docs_fts.rowid
|
|
12325
|
+
WHERE session_docs_fts MATCH @q
|
|
12326
|
+
${whereClause}
|
|
12327
|
+
ORDER BY bm25 ASC
|
|
12328
|
+
LIMIT @limit
|
|
12329
|
+
`;
|
|
12330
|
+
const start = Date.now();
|
|
12331
|
+
const rows = this.db.prepare(sql).all(params);
|
|
12332
|
+
const durationMs = Date.now() - start;
|
|
12333
|
+
const matches = rows.map((r) => ({
|
|
12334
|
+
sessionId: r.sessionId,
|
|
12335
|
+
archived: r.archived === 1,
|
|
12336
|
+
fileKind: r.fileKind,
|
|
12337
|
+
path: r.path,
|
|
12338
|
+
bm25: r.bm25,
|
|
12339
|
+
snippet: r.snippet
|
|
12340
|
+
}));
|
|
12341
|
+
return { matches, durationMs, totalIndexed: this.totalIndexed() };
|
|
12342
|
+
}
|
|
12343
|
+
close() {
|
|
12344
|
+
this.db.close();
|
|
12345
|
+
}
|
|
12346
|
+
};
|
|
12347
|
+
function openSearchIndex(projectPath) {
|
|
12348
|
+
return new SqliteSearchIndex(searchIndexPath(projectPath));
|
|
12349
|
+
}
|
|
12350
|
+
function indexSessionDirectory(idx, args) {
|
|
12351
|
+
const kinds = args.fileKinds ?? [...import_types29.INDEXED_FILE_KINDS];
|
|
12352
|
+
const cap = args.maxBytesPerBody ?? 256 * 1024;
|
|
12353
|
+
let docsWritten = 0;
|
|
12354
|
+
for (const kind of kinds) {
|
|
12355
|
+
const fileName = FILE_KIND_TO_FILENAME[kind];
|
|
12356
|
+
const filePath = path17.join(args.sessionDir, fileName);
|
|
12357
|
+
if (!fs15.existsSync(filePath)) continue;
|
|
12358
|
+
let body = fs15.readFileSync(filePath, "utf8");
|
|
12359
|
+
if (Buffer.byteLength(body, "utf8") > cap) {
|
|
12360
|
+
body = body.slice(0, cap) + "\n\n[TRUNCATED]";
|
|
12361
|
+
}
|
|
12362
|
+
const stat = fs15.statSync(filePath);
|
|
12363
|
+
const relPath = path17.relative(args.projectPath, filePath).replaceAll("\\", "/");
|
|
12364
|
+
idx.upsertSessionDoc({
|
|
12365
|
+
sessionId: args.sessionId,
|
|
12366
|
+
archived: args.archived,
|
|
12367
|
+
fileKind: kind,
|
|
12368
|
+
path: relPath,
|
|
12369
|
+
mtimeMs: stat.mtimeMs,
|
|
12370
|
+
body
|
|
12371
|
+
});
|
|
12372
|
+
docsWritten++;
|
|
12373
|
+
}
|
|
12374
|
+
return { docsWritten };
|
|
12375
|
+
}
|
|
12376
|
+
function reindexFromArchive(projectPath, opts = {}) {
|
|
12377
|
+
const start = Date.now();
|
|
12378
|
+
const archiveBase = path17.join(projectPath, ".harness", "archive", "sessions");
|
|
12379
|
+
const idx = openSearchIndex(projectPath);
|
|
12380
|
+
try {
|
|
12381
|
+
idx.resetArchived();
|
|
12382
|
+
let sessionsIndexed = 0;
|
|
12383
|
+
let docsWritten = 0;
|
|
12384
|
+
if (fs15.existsSync(archiveBase)) {
|
|
12385
|
+
const entries = fs15.readdirSync(archiveBase, { withFileTypes: true });
|
|
12386
|
+
for (const entry of entries) {
|
|
12387
|
+
if (!entry.isDirectory()) continue;
|
|
12388
|
+
const sessionDir = path17.join(archiveBase, entry.name);
|
|
12389
|
+
const result = indexSessionDirectory(idx, {
|
|
12390
|
+
sessionId: entry.name,
|
|
12391
|
+
sessionDir,
|
|
12392
|
+
archived: true,
|
|
12393
|
+
projectPath,
|
|
12394
|
+
...opts.fileKinds && { fileKinds: opts.fileKinds },
|
|
12395
|
+
...opts.maxBytesPerBody !== void 0 && { maxBytesPerBody: opts.maxBytesPerBody }
|
|
12396
|
+
});
|
|
12397
|
+
if (result.docsWritten > 0) sessionsIndexed++;
|
|
12398
|
+
docsWritten += result.docsWritten;
|
|
12399
|
+
}
|
|
12400
|
+
}
|
|
12401
|
+
return { sessionsIndexed, docsWritten, durationMs: Date.now() - start };
|
|
12402
|
+
} finally {
|
|
12403
|
+
idx.close();
|
|
12404
|
+
}
|
|
12405
|
+
}
|
|
12406
|
+
|
|
12407
|
+
// src/sessions/summarize.ts
|
|
12408
|
+
var fs16 = __toESM(require("fs"));
|
|
12409
|
+
var path18 = __toESM(require("path"));
|
|
12410
|
+
var import_types30 = require("@harness-engineering/types");
|
|
12411
|
+
var import_types31 = require("@harness-engineering/types");
|
|
12412
|
+
var LLM_SUMMARY_FILE = "llm-summary.md";
|
|
12413
|
+
var SUMMARY_INPUT_FILES = [
|
|
12414
|
+
{ filename: "summary.md", kind: "summary" },
|
|
12415
|
+
{ filename: "learnings.md", kind: "learnings" },
|
|
12416
|
+
{ filename: "failures.md", kind: "failures" },
|
|
12417
|
+
{ filename: "session-sections.md", kind: "sections" }
|
|
12418
|
+
];
|
|
12419
|
+
var DEFAULT_INPUT_BUDGET_TOKENS = 16e3;
|
|
12420
|
+
var DEFAULT_TIMEOUT_MS4 = 6e4;
|
|
12421
|
+
var CHARS_PER_TOKEN = 4;
|
|
12422
|
+
var SYSTEM_PROMPT = `You produce concise, structured retrospectives of completed harness-engineering sessions.
|
|
12423
|
+
|
|
12424
|
+
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.`;
|
|
12425
|
+
var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-engineering session. Produce a structured summary capturing:
|
|
12426
|
+
- headline: one-sentence retrospective (\u2264 120 chars)
|
|
12427
|
+
- keyOutcomes: concrete things that shipped / decisions made (\u2264 20 strings)
|
|
12428
|
+
- openQuestions: items still open (\u2264 20 strings)
|
|
12429
|
+
- relatedSessions: other session slugs referenced (may be empty)
|
|
12430
|
+
|
|
12431
|
+
---
|
|
12432
|
+
|
|
12433
|
+
`;
|
|
12434
|
+
function readInputCorpus(archiveDir) {
|
|
12435
|
+
const parts = [];
|
|
12436
|
+
for (const { filename, kind } of SUMMARY_INPUT_FILES) {
|
|
12437
|
+
const p = path18.join(archiveDir, filename);
|
|
12438
|
+
if (!fs16.existsSync(p)) continue;
|
|
12439
|
+
try {
|
|
12440
|
+
const content = fs16.readFileSync(p, "utf8");
|
|
12441
|
+
if (content.trim().length === 0) continue;
|
|
12442
|
+
parts.push(`## FILE: ${kind}
|
|
12443
|
+
|
|
12444
|
+
${content.trim()}`);
|
|
12445
|
+
} catch {
|
|
12446
|
+
}
|
|
12447
|
+
}
|
|
12448
|
+
return parts.join("\n\n");
|
|
12449
|
+
}
|
|
12450
|
+
function truncateForBudget(text, inputBudgetTokens) {
|
|
12451
|
+
const cap = Math.max(0, inputBudgetTokens * CHARS_PER_TOKEN);
|
|
12452
|
+
if (text.length <= cap) return text;
|
|
12453
|
+
return text.slice(0, cap) + "\n\n[TRUNCATED \u2014 input exceeded token budget]";
|
|
12454
|
+
}
|
|
12455
|
+
function renderLlmSummaryMarkdown(summary, meta) {
|
|
12456
|
+
const lines = [
|
|
12457
|
+
"---",
|
|
12458
|
+
`generatedAt: ${meta.generatedAt}`,
|
|
12459
|
+
`model: ${meta.model}`,
|
|
12460
|
+
`inputTokens: ${meta.inputTokens}`,
|
|
12461
|
+
`outputTokens: ${meta.outputTokens}`,
|
|
12462
|
+
`schemaVersion: ${meta.schemaVersion}`,
|
|
12463
|
+
"---",
|
|
12464
|
+
"",
|
|
12465
|
+
"## Headline",
|
|
12466
|
+
summary.headline,
|
|
12467
|
+
"",
|
|
12468
|
+
"## Key outcomes"
|
|
12469
|
+
];
|
|
12470
|
+
if (summary.keyOutcomes.length === 0) {
|
|
12471
|
+
lines.push("_(none)_");
|
|
12472
|
+
} else {
|
|
12473
|
+
for (const item of summary.keyOutcomes) lines.push(`- ${item}`);
|
|
12474
|
+
}
|
|
12475
|
+
lines.push("", "## Open questions");
|
|
12476
|
+
if (summary.openQuestions.length === 0) {
|
|
12477
|
+
lines.push("_(none)_");
|
|
12478
|
+
} else {
|
|
12479
|
+
for (const item of summary.openQuestions) lines.push(`- ${item}`);
|
|
12480
|
+
}
|
|
12481
|
+
lines.push("", "## Related sessions");
|
|
12482
|
+
if (summary.relatedSessions.length === 0) {
|
|
12483
|
+
lines.push("_(none)_");
|
|
12484
|
+
} else {
|
|
12485
|
+
for (const item of summary.relatedSessions) lines.push(`- ${item}`);
|
|
12486
|
+
}
|
|
12487
|
+
lines.push("");
|
|
12488
|
+
return lines.join("\n");
|
|
12489
|
+
}
|
|
12490
|
+
function writeStubMarkdown(archiveDir, reason) {
|
|
12491
|
+
const filePath = path18.join(archiveDir, LLM_SUMMARY_FILE);
|
|
12492
|
+
const body = `---
|
|
12493
|
+
generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
12494
|
+
schemaVersion: 1
|
|
12495
|
+
status: failed
|
|
12496
|
+
---
|
|
12497
|
+
|
|
12498
|
+
## Summary unavailable
|
|
12499
|
+
|
|
12500
|
+
- reason: ${reason}
|
|
12501
|
+
`;
|
|
12502
|
+
fs16.writeFileSync(filePath, body, "utf8");
|
|
12503
|
+
return filePath;
|
|
12504
|
+
}
|
|
12505
|
+
async function summarizeArchivedSession(ctx) {
|
|
12506
|
+
const writeStubOnError = ctx.writeStubOnError ?? true;
|
|
12507
|
+
if (!fs16.existsSync(ctx.archiveDir)) {
|
|
12508
|
+
return (0, import_types31.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
|
|
12509
|
+
}
|
|
12510
|
+
const corpus = readInputCorpus(ctx.archiveDir);
|
|
12511
|
+
if (corpus.trim().length === 0) {
|
|
12512
|
+
return (0, import_types31.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
|
|
12513
|
+
}
|
|
12514
|
+
const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
|
|
12515
|
+
const truncated = truncateForBudget(corpus, inputBudgetTokens);
|
|
12516
|
+
const prompt = USER_PROMPT_PREAMBLE + truncated;
|
|
12517
|
+
const timeoutMs = ctx.config?.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
|
|
12518
|
+
const analyzeOpts = {
|
|
12519
|
+
prompt,
|
|
12520
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
12521
|
+
responseSchema: import_types30.SessionSummarySchema,
|
|
12522
|
+
...ctx.config?.model && { model: ctx.config.model }
|
|
12523
|
+
};
|
|
12524
|
+
let response;
|
|
12525
|
+
try {
|
|
12526
|
+
response = await Promise.race([
|
|
12527
|
+
ctx.provider.analyze(analyzeOpts),
|
|
12528
|
+
new Promise(
|
|
12529
|
+
(_, reject) => setTimeout(
|
|
12530
|
+
() => reject(new Error(`provider call timed out after ${timeoutMs}ms`)),
|
|
12531
|
+
timeoutMs
|
|
12532
|
+
)
|
|
12533
|
+
)
|
|
12534
|
+
]);
|
|
12535
|
+
} catch (e) {
|
|
12536
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
12537
|
+
ctx.logger?.warn?.("session summary: provider call failed", { reason });
|
|
12538
|
+
let stubPath;
|
|
12539
|
+
if (writeStubOnError) {
|
|
12540
|
+
try {
|
|
12541
|
+
stubPath = writeStubMarkdown(ctx.archiveDir, reason);
|
|
12542
|
+
} catch {
|
|
12543
|
+
}
|
|
12544
|
+
}
|
|
12545
|
+
return (0, import_types31.Err)(
|
|
12546
|
+
new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
|
|
12547
|
+
);
|
|
12548
|
+
}
|
|
12549
|
+
const parsed = import_types30.SessionSummarySchema.safeParse(response.result);
|
|
12550
|
+
if (!parsed.success) {
|
|
12551
|
+
const reason = `schema validation failed: ${parsed.error.message}`;
|
|
12552
|
+
ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
|
|
12553
|
+
if (writeStubOnError) {
|
|
12554
|
+
try {
|
|
12555
|
+
writeStubMarkdown(ctx.archiveDir, reason);
|
|
12556
|
+
} catch {
|
|
12557
|
+
}
|
|
12558
|
+
}
|
|
12559
|
+
return (0, import_types31.Err)(new Error(reason));
|
|
12560
|
+
}
|
|
12561
|
+
const meta = {
|
|
12562
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
12563
|
+
model: response.model,
|
|
12564
|
+
inputTokens: response.tokenUsage.inputTokens,
|
|
12565
|
+
outputTokens: response.tokenUsage.outputTokens,
|
|
12566
|
+
schemaVersion: 1
|
|
12567
|
+
};
|
|
12568
|
+
const filePath = path18.join(ctx.archiveDir, LLM_SUMMARY_FILE);
|
|
12569
|
+
const body = renderLlmSummaryMarkdown(parsed.data, meta);
|
|
12570
|
+
fs16.writeFileSync(filePath, body, "utf8");
|
|
12571
|
+
return (0, import_types31.Ok)({ summary: parsed.data, meta, filePath });
|
|
12572
|
+
}
|
|
12573
|
+
function isSummaryEnabled(config) {
|
|
12574
|
+
if (!config) return false;
|
|
12575
|
+
if (config.enabled === false) return false;
|
|
12576
|
+
return true;
|
|
12577
|
+
}
|
|
12578
|
+
|
|
12579
|
+
// src/sessions/archive-hooks.ts
|
|
12580
|
+
var defaultLogger = {
|
|
12581
|
+
warn: (msg, meta) => console.warn(`[sessions] ${msg}`, meta)
|
|
12582
|
+
};
|
|
12583
|
+
async function runSummaryStep(opts, logger, sessionId, archiveDir) {
|
|
12584
|
+
const enabled = isSummaryEnabled(opts.config?.summary) && opts.provider != null;
|
|
12585
|
+
if (!enabled || !opts.provider) return;
|
|
12586
|
+
const ctx = {
|
|
12587
|
+
archiveDir,
|
|
12588
|
+
provider: opts.provider,
|
|
12589
|
+
...opts.config?.summary && { config: opts.config.summary },
|
|
12590
|
+
...logger && { logger }
|
|
12591
|
+
};
|
|
12592
|
+
try {
|
|
12593
|
+
const result = await summarizeArchivedSession(ctx);
|
|
12594
|
+
if (!result.ok) {
|
|
12595
|
+
logger.warn?.("session summary: failed", {
|
|
12596
|
+
sessionId,
|
|
12597
|
+
error: result.error.message
|
|
12598
|
+
});
|
|
12599
|
+
}
|
|
12600
|
+
} catch (e) {
|
|
12601
|
+
logger.warn?.("session summary: threw", {
|
|
12602
|
+
sessionId,
|
|
12603
|
+
error: e instanceof Error ? e.message : String(e)
|
|
12604
|
+
});
|
|
12605
|
+
}
|
|
12606
|
+
}
|
|
12607
|
+
function runIndexStep(opts, logger, sessionId, archiveDir) {
|
|
12608
|
+
try {
|
|
12609
|
+
const idx = openSearchIndex(opts.projectPath);
|
|
12610
|
+
try {
|
|
12611
|
+
const result = indexSessionDirectory(idx, {
|
|
12612
|
+
sessionId,
|
|
12613
|
+
sessionDir: archiveDir,
|
|
12614
|
+
archived: true,
|
|
12615
|
+
projectPath: opts.projectPath,
|
|
12616
|
+
...opts.config?.search?.indexedFileKinds && {
|
|
12617
|
+
fileKinds: opts.config.search.indexedFileKinds
|
|
12618
|
+
},
|
|
12619
|
+
...opts.config?.search?.maxIndexBytesPerFile !== void 0 && {
|
|
12620
|
+
maxBytesPerBody: opts.config.search.maxIndexBytesPerFile
|
|
12621
|
+
}
|
|
12622
|
+
});
|
|
12623
|
+
if (result.docsWritten === 0) {
|
|
12624
|
+
logger.warn?.("session index: no docs written", { sessionId, archiveDir });
|
|
12625
|
+
}
|
|
12626
|
+
} finally {
|
|
12627
|
+
idx.close();
|
|
12628
|
+
}
|
|
12629
|
+
} catch (e) {
|
|
12630
|
+
logger.warn?.("session index: failed", {
|
|
12631
|
+
sessionId,
|
|
12632
|
+
error: e instanceof Error ? e.message : String(e)
|
|
12633
|
+
});
|
|
12634
|
+
}
|
|
12635
|
+
}
|
|
12636
|
+
function buildArchiveHooks(opts) {
|
|
12637
|
+
const logger = opts.logger ?? defaultLogger;
|
|
12638
|
+
return {
|
|
12639
|
+
async onArchived({ sessionId, archiveDir }) {
|
|
12640
|
+
await runSummaryStep(opts, logger, sessionId, archiveDir);
|
|
12641
|
+
runIndexStep(opts, logger, sessionId, archiveDir);
|
|
12642
|
+
}
|
|
12643
|
+
};
|
|
12644
|
+
}
|
|
11229
12645
|
// Annotate the CommonJS export names for ESM import in node:
|
|
11230
12646
|
0 && (module.exports = {
|
|
11231
12647
|
AnalysisArchive,
|
|
@@ -11242,6 +12658,10 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11242
12658
|
PromptRenderer,
|
|
11243
12659
|
RETRY_DELAYS_MS,
|
|
11244
12660
|
RoadmapTrackerAdapter,
|
|
12661
|
+
SinkConfigError,
|
|
12662
|
+
SinkRegistry,
|
|
12663
|
+
SlackSink,
|
|
12664
|
+
SqliteSearchIndex,
|
|
11245
12665
|
StreamRecorder,
|
|
11246
12666
|
TokenStore,
|
|
11247
12667
|
WebhookQueue,
|
|
@@ -11250,6 +12670,7 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11250
12670
|
WorkspaceManager,
|
|
11251
12671
|
applyEvent,
|
|
11252
12672
|
artifactPresenceFromIssue,
|
|
12673
|
+
buildArchiveHooks,
|
|
11253
12674
|
calculateRetryDelay,
|
|
11254
12675
|
canDispatch,
|
|
11255
12676
|
computeRateLimitDelay,
|
|
@@ -11261,20 +12682,31 @@ async function syncMain(repoRoot, opts = {}) {
|
|
|
11261
12682
|
getAvailableSlots,
|
|
11262
12683
|
getDefaultConfig,
|
|
11263
12684
|
getPerStateCount,
|
|
12685
|
+
indexSessionDirectory,
|
|
11264
12686
|
isEligible,
|
|
12687
|
+
isSummaryEnabled,
|
|
11265
12688
|
launchTUI,
|
|
11266
12689
|
loadPublishedIndex,
|
|
11267
12690
|
migrateAgentConfig,
|
|
12691
|
+
normalizeFts5Query,
|
|
12692
|
+
openSearchIndex,
|
|
11268
12693
|
reconcile,
|
|
12694
|
+
reindexFromArchive,
|
|
11269
12695
|
renderAnalysisComment,
|
|
12696
|
+
renderLlmSummaryMarkdown,
|
|
11270
12697
|
renderPRComment,
|
|
11271
12698
|
resolveEscalationConfig,
|
|
11272
12699
|
resolveOrchestratorId,
|
|
11273
12700
|
routeIssue,
|
|
11274
12701
|
savePublishedIndex,
|
|
12702
|
+
searchIndexPath,
|
|
11275
12703
|
selectCandidates,
|
|
11276
12704
|
sortCandidates,
|
|
12705
|
+
summarizeArchivedSession,
|
|
11277
12706
|
syncMain,
|
|
11278
12707
|
triageIssue,
|
|
11279
|
-
|
|
12708
|
+
truncateForBudget,
|
|
12709
|
+
validateWorkflowConfig,
|
|
12710
|
+
wireNotificationSinks,
|
|
12711
|
+
wrapAsEnvelope
|
|
11280
12712
|
});
|