@harness-engineering/orchestrator 0.4.6 → 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.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
- validateWorkflowConfig: () => validateWorkflowConfig
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 = (path17, name) => {
1970
+ const checkRef = (path19, name) => {
1955
1971
  if (name !== void 0 && !names.has(name)) {
1956
1972
  issues.push({
1957
- path: path17,
1958
- message: `routing.${path17.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
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 import_node_crypto15 = require("crypto");
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 path17 of presentLegacy) {
3705
- if (CASE1_ALWAYS_SUPPRESS.has(path17)) continue;
3706
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path17)) continue;
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 '${path17}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
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
- (path17) => `Deprecated config field '${path17}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
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 = (path17, name) => {
3849
- if (name !== void 0 && !known.has(name)) missing.push({ path: path17, name });
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: path17, name }) => `routing.${path17} -> '${name}'`).join("; ");
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 import_types16 = require("@harness-engineering/types");
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 sanitizeExtraArgs(extraArgs) {
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, import_types16.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
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 = sanitizeExtraArgs(this.containerConfig.extraArgs);
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, import_types16.Err)(
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, import_types16.Err)(
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, import_types16.Err)({
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 import_node_child_process5 = require("child_process");
5164
- var import_types17 = require("@harness-engineering/types");
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, import_node_child_process5.execFile)("docker", args, (error, stdout) => {
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, import_types17.Ok)({ containerId, runtime: this.name });
5784
+ return (0, import_types19.Ok)({ containerId, runtime: this.name });
5198
5785
  } catch (error) {
5199
- return (0, import_types17.Err)({
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, import_node_child_process5.spawn)("docker", execArgs);
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, import_types17.Ok)(void 0);
5830
+ return (0, import_types19.Ok)(void 0);
5244
5831
  } catch (error) {
5245
- return (0, import_types17.Err)({
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, import_types17.Ok)(void 0);
5842
+ return (0, import_types19.Ok)(void 0);
5256
5843
  } catch (error) {
5257
- return (0, import_types17.Err)({
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 import_types18 = require("@harness-engineering/types");
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, import_types18.Err)({
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, import_types18.Ok)(secrets);
5870
+ return (0, import_types20.Ok)(secrets);
5284
5871
  }
5285
5872
  async healthCheck() {
5286
- return (0, import_types18.Ok)(void 0);
5873
+ return (0, import_types20.Ok)(void 0);
5287
5874
  }
5288
5875
  };
5289
5876
 
5290
5877
  // src/agent/secrets/onepassword.ts
5291
- var import_node_child_process6 = require("child_process");
5292
- var import_types19 = require("@harness-engineering/types");
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, import_node_child_process6.execFile)("op", args, (error, stdout) => {
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, import_types19.Err)({
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, import_types19.Ok)(secrets);
5911
+ return (0, import_types21.Ok)(secrets);
5325
5912
  }
5326
5913
  async healthCheck() {
5327
5914
  try {
5328
5915
  await opExec(["--version"]);
5329
- return (0, import_types19.Ok)(void 0);
5916
+ return (0, import_types21.Ok)(void 0);
5330
5917
  } catch (error) {
5331
- return (0, import_types19.Err)({
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 import_node_child_process7 = require("child_process");
5341
- var import_types20 = require("@harness-engineering/types");
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, import_node_child_process7.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
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, import_types20.Err)({ category, message: `Failed to read from Vault: ${msg}` });
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, import_types20.Err)({
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, import_types20.Ok)(secrets);
5971
+ return (0, import_types22.Ok)(secrets);
5385
5972
  }
5386
5973
  async healthCheck() {
5387
5974
  try {
5388
5975
  await vaultExec(["version"]);
5389
- return (0, import_types20.Ok)(void 0);
5976
+ return (0, import_types22.Ok)(void 0);
5390
5977
  } catch (error) {
5391
- return (0, import_types20.Err)({
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 import_node_child_process8 = require("child_process");
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, import_node_child_process8.spawn)(command, args, { env: buildChildEnv(), stdio: "pipe" });
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 import_types21 = require("@harness-engineering/types");
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) => import_types21.WebhookSubscriptionPublicSchema.parse(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 import_types22 = require("@harness-engineering/types");
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(import_types22.TokenScopeSchema).min(1),
7130
- bridgeKind: import_types22.BridgeKindSchema.optional(),
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 = import_types22.AuthTokenPublicSchema.parse(result.record);
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 import_types23 = require("@harness-engineering/types");
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(path17) {
7388
- this.path = path17;
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 = import_types23.AuthTokenSchema.safeParse(entry);
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) => import_types23.AuthTokenPublicSchema.parse(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 import_types24 = require("@harness-engineering/types");
8082
+ var import_types26 = require("@harness-engineering/types");
7494
8083
  var AuditLogger = class {
7495
- constructor(path17, opts = {}) {
7496
- this.path = path17;
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 = import_types24.AuthAuditEntrySchema.parse({
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, path17) {
8180
+ function requiredBridgeScope(method, path19) {
7592
8181
  for (const r of V1_BRIDGE_ROUTES) {
7593
- if (r.method === method && r.pattern.test(path17)) return r.scope;
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, path17) {
7604
- const bridgeScope = requiredBridgeScope(method, path17);
8192
+ function requiredScopeForRoute(method, path19) {
8193
+ const bridgeScope = requiredBridgeScope(method, path19);
7605
8194
  if (bridgeScope) return bridgeScope;
7606
- if (path17 === "/api/v1/auth/token" && method === "POST") return "admin";
7607
- if (path17 === "/api/v1/auth/tokens" && method === "GET") return "admin";
7608
- if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path17) && method === "DELETE") return "admin";
7609
- if ((path17 === "/api/state" || path17 === "/api/v1/state") && method === "GET") return "read-status";
7610
- if (path17.startsWith("/api/interactions")) return "resolve-interaction";
7611
- if (path17.startsWith("/api/plans")) return "read-status";
7612
- if (path17.startsWith("/api/analyze") || path17.startsWith("/api/analyses")) return "read-status";
7613
- if (path17.startsWith("/api/roadmap-actions")) return "modify-roadmap";
7614
- if (path17.startsWith("/api/dispatch-actions")) return "trigger-job";
7615
- if (path17.startsWith("/api/local-model") || path17.startsWith("/api/local-models"))
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 (path17.startsWith("/api/maintenance")) return "trigger-job";
7618
- if (path17.startsWith("/api/streams")) return "read-status";
7619
- if (path17.startsWith("/api/sessions")) return "read-status";
7620
- if (path17.startsWith("/api/chat-proxy")) return "trigger-job";
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 import_types25 = require("@harness-engineering/types");
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(path17) {
8025
- this.path = path17;
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 = import_types25.WebhookSubscriptionSchema.safeParse(entry);
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 import_types26 = require("@harness-engineering/types");
10077
+ var import_types28 = require("@harness-engineering/types");
9160
10078
  var SingleProcessLeaderElector = class {
9161
10079
  async electLeader() {
9162
- return (0, import_types26.Ok)("claimed");
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, import_node_crypto15.randomUUID)()}`,
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, import_node_crypto15.randomUUID)()}`,
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 import_node_child_process9 = require("child_process");
12046
+ var import_node_child_process11 = require("child_process");
11088
12047
  var import_node_util3 = require("util");
11089
- var DEFAULT_TIMEOUT_MS2 = 6e4;
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 ?? import_node_child_process9.execFile;
11154
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
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
- validateWorkflowConfig
12708
+ truncateForBudget,
12709
+ validateWorkflowConfig,
12710
+ wireNotificationSinks,
12711
+ wrapAsEnvelope
11280
12712
  });