@harness-engineering/orchestrator 0.4.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,8 +31,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  AnalysisArchive: () => AnalysisArchive,
34
+ BUILT_IN_TASKS: () => BUILT_IN_TASKS,
34
35
  BackendRouter: () => BackendRouter,
35
36
  ClaimManager: () => ClaimManager,
37
+ GateNotReadyError: () => GateNotReadyError,
38
+ GateRunError: () => GateRunError,
36
39
  InteractionQueue: () => InteractionQueue,
37
40
  LinearGraphQLStub: () => LinearGraphQLStub,
38
41
  MAX_ATTEMPTS: () => MAX_ATTEMPTS,
@@ -41,10 +44,16 @@ __export(index_exports, {
41
44
  Orchestrator: () => Orchestrator,
42
45
  OrchestratorBackendFactory: () => OrchestratorBackendFactory,
43
46
  PRDetector: () => PRDetector,
47
+ PromotionError: () => PromotionError,
44
48
  PromptRenderer: () => PromptRenderer,
45
49
  RETRY_DELAYS_MS: () => RETRY_DELAYS_MS,
46
50
  RoadmapTrackerAdapter: () => RoadmapTrackerAdapter,
51
+ SinkConfigError: () => SinkConfigError,
52
+ SinkRegistry: () => SinkRegistry,
53
+ SlackSink: () => SlackSink,
54
+ SqliteSearchIndex: () => SqliteSearchIndex,
47
55
  StreamRecorder: () => StreamRecorder,
56
+ TaskOutputStore: () => TaskOutputStore,
48
57
  TokenStore: () => TokenStore,
49
58
  WebhookQueue: () => WebhookQueue,
50
59
  WorkflowLoader: () => WorkflowLoader,
@@ -52,33 +61,51 @@ __export(index_exports, {
52
61
  WorkspaceManager: () => WorkspaceManager,
53
62
  applyEvent: () => applyEvent,
54
63
  artifactPresenceFromIssue: () => artifactPresenceFromIssue,
64
+ buildArchiveHooks: () => buildArchiveHooks,
55
65
  calculateRetryDelay: () => calculateRetryDelay,
56
66
  canDispatch: () => canDispatch,
57
67
  computeRateLimitDelay: () => computeRateLimitDelay,
58
68
  createBackend: () => createBackend,
59
69
  createEmptyState: () => createEmptyState,
60
70
  detectScopeTier: () => detectScopeTier,
71
+ emitProposalApproved: () => emitProposalApproved,
72
+ emitProposalCreated: () => emitProposalCreated,
73
+ emitProposalRejected: () => emitProposalRejected,
61
74
  extractHighlights: () => extractHighlights,
62
75
  extractTitlePrefix: () => extractTitlePrefix,
63
76
  getAvailableSlots: () => getAvailableSlots,
64
77
  getDefaultConfig: () => getDefaultConfig,
65
78
  getPerStateCount: () => getPerStateCount,
79
+ indexSessionDirectory: () => indexSessionDirectory,
66
80
  isEligible: () => isEligible,
81
+ isSummaryEnabled: () => isSummaryEnabled,
67
82
  launchTUI: () => launchTUI,
68
83
  loadPublishedIndex: () => loadPublishedIndex,
69
84
  migrateAgentConfig: () => migrateAgentConfig,
85
+ normalizeFts5Query: () => normalizeFts5Query,
86
+ openSearchIndex: () => openSearchIndex,
87
+ promote: () => promote,
70
88
  reconcile: () => reconcile,
89
+ reindexFromArchive: () => reindexFromArchive,
71
90
  renderAnalysisComment: () => renderAnalysisComment,
91
+ renderLlmSummaryMarkdown: () => renderLlmSummaryMarkdown,
72
92
  renderPRComment: () => renderPRComment,
73
93
  resolveEscalationConfig: () => resolveEscalationConfig,
74
94
  resolveOrchestratorId: () => resolveOrchestratorId,
75
95
  routeIssue: () => routeIssue,
96
+ runGate: () => runGate,
76
97
  savePublishedIndex: () => savePublishedIndex,
98
+ searchIndexPath: () => searchIndexPath,
77
99
  selectCandidates: () => selectCandidates,
78
100
  sortCandidates: () => sortCandidates,
101
+ summarizeArchivedSession: () => summarizeArchivedSession,
79
102
  syncMain: () => syncMain,
80
103
  triageIssue: () => triageIssue,
81
- validateWorkflowConfig: () => validateWorkflowConfig
104
+ truncateForBudget: () => truncateForBudget,
105
+ validateCustomTasks: () => validateCustomTasks,
106
+ validateWorkflowConfig: () => validateWorkflowConfig,
107
+ wireNotificationSinks: () => wireNotificationSinks,
108
+ wrapAsEnvelope: () => wrapAsEnvelope
82
109
  });
83
110
  module.exports = __toCommonJS(index_exports);
84
111
 
@@ -1228,7 +1255,7 @@ var ClaimManager = class {
1228
1255
  const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
1229
1256
  if (!claimResult.ok) return claimResult;
1230
1257
  if (this.verifyDelayMs > 0) {
1231
- await new Promise((resolve6) => setTimeout(resolve6, this.verifyDelayMs));
1258
+ await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
1232
1259
  }
1233
1260
  const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
1234
1261
  if (!statesResult.ok) return statesResult;
@@ -1951,11 +1978,11 @@ var BackendsMapSchema = import_zod2.z.record(import_zod2.z.string(), BackendDefS
1951
1978
  function crossFieldRoutingIssues(backends, routing) {
1952
1979
  const issues = [];
1953
1980
  const names = new Set(Object.keys(backends));
1954
- const checkRef = (path17, name) => {
1981
+ const checkRef = (path22, name) => {
1955
1982
  if (name !== void 0 && !names.has(name)) {
1956
1983
  issues.push({
1957
- path: path17,
1958
- message: `routing.${path17.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1984
+ path: path22,
1985
+ message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1959
1986
  });
1960
1987
  }
1961
1988
  };
@@ -2619,7 +2646,7 @@ var WorkspaceHooks = class {
2619
2646
  if (!command) {
2620
2647
  return (0, import_types7.Ok)(void 0);
2621
2648
  }
2622
- return new Promise((resolve6) => {
2649
+ return new Promise((resolve7) => {
2623
2650
  const filteredEnv = {};
2624
2651
  for (const [k, v] of Object.entries(process.env)) {
2625
2652
  if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
@@ -2632,19 +2659,19 @@ var WorkspaceHooks = class {
2632
2659
  });
2633
2660
  const timeout = setTimeout(() => {
2634
2661
  child.kill();
2635
- resolve6((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2662
+ resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2636
2663
  }, this.config.timeoutMs);
2637
2664
  child.on("exit", (code) => {
2638
2665
  clearTimeout(timeout);
2639
2666
  if (code === 0) {
2640
- resolve6((0, import_types7.Ok)(void 0));
2667
+ resolve7((0, import_types7.Ok)(void 0));
2641
2668
  } else {
2642
- resolve6((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2669
+ resolve7((0, import_types7.Err)(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2643
2670
  }
2644
2671
  });
2645
2672
  child.on("error", (error) => {
2646
2673
  clearTimeout(timeout);
2647
- resolve6((0, import_types7.Err)(error));
2674
+ resolve7((0, import_types7.Err)(error));
2648
2675
  });
2649
2676
  });
2650
2677
  }
@@ -2682,7 +2709,7 @@ var MockBackend = class {
2682
2709
  content: "Thinking...",
2683
2710
  sessionId: session.sessionId
2684
2711
  };
2685
- await new Promise((resolve6) => setTimeout(resolve6, 100));
2712
+ await new Promise((resolve7) => setTimeout(resolve7, 100));
2686
2713
  yield {
2687
2714
  type: "thought",
2688
2715
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2734,9 +2761,9 @@ var PromptRenderer = class {
2734
2761
 
2735
2762
  // src/orchestrator.ts
2736
2763
  var import_node_events = require("events");
2737
- var path16 = __toESM(require("path"));
2738
- var import_node_crypto15 = require("crypto");
2739
- var import_core11 = require("@harness-engineering/core");
2764
+ var path19 = __toESM(require("path"));
2765
+ var import_node_crypto16 = require("crypto");
2766
+ var import_core14 = require("@harness-engineering/core");
2740
2767
 
2741
2768
  // src/intelligence/pipeline-runner.ts
2742
2769
  var path7 = __toESM(require("path"));
@@ -3297,7 +3324,7 @@ var CompletionHandler = class {
3297
3324
  };
3298
3325
 
3299
3326
  // src/orchestrator.ts
3300
- var import_core12 = require("@harness-engineering/core");
3327
+ var import_core15 = require("@harness-engineering/core");
3301
3328
 
3302
3329
  // src/tracker/adapters/github-issues-issue-tracker.ts
3303
3330
  var import_types9 = require("@harness-engineering/types");
@@ -3701,11 +3728,11 @@ function detectLegacyFields(agent) {
3701
3728
  }
3702
3729
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3703
3730
  const warnings = [];
3704
- for (const path17 of presentLegacy) {
3705
- if (CASE1_ALWAYS_SUPPRESS.has(path17)) continue;
3706
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path17)) continue;
3731
+ for (const path22 of presentLegacy) {
3732
+ if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
3733
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
3707
3734
  warnings.push(
3708
- `Ignoring legacy field '${path17}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3735
+ `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3709
3736
  );
3710
3737
  }
3711
3738
  return warnings;
@@ -3733,7 +3760,7 @@ function migrateAgentConfig(agent) {
3733
3760
  }
3734
3761
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3735
3762
  const warnings = presentLegacy.map(
3736
- (path17) => `Deprecated config field '${path17}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3763
+ (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3737
3764
  );
3738
3765
  return {
3739
3766
  config: { ...agent, backends, routing },
@@ -3822,6 +3849,10 @@ var BackendRouter = class {
3822
3849
  const intel = this.routing.intelligence;
3823
3850
  return intel?.[useCase.layer] ?? this.routing.default;
3824
3851
  }
3852
+ case "isolation": {
3853
+ const iso = this.routing.isolation;
3854
+ return iso?.[useCase.tier] ?? this.routing.default;
3855
+ }
3825
3856
  case "maintenance":
3826
3857
  case "chat":
3827
3858
  return this.routing.default;
@@ -3845,8 +3876,8 @@ var BackendRouter = class {
3845
3876
  validateReferences() {
3846
3877
  const known = new Set(Object.keys(this.backends));
3847
3878
  const missing = [];
3848
- const check = (path17, name) => {
3849
- if (name !== void 0 && !known.has(name)) missing.push({ path: path17, name });
3879
+ const check = (path22, name) => {
3880
+ if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
3850
3881
  };
3851
3882
  check("default", this.routing.default);
3852
3883
  check("quick-fix", this.routing["quick-fix"]);
@@ -3855,8 +3886,11 @@ var BackendRouter = class {
3855
3886
  check("diagnostic", this.routing.diagnostic);
3856
3887
  check("intelligence.sel", this.routing.intelligence?.sel);
3857
3888
  check("intelligence.pesl", this.routing.intelligence?.pesl);
3889
+ check("isolation.none", this.routing.isolation?.none);
3890
+ check("isolation.container", this.routing.isolation?.container);
3891
+ check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
3858
3892
  if (missing.length > 0) {
3859
- const detail = missing.map(({ path: path17, name }) => `routing.${path17} -> '${name}'`).join("; ");
3893
+ const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
3860
3894
  const known_ = [...known].join(", ") || "(none)";
3861
3895
  throw new Error(
3862
3896
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3870,11 +3904,11 @@ var import_node_child_process4 = require("child_process");
3870
3904
  var readline = __toESM(require("readline"));
3871
3905
  var import_node_crypto3 = require("crypto");
3872
3906
  var import_types10 = require("@harness-engineering/types");
3873
- function resolveExitCode(code, command, resolve6) {
3907
+ function resolveExitCode(code, command, resolve7) {
3874
3908
  if (code === 0) {
3875
- resolve6((0, import_types10.Ok)(void 0));
3909
+ resolve7((0, import_types10.Ok)(void 0));
3876
3910
  } else {
3877
- resolve6(
3911
+ resolve7(
3878
3912
  (0, import_types10.Err)({
3879
3913
  category: "agent_not_found",
3880
3914
  message: `Claude command '${command}' not found or failed`
@@ -3882,8 +3916,8 @@ function resolveExitCode(code, command, resolve6) {
3882
3916
  );
3883
3917
  }
3884
3918
  }
3885
- function resolveSpawnError(command, resolve6) {
3886
- resolve6((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3919
+ function resolveSpawnError(command, resolve7) {
3920
+ resolve7((0, import_types10.Err)({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3887
3921
  }
3888
3922
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3889
3923
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4196,10 +4230,10 @@ var ClaudeBackend = class {
4196
4230
  errRl.close();
4197
4231
  }
4198
4232
  if (exitCode === null) {
4199
- await new Promise((resolve6) => {
4233
+ await new Promise((resolve7) => {
4200
4234
  child.on("exit", (code) => {
4201
4235
  exitCode = code;
4202
- resolve6(null);
4236
+ resolve7(null);
4203
4237
  });
4204
4238
  });
4205
4239
  }
@@ -4221,10 +4255,10 @@ var ClaudeBackend = class {
4221
4255
  return (0, import_types10.Ok)(void 0);
4222
4256
  }
4223
4257
  async healthCheck() {
4224
- return new Promise((resolve6) => {
4258
+ return new Promise((resolve7) => {
4225
4259
  const child = (0, import_node_child_process4.spawn)(this.command, ["--version"]);
4226
- child.on("exit", (code) => resolveExitCode(code, this.command, resolve6));
4227
- child.on("error", () => resolveSpawnError(this.command, resolve6));
4260
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
4261
+ child.on("error", () => resolveSpawnError(this.command, resolve7));
4228
4262
  });
4229
4263
  }
4230
4264
  };
@@ -4832,7 +4866,7 @@ var PiBackend = class {
4832
4866
  } else {
4833
4867
  resolvedModelName = this.config.model;
4834
4868
  }
4835
- const piSdk = await import("@mariozechner/pi-coding-agent");
4869
+ const piSdk = await import("@earendil-works/pi-coding-agent");
4836
4870
  const model = buildLocalModel({
4837
4871
  model: resolvedModelName,
4838
4872
  endpoint: this.config.endpoint,
@@ -4987,7 +5021,7 @@ var PiBackend = class {
4987
5021
  }
4988
5022
  async healthCheck() {
4989
5023
  try {
4990
- await import("@mariozechner/pi-coding-agent");
5024
+ await import("@earendil-works/pi-coding-agent");
4991
5025
  return (0, import_types15.Ok)(void 0);
4992
5026
  } catch (err) {
4993
5027
  return (0, import_types15.Err)({
@@ -4998,6 +5032,541 @@ var PiBackend = class {
4998
5032
  }
4999
5033
  };
5000
5034
 
5035
+ // src/agent/backends/ssh.ts
5036
+ var import_node_child_process5 = require("child_process");
5037
+ var import_types16 = require("@harness-engineering/types");
5038
+ var DEFAULT_TIMEOUT_MS2 = 9e4;
5039
+ var FORBIDDEN_HOST_CHARS = /[;&|`$()\n\r<>]/;
5040
+ var SshBackend = class {
5041
+ name = "ssh";
5042
+ config;
5043
+ spawnImpl;
5044
+ constructor(config) {
5045
+ if (!config.host || typeof config.host !== "string") {
5046
+ throw new Error("SshBackend: `host` is required");
5047
+ }
5048
+ if (FORBIDDEN_HOST_CHARS.test(config.host) || config.host.startsWith("-")) {
5049
+ throw new Error(
5050
+ `SshBackend: invalid host '${config.host}' (contains shell metacharacters or starts with '-')`
5051
+ );
5052
+ }
5053
+ if (!config.remoteCommand || typeof config.remoteCommand !== "string") {
5054
+ throw new Error("SshBackend: `remoteCommand` is required");
5055
+ }
5056
+ if (config.user !== void 0 && /[\s;&|`$]/.test(config.user)) {
5057
+ throw new Error(`SshBackend: invalid user '${config.user}'`);
5058
+ }
5059
+ this.config = {
5060
+ host: config.host,
5061
+ remoteCommand: config.remoteCommand,
5062
+ sshBinary: config.sshBinary ?? "ssh",
5063
+ sshOptions: config.sshOptions ?? [],
5064
+ timeoutMs: config.timeoutMs ?? DEFAULT_TIMEOUT_MS2,
5065
+ ...config.user !== void 0 ? { user: config.user } : {},
5066
+ ...config.port !== void 0 ? { port: config.port } : {},
5067
+ ...config.identityFile !== void 0 ? { identityFile: config.identityFile } : {}
5068
+ };
5069
+ this.spawnImpl = config.spawnImpl ?? import_node_child_process5.spawn;
5070
+ }
5071
+ /**
5072
+ * Builds the argv passed to the `ssh` binary. Exported as a method on
5073
+ * the class so tests can assert the exact shape without spawning.
5074
+ *
5075
+ * Layout: `[options..., target, '--', remoteCommand]`
5076
+ */
5077
+ buildSshArgs() {
5078
+ const args = [];
5079
+ if (this.config.identityFile) {
5080
+ args.push("-i", this.config.identityFile);
5081
+ }
5082
+ if (this.config.port !== void 0) {
5083
+ args.push("-p", String(this.config.port));
5084
+ }
5085
+ args.push("-o", "BatchMode=yes");
5086
+ for (const opt of this.config.sshOptions) {
5087
+ args.push("-o", opt);
5088
+ }
5089
+ const target = this.config.user ? `${this.config.user}@${this.config.host}` : this.config.host;
5090
+ args.push(target);
5091
+ args.push("--");
5092
+ args.push(this.config.remoteCommand);
5093
+ return args;
5094
+ }
5095
+ async startSession(params) {
5096
+ const session = {
5097
+ sessionId: `ssh-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5098
+ workspacePath: params.workspacePath,
5099
+ backendName: this.name,
5100
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5101
+ ...params.systemPrompt !== void 0 && { systemPrompt: params.systemPrompt }
5102
+ };
5103
+ return (0, import_types16.Ok)(session);
5104
+ }
5105
+ async *runTurn(session, params) {
5106
+ const child = this.spawnImpl(this.config.sshBinary, this.buildSshArgs(), {
5107
+ stdio: ["pipe", "pipe", "pipe"]
5108
+ });
5109
+ const payload = JSON.stringify({
5110
+ kind: "turn",
5111
+ prompt: params.prompt,
5112
+ isContinuation: params.isContinuation,
5113
+ systemPrompt: session.systemPrompt
5114
+ });
5115
+ try {
5116
+ child.stdin.write(payload + "\n");
5117
+ child.stdin.end();
5118
+ } catch (err) {
5119
+ const message = err instanceof Error ? err.message : "failed to write to ssh stdin";
5120
+ try {
5121
+ child.kill("SIGTERM");
5122
+ } catch {
5123
+ }
5124
+ return errResult(session.sessionId, message);
5125
+ }
5126
+ const timeout = setTimeout(() => {
5127
+ try {
5128
+ child.kill("SIGTERM");
5129
+ } catch {
5130
+ }
5131
+ }, this.config.timeoutMs);
5132
+ let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
5133
+ let success = true;
5134
+ let lastError;
5135
+ try {
5136
+ for await (const line of readLines(child.stdout)) {
5137
+ let event;
5138
+ try {
5139
+ event = parseEvent(line, session.sessionId);
5140
+ } catch (err) {
5141
+ const message = err instanceof Error ? err.message : "unparseable ssh event";
5142
+ success = false;
5143
+ lastError = message;
5144
+ break;
5145
+ }
5146
+ if (!event) continue;
5147
+ if (event.usage) finalUsage = event.usage;
5148
+ if (event.type === "error" && typeof event.content === "string") {
5149
+ lastError = event.content;
5150
+ success = false;
5151
+ }
5152
+ yield event;
5153
+ }
5154
+ const exitCode = await waitForExit(child);
5155
+ if (exitCode !== 0 && exitCode !== null) {
5156
+ success = false;
5157
+ lastError = lastError ?? `ssh exited with code ${exitCode}`;
5158
+ }
5159
+ } finally {
5160
+ clearTimeout(timeout);
5161
+ }
5162
+ return {
5163
+ success,
5164
+ sessionId: session.sessionId,
5165
+ usage: finalUsage,
5166
+ ...lastError !== void 0 ? { error: lastError } : {}
5167
+ };
5168
+ }
5169
+ async stopSession(_session) {
5170
+ return (0, import_types16.Ok)(void 0);
5171
+ }
5172
+ async healthCheck() {
5173
+ const args = [...this.buildSshArgs()];
5174
+ args[args.length - 1] = "true";
5175
+ return new Promise((resolve7) => {
5176
+ let child;
5177
+ try {
5178
+ child = this.spawnImpl(this.config.sshBinary, args, {
5179
+ stdio: ["ignore", "ignore", "pipe"]
5180
+ });
5181
+ } catch (err) {
5182
+ resolve7(
5183
+ (0, import_types16.Err)({
5184
+ category: "agent_not_found",
5185
+ message: err instanceof Error ? err.message : "failed to spawn ssh"
5186
+ })
5187
+ );
5188
+ return;
5189
+ }
5190
+ let stderr = "";
5191
+ child.stderr?.on("data", (chunk) => {
5192
+ stderr += chunk.toString();
5193
+ });
5194
+ const timer = setTimeout(() => {
5195
+ try {
5196
+ child.kill("SIGTERM");
5197
+ } catch {
5198
+ }
5199
+ }, this.config.timeoutMs);
5200
+ child.on("close", (code) => {
5201
+ clearTimeout(timer);
5202
+ if (code === 0) {
5203
+ resolve7((0, import_types16.Ok)(void 0));
5204
+ } else {
5205
+ resolve7(
5206
+ (0, import_types16.Err)({
5207
+ category: "agent_not_found",
5208
+ message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
5209
+ })
5210
+ );
5211
+ }
5212
+ });
5213
+ child.on("error", (err) => {
5214
+ clearTimeout(timer);
5215
+ resolve7((0, import_types16.Err)({ category: "agent_not_found", message: err.message }));
5216
+ });
5217
+ });
5218
+ }
5219
+ };
5220
+ function errResult(sessionId, message) {
5221
+ return {
5222
+ success: false,
5223
+ sessionId,
5224
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5225
+ error: message
5226
+ };
5227
+ }
5228
+ function parseEvent(line, sessionId) {
5229
+ const trimmed = line.trim();
5230
+ if (trimmed.length === 0) return null;
5231
+ const raw = JSON.parse(trimmed);
5232
+ if (typeof raw.type !== "string") {
5233
+ throw new Error(`ssh event missing 'type': ${trimmed.slice(0, 200)}`);
5234
+ }
5235
+ const ev = {
5236
+ type: raw.type,
5237
+ timestamp: typeof raw.timestamp === "string" ? raw.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
5238
+ sessionId
5239
+ };
5240
+ if (typeof raw.subtype === "string") ev.subtype = raw.subtype;
5241
+ if (raw.content !== void 0) ev.content = raw.content;
5242
+ if (isUsage(raw.usage)) ev.usage = raw.usage;
5243
+ return ev;
5244
+ }
5245
+ function isUsage(u) {
5246
+ if (!u || typeof u !== "object") return false;
5247
+ const o = u;
5248
+ return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
5249
+ }
5250
+ async function* readLines(stream) {
5251
+ let buffer = "";
5252
+ for await (const chunk of stream) {
5253
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5254
+ let idx;
5255
+ while ((idx = buffer.indexOf("\n")) >= 0) {
5256
+ yield buffer.slice(0, idx);
5257
+ buffer = buffer.slice(idx + 1);
5258
+ }
5259
+ }
5260
+ if (buffer.length > 0) yield buffer;
5261
+ }
5262
+ function waitForExit(child) {
5263
+ return new Promise((resolve7) => {
5264
+ if (child.exitCode !== null) {
5265
+ resolve7(child.exitCode);
5266
+ return;
5267
+ }
5268
+ child.once("close", (code) => resolve7(code));
5269
+ child.once("error", () => resolve7(null));
5270
+ });
5271
+ }
5272
+
5273
+ // src/agent/backends/serverless.ts
5274
+ var import_node_child_process6 = require("child_process");
5275
+ var import_types17 = require("@harness-engineering/types");
5276
+ var ServerlessBackend = class {
5277
+ handles = /* @__PURE__ */ new Map();
5278
+ async startSession(params) {
5279
+ const start = await this.coldStart(params);
5280
+ if (!start.ok) return start;
5281
+ const session = {
5282
+ sessionId: `${this.name}-session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
5283
+ workspacePath: params.workspacePath,
5284
+ backendName: this.name,
5285
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5286
+ };
5287
+ this.handles.set(session.sessionId, start.value);
5288
+ return (0, import_types17.Ok)(session);
5289
+ }
5290
+ async *runTurn(session, params) {
5291
+ const handle = this.handles.get(session.sessionId);
5292
+ if (!handle) {
5293
+ return {
5294
+ success: false,
5295
+ sessionId: session.sessionId,
5296
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5297
+ error: `no serverless handle for session ${session.sessionId}`
5298
+ };
5299
+ }
5300
+ return yield* this.runOnHandle(handle, params, session);
5301
+ }
5302
+ async stopSession(session) {
5303
+ const handle = this.handles.get(session.sessionId);
5304
+ if (!handle) return (0, import_types17.Ok)(void 0);
5305
+ this.handles.delete(session.sessionId);
5306
+ return this.teardown(handle);
5307
+ }
5308
+ };
5309
+ var FORBIDDEN_IMAGE_CHARS = /[;&|`$()\n\r<>]/;
5310
+ var BLOCKED_DOCKER_FLAGS = [
5311
+ "--privileged",
5312
+ "--cap-add",
5313
+ "--security-opt",
5314
+ "--pid",
5315
+ "--ipc",
5316
+ "--userns"
5317
+ ];
5318
+ var DEFAULT_OCI_TIMEOUT_MS = 9e4;
5319
+ var OciServerlessBackend = class extends ServerlessBackend {
5320
+ name = "serverless:oci";
5321
+ config;
5322
+ spawnImpl;
5323
+ envSource;
5324
+ constructor(config) {
5325
+ super();
5326
+ if (!config.image || typeof config.image !== "string") {
5327
+ throw new Error("OciServerlessBackend: `image` is required");
5328
+ }
5329
+ if (FORBIDDEN_IMAGE_CHARS.test(config.image) || config.image.startsWith("-")) {
5330
+ throw new Error(
5331
+ `OciServerlessBackend: invalid image '${config.image}' (contains shell metacharacters or starts with '-')`
5332
+ );
5333
+ }
5334
+ this.config = {
5335
+ image: config.image,
5336
+ pullPolicy: config.pullPolicy ?? "if-not-present",
5337
+ runtime: config.runtime ?? "docker",
5338
+ envPassthrough: config.envPassthrough ?? [],
5339
+ timeoutMs: config.timeoutMs ?? DEFAULT_OCI_TIMEOUT_MS,
5340
+ extraArgs: sanitizeExtraArgs(config.extraArgs),
5341
+ ...config.registry !== void 0 ? { registry: config.registry } : {}
5342
+ };
5343
+ this.spawnImpl = config.spawnImpl ?? import_node_child_process6.spawn;
5344
+ this.envSource = config.envSource ?? process.env;
5345
+ }
5346
+ /** Builds the argv for `docker run -d ...`. Exposed for tests. */
5347
+ buildRunArgs() {
5348
+ const env = this.collectEnv();
5349
+ const args = ["run", "-d", "--rm"];
5350
+ for (const [k, v] of Object.entries(env)) {
5351
+ args.push("-e", `${k}=${v}`);
5352
+ }
5353
+ for (const ea of this.config.extraArgs) {
5354
+ args.push(ea);
5355
+ }
5356
+ args.push("--");
5357
+ args.push(this.config.image);
5358
+ return args;
5359
+ }
5360
+ /** Builds the argv for `docker exec <id> -- agent`. Exposed for tests. */
5361
+ buildExecArgs(handleId) {
5362
+ return ["exec", "-i", handleId, "/agent"];
5363
+ }
5364
+ async coldStart(_params) {
5365
+ if (this.config.pullPolicy === "always") {
5366
+ const pull = await this.runOneShot(this.config.runtime, ["pull", this.config.image]);
5367
+ if (!pull.ok) return pull;
5368
+ }
5369
+ const result = await this.runOneShot(this.config.runtime, this.buildRunArgs());
5370
+ if (!result.ok) return result;
5371
+ const id = result.value.trim().split(/\s+/)[0] ?? "";
5372
+ if (!id) {
5373
+ return (0, import_types17.Err)({
5374
+ category: "response_error",
5375
+ message: "OciServerlessBackend: empty container id from runtime"
5376
+ });
5377
+ }
5378
+ return (0, import_types17.Ok)({ id, adapter: this.name });
5379
+ }
5380
+ async *runOnHandle(handle, params, session) {
5381
+ const child = this.spawnImpl(this.config.runtime, this.buildExecArgs(handle.id), {
5382
+ stdio: ["pipe", "pipe", "pipe"]
5383
+ });
5384
+ const payload = JSON.stringify({
5385
+ kind: "turn",
5386
+ prompt: params.prompt,
5387
+ isContinuation: params.isContinuation
5388
+ });
5389
+ try {
5390
+ child.stdin.write(payload + "\n");
5391
+ child.stdin.end();
5392
+ } catch (err) {
5393
+ const message = err instanceof Error ? err.message : "failed to write to docker stdin";
5394
+ return turnFailure(session.sessionId, message);
5395
+ }
5396
+ const timeout = setTimeout(() => {
5397
+ try {
5398
+ child.kill("SIGTERM");
5399
+ } catch {
5400
+ }
5401
+ }, this.config.timeoutMs);
5402
+ let finalUsage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
5403
+ let success = true;
5404
+ let lastError;
5405
+ try {
5406
+ for await (const line of readLines2(child.stdout)) {
5407
+ const ev = tryParseEvent(line, session.sessionId);
5408
+ if (!ev) continue;
5409
+ if (ev.usage) finalUsage = ev.usage;
5410
+ if (ev.type === "error" && typeof ev.content === "string") {
5411
+ success = false;
5412
+ lastError = ev.content;
5413
+ }
5414
+ yield ev;
5415
+ }
5416
+ const code = await waitForExit2(child);
5417
+ if (code !== 0 && code !== null) {
5418
+ success = false;
5419
+ lastError = lastError ?? `runtime exec exited with code ${code}`;
5420
+ }
5421
+ } finally {
5422
+ clearTimeout(timeout);
5423
+ }
5424
+ return {
5425
+ success,
5426
+ sessionId: session.sessionId,
5427
+ usage: finalUsage,
5428
+ ...lastError !== void 0 ? { error: lastError } : {}
5429
+ };
5430
+ }
5431
+ async teardown(handle) {
5432
+ if (handle.adapter !== this.name) {
5433
+ return (0, import_types17.Err)({
5434
+ category: "response_error",
5435
+ message: `handle adapter mismatch: got '${handle.adapter}', expected '${this.name}'`
5436
+ });
5437
+ }
5438
+ const stop = await this.runOneShot(this.config.runtime, ["stop", handle.id]);
5439
+ if (!stop.ok) return stop;
5440
+ return (0, import_types17.Ok)(void 0);
5441
+ }
5442
+ async healthCheck() {
5443
+ return mapOk(
5444
+ await this.runOneShot(this.config.runtime, ["version", "--format", "{{.Server.Version}}"])
5445
+ );
5446
+ }
5447
+ collectEnv() {
5448
+ const out = {};
5449
+ for (const key of this.config.envPassthrough) {
5450
+ const val = this.envSource[key];
5451
+ if (typeof val === "string") out[key] = val;
5452
+ }
5453
+ return out;
5454
+ }
5455
+ runOneShot(binary, args) {
5456
+ return new Promise((resolve7) => {
5457
+ let child;
5458
+ try {
5459
+ child = this.spawnImpl(binary, args, {
5460
+ stdio: ["ignore", "pipe", "pipe"]
5461
+ });
5462
+ } catch (err) {
5463
+ resolve7(
5464
+ (0, import_types17.Err)({
5465
+ category: "agent_not_found",
5466
+ message: err instanceof Error ? err.message : "failed to spawn runtime"
5467
+ })
5468
+ );
5469
+ return;
5470
+ }
5471
+ let stdout = "";
5472
+ let stderr = "";
5473
+ child.stdout?.on("data", (chunk) => {
5474
+ stdout += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5475
+ });
5476
+ child.stderr?.on("data", (chunk) => {
5477
+ stderr += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5478
+ });
5479
+ const timer = setTimeout(() => {
5480
+ try {
5481
+ child.kill("SIGTERM");
5482
+ } catch {
5483
+ }
5484
+ }, this.config.timeoutMs);
5485
+ child.on("close", (code) => {
5486
+ clearTimeout(timer);
5487
+ if (code === 0) {
5488
+ resolve7((0, import_types17.Ok)(stdout));
5489
+ } else {
5490
+ resolve7(
5491
+ (0, import_types17.Err)({
5492
+ category: "response_error",
5493
+ message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
5494
+ })
5495
+ );
5496
+ }
5497
+ });
5498
+ child.on("error", (err) => {
5499
+ clearTimeout(timer);
5500
+ resolve7((0, import_types17.Err)({ category: "agent_not_found", message: err.message }));
5501
+ });
5502
+ });
5503
+ }
5504
+ };
5505
+ function sanitizeExtraArgs(extraArgs) {
5506
+ if (!extraArgs) return [];
5507
+ return extraArgs.filter((arg) => !BLOCKED_DOCKER_FLAGS.some((flag) => arg.startsWith(flag)));
5508
+ }
5509
+ function mapOk(r) {
5510
+ return r.ok ? (0, import_types17.Ok)(void 0) : r;
5511
+ }
5512
+ function turnFailure(sessionId, message) {
5513
+ return {
5514
+ success: false,
5515
+ sessionId,
5516
+ usage: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
5517
+ error: message
5518
+ };
5519
+ }
5520
+ function tryParseEvent(line, sessionId) {
5521
+ const trimmed = line.trim();
5522
+ if (!trimmed) return null;
5523
+ let raw;
5524
+ try {
5525
+ raw = JSON.parse(trimmed);
5526
+ } catch {
5527
+ return null;
5528
+ }
5529
+ if (!raw || typeof raw !== "object") return null;
5530
+ const o = raw;
5531
+ if (typeof o.type !== "string") return null;
5532
+ const ev = {
5533
+ type: o.type,
5534
+ timestamp: typeof o.timestamp === "string" ? o.timestamp : (/* @__PURE__ */ new Date()).toISOString(),
5535
+ sessionId
5536
+ };
5537
+ if (typeof o.subtype === "string") ev.subtype = o.subtype;
5538
+ if (o.content !== void 0) ev.content = o.content;
5539
+ if (isUsage2(o.usage)) ev.usage = o.usage;
5540
+ return ev;
5541
+ }
5542
+ function isUsage2(u) {
5543
+ if (!u || typeof u !== "object") return false;
5544
+ const o = u;
5545
+ return typeof o.inputTokens === "number" && typeof o.outputTokens === "number" && typeof o.totalTokens === "number";
5546
+ }
5547
+ async function* readLines2(stream) {
5548
+ let buffer = "";
5549
+ for await (const chunk of stream) {
5550
+ buffer += typeof chunk === "string" ? chunk : chunk.toString("utf-8");
5551
+ let idx;
5552
+ while ((idx = buffer.indexOf("\n")) >= 0) {
5553
+ yield buffer.slice(0, idx);
5554
+ buffer = buffer.slice(idx + 1);
5555
+ }
5556
+ }
5557
+ if (buffer.length > 0) yield buffer;
5558
+ }
5559
+ function waitForExit2(child) {
5560
+ return new Promise((resolve7) => {
5561
+ if (child.exitCode !== null) {
5562
+ resolve7(child.exitCode);
5563
+ return;
5564
+ }
5565
+ child.once("close", (code) => resolve7(code));
5566
+ child.once("error", () => resolve7(null));
5567
+ });
5568
+ }
5569
+
5001
5570
  // src/agent/backend-factory.ts
5002
5571
  function makeGetModel(model) {
5003
5572
  if (typeof model === "string") return () => model;
@@ -5047,6 +5616,35 @@ function createBackend(def, options = {}) {
5047
5616
  ...def.timeoutMs !== void 0 ? { timeoutMs: def.timeoutMs } : {}
5048
5617
  });
5049
5618
  }
5619
+ case "ssh": {
5620
+ return new SshBackend({
5621
+ host: def.host,
5622
+ remoteCommand: def.remoteCommand,
5623
+ ...def.user !== void 0 ? { user: def.user } : {},
5624
+ ...def.port !== void 0 ? { port: def.port } : {},
5625
+ ...def.identityFile !== void 0 ? { identityFile: def.identityFile } : {},
5626
+ ...def.sshOptions !== void 0 ? { sshOptions: def.sshOptions } : {},
5627
+ ...def.sshBinary !== void 0 ? { sshBinary: def.sshBinary } : {}
5628
+ });
5629
+ }
5630
+ case "serverless": {
5631
+ switch (def.adapter) {
5632
+ case "oci":
5633
+ return new OciServerlessBackend({
5634
+ image: def.image,
5635
+ ...def.registry !== void 0 ? { registry: def.registry } : {},
5636
+ ...def.pullPolicy !== void 0 ? { pullPolicy: def.pullPolicy } : {},
5637
+ ...def.envPassthrough !== void 0 ? { envPassthrough: def.envPassthrough } : {},
5638
+ ...def.runtime !== void 0 ? { runtime: def.runtime } : {}
5639
+ });
5640
+ default: {
5641
+ const exhaustive = def.adapter;
5642
+ throw new Error(
5643
+ `createBackend: unknown serverless adapter ${JSON.stringify(exhaustive)}`
5644
+ );
5645
+ }
5646
+ }
5647
+ }
5050
5648
  default: {
5051
5649
  const exhaustive = def;
5052
5650
  throw new Error(`createBackend: unknown backend type ${JSON.stringify(exhaustive)}`);
@@ -5055,12 +5653,12 @@ function createBackend(def, options = {}) {
5055
5653
  }
5056
5654
 
5057
5655
  // src/agent/backends/container.ts
5058
- var import_types16 = require("@harness-engineering/types");
5656
+ var import_types18 = require("@harness-engineering/types");
5059
5657
  function toAgentError(message, details) {
5060
5658
  return { category: "response_error", message, details };
5061
5659
  }
5062
5660
  var BLOCKED_FLAGS = ["--privileged", "--cap-add", "--security-opt", "--pid", "--ipc", "--userns"];
5063
- function sanitizeExtraArgs(extraArgs) {
5661
+ function sanitizeExtraArgs2(extraArgs) {
5064
5662
  if (!extraArgs) return [];
5065
5663
  return extraArgs.filter((arg) => !BLOCKED_FLAGS.some((flag) => arg.startsWith(flag)));
5066
5664
  }
@@ -5086,7 +5684,7 @@ var ContainerBackend = class {
5086
5684
  }
5087
5685
  const result = await this.secretBackend.resolveSecrets(this.secretKeys);
5088
5686
  if (!result.ok) {
5089
- return (0, import_types16.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
5687
+ return (0, import_types18.Err)(toAgentError(`Secret resolution failed: ${result.error.message}`, result.error));
5090
5688
  }
5091
5689
  return { ok: true, value: result.value };
5092
5690
  }
@@ -5099,7 +5697,7 @@ var ContainerBackend = class {
5099
5697
  network: this.containerConfig.network ?? "none",
5100
5698
  env
5101
5699
  };
5102
- const sanitized = sanitizeExtraArgs(this.containerConfig.extraArgs);
5700
+ const sanitized = sanitizeExtraArgs2(this.containerConfig.extraArgs);
5103
5701
  if (sanitized.length > 0) {
5104
5702
  opts.extraArgs = sanitized;
5105
5703
  }
@@ -5111,7 +5709,7 @@ var ContainerBackend = class {
5111
5709
  const createOpts = this.buildCreateOpts(params, envResult.value);
5112
5710
  const containerResult = await this.runtime.createContainer(createOpts);
5113
5711
  if (!containerResult.ok) {
5114
- return (0, import_types16.Err)(
5712
+ return (0, import_types18.Err)(
5115
5713
  toAgentError(
5116
5714
  `Container creation failed: ${containerResult.error.message}`,
5117
5715
  containerResult.error
@@ -5136,7 +5734,7 @@ var ContainerBackend = class {
5136
5734
  this.containerHandles.delete(session.sessionId);
5137
5735
  const removeResult = await this.runtime.removeContainer(handle);
5138
5736
  if (!removeResult.ok) {
5139
- return (0, import_types16.Err)(
5737
+ return (0, import_types18.Err)(
5140
5738
  toAgentError(
5141
5739
  `Container removal failed: ${removeResult.error.message}`,
5142
5740
  removeResult.error
@@ -5149,7 +5747,7 @@ var ContainerBackend = class {
5149
5747
  async healthCheck() {
5150
5748
  const runtimeResult = await this.runtime.healthCheck();
5151
5749
  if (!runtimeResult.ok) {
5152
- return (0, import_types16.Err)({
5750
+ return (0, import_types18.Err)({
5153
5751
  category: "agent_not_found",
5154
5752
  message: `Container runtime unhealthy: ${runtimeResult.error.message}`,
5155
5753
  details: runtimeResult.error
@@ -5160,16 +5758,16 @@ var ContainerBackend = class {
5160
5758
  };
5161
5759
 
5162
5760
  // src/agent/runtime/docker.ts
5163
- var import_node_child_process5 = require("child_process");
5164
- var import_types17 = require("@harness-engineering/types");
5761
+ var import_node_child_process7 = require("child_process");
5762
+ var import_types19 = require("@harness-engineering/types");
5165
5763
  function dockerExec(args) {
5166
- return new Promise((resolve6, reject) => {
5167
- (0, import_node_child_process5.execFile)("docker", args, (error, stdout) => {
5764
+ return new Promise((resolve7, reject) => {
5765
+ (0, import_node_child_process7.execFile)("docker", args, (error, stdout) => {
5168
5766
  if (error) {
5169
5767
  reject(error);
5170
5768
  return;
5171
5769
  }
5172
- resolve6(stdout.trim());
5770
+ resolve7(stdout.trim());
5173
5771
  });
5174
5772
  });
5175
5773
  }
@@ -5194,9 +5792,9 @@ var DockerRuntime = class {
5194
5792
  args.push(opts.image);
5195
5793
  args.push("sleep", "infinity");
5196
5794
  const containerId = await dockerExec(args);
5197
- return (0, import_types17.Ok)({ containerId, runtime: this.name });
5795
+ return (0, import_types19.Ok)({ containerId, runtime: this.name });
5198
5796
  } catch (error) {
5199
- return (0, import_types17.Err)({
5797
+ return (0, import_types19.Err)({
5200
5798
  category: "container_create_failed",
5201
5799
  message: `Failed to create container: ${error instanceof Error ? error.message : String(error)}`,
5202
5800
  details: error
@@ -5218,7 +5816,7 @@ var DockerRuntime = class {
5218
5816
  }
5219
5817
  }
5220
5818
  execArgs.push(handle.containerId, ...cmd);
5221
- const child = (0, import_node_child_process5.spawn)("docker", execArgs);
5819
+ const child = (0, import_node_child_process7.spawn)("docker", execArgs);
5222
5820
  const readline3 = await import("readline");
5223
5821
  const rl = readline3.createInterface({ input: child.stdout, terminal: false });
5224
5822
  try {
@@ -5228,11 +5826,11 @@ var DockerRuntime = class {
5228
5826
  } finally {
5229
5827
  rl.close();
5230
5828
  }
5231
- const exitCode = await new Promise((resolve6) => {
5829
+ const exitCode = await new Promise((resolve7) => {
5232
5830
  if (child.exitCode !== null) {
5233
- resolve6(child.exitCode);
5831
+ resolve7(child.exitCode);
5234
5832
  } else {
5235
- child.on("exit", (code) => resolve6(code ?? 1));
5833
+ child.on("exit", (code) => resolve7(code ?? 1));
5236
5834
  }
5237
5835
  });
5238
5836
  return exitCode;
@@ -5240,9 +5838,9 @@ var DockerRuntime = class {
5240
5838
  async removeContainer(handle) {
5241
5839
  try {
5242
5840
  await dockerExec(["rm", "-f", handle.containerId]);
5243
- return (0, import_types17.Ok)(void 0);
5841
+ return (0, import_types19.Ok)(void 0);
5244
5842
  } catch (error) {
5245
- return (0, import_types17.Err)({
5843
+ return (0, import_types19.Err)({
5246
5844
  category: "container_remove_failed",
5247
5845
  message: `Failed to remove container: ${error instanceof Error ? error.message : String(error)}`,
5248
5846
  details: error
@@ -5252,9 +5850,9 @@ var DockerRuntime = class {
5252
5850
  async healthCheck() {
5253
5851
  try {
5254
5852
  await dockerExec(["info", "--format", "{{.ServerVersion}}"]);
5255
- return (0, import_types17.Ok)(void 0);
5853
+ return (0, import_types19.Ok)(void 0);
5256
5854
  } catch (error) {
5257
- return (0, import_types17.Err)({
5855
+ return (0, import_types19.Err)({
5258
5856
  category: "runtime_not_found",
5259
5857
  message: `Docker is not available: ${error instanceof Error ? error.message : String(error)}`,
5260
5858
  details: error
@@ -5264,7 +5862,7 @@ var DockerRuntime = class {
5264
5862
  };
5265
5863
 
5266
5864
  // src/agent/secrets/env.ts
5267
- var import_types18 = require("@harness-engineering/types");
5865
+ var import_types20 = require("@harness-engineering/types");
5268
5866
  var EnvSecretBackend = class {
5269
5867
  name = "env";
5270
5868
  async resolveSecrets(keys) {
@@ -5272,7 +5870,7 @@ var EnvSecretBackend = class {
5272
5870
  for (const key of keys) {
5273
5871
  const value = process.env[key];
5274
5872
  if (value === void 0) {
5275
- return (0, import_types18.Err)({
5873
+ return (0, import_types20.Err)({
5276
5874
  category: "secret_not_found",
5277
5875
  message: `Environment variable '${key}' is not set`,
5278
5876
  key
@@ -5280,24 +5878,24 @@ var EnvSecretBackend = class {
5280
5878
  }
5281
5879
  secrets[key] = value;
5282
5880
  }
5283
- return (0, import_types18.Ok)(secrets);
5881
+ return (0, import_types20.Ok)(secrets);
5284
5882
  }
5285
5883
  async healthCheck() {
5286
- return (0, import_types18.Ok)(void 0);
5884
+ return (0, import_types20.Ok)(void 0);
5287
5885
  }
5288
5886
  };
5289
5887
 
5290
5888
  // src/agent/secrets/onepassword.ts
5291
- var import_node_child_process6 = require("child_process");
5292
- var import_types19 = require("@harness-engineering/types");
5889
+ var import_node_child_process8 = require("child_process");
5890
+ var import_types21 = require("@harness-engineering/types");
5293
5891
  function opExec(args) {
5294
- return new Promise((resolve6, reject) => {
5295
- (0, import_node_child_process6.execFile)("op", args, (error, stdout) => {
5892
+ return new Promise((resolve7, reject) => {
5893
+ (0, import_node_child_process8.execFile)("op", args, (error, stdout) => {
5296
5894
  if (error) {
5297
5895
  reject(error);
5298
5896
  return;
5299
5897
  }
5300
- resolve6(stdout.trim());
5898
+ resolve7(stdout.trim());
5301
5899
  });
5302
5900
  });
5303
5901
  }
@@ -5314,21 +5912,21 @@ var OnePasswordSecretBackend = class {
5314
5912
  const value = await opExec(["read", `op://${this.vault}/${key}/password`]);
5315
5913
  secrets[key] = value;
5316
5914
  } catch (error) {
5317
- return (0, import_types19.Err)({
5915
+ return (0, import_types21.Err)({
5318
5916
  category: "access_denied",
5319
5917
  message: `Failed to read secret '${key}' from 1Password: ${error instanceof Error ? error.message : String(error)}`,
5320
5918
  key
5321
5919
  });
5322
5920
  }
5323
5921
  }
5324
- return (0, import_types19.Ok)(secrets);
5922
+ return (0, import_types21.Ok)(secrets);
5325
5923
  }
5326
5924
  async healthCheck() {
5327
5925
  try {
5328
5926
  await opExec(["--version"]);
5329
- return (0, import_types19.Ok)(void 0);
5927
+ return (0, import_types21.Ok)(void 0);
5330
5928
  } catch (error) {
5331
- return (0, import_types19.Err)({
5929
+ return (0, import_types21.Err)({
5332
5930
  category: "provider_unavailable",
5333
5931
  message: `1Password CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5334
5932
  });
@@ -5337,16 +5935,16 @@ var OnePasswordSecretBackend = class {
5337
5935
  };
5338
5936
 
5339
5937
  // src/agent/secrets/vault.ts
5340
- var import_node_child_process7 = require("child_process");
5341
- var import_types20 = require("@harness-engineering/types");
5938
+ var import_node_child_process9 = require("child_process");
5939
+ var import_types22 = require("@harness-engineering/types");
5342
5940
  function vaultExec(args, env) {
5343
- return new Promise((resolve6, reject) => {
5344
- (0, import_node_child_process7.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5941
+ return new Promise((resolve7, reject) => {
5942
+ (0, import_node_child_process9.execFile)("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5345
5943
  if (error) {
5346
5944
  reject(error);
5347
5945
  return;
5348
5946
  }
5349
- resolve6(stdout.trim());
5947
+ resolve7(stdout.trim());
5350
5948
  });
5351
5949
  });
5352
5950
  }
@@ -5369,11 +5967,11 @@ var VaultSecretBackend = class {
5369
5967
  } catch (error) {
5370
5968
  const msg = error instanceof Error ? error.message : String(error);
5371
5969
  const category = error instanceof SyntaxError ? "access_denied" : "access_denied";
5372
- return (0, import_types20.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5970
+ return (0, import_types22.Err)({ category, message: `Failed to read from Vault: ${msg}` });
5373
5971
  }
5374
5972
  const missing = keys.find((k) => !(k in data));
5375
5973
  if (missing) {
5376
- return (0, import_types20.Err)({
5974
+ return (0, import_types22.Err)({
5377
5975
  category: "secret_not_found",
5378
5976
  message: `Secret key '${missing}' not found in Vault path '${this.path}'`,
5379
5977
  key: missing
@@ -5381,14 +5979,14 @@ var VaultSecretBackend = class {
5381
5979
  }
5382
5980
  const secrets = {};
5383
5981
  for (const key of keys) secrets[key] = data[key];
5384
- return (0, import_types20.Ok)(secrets);
5982
+ return (0, import_types22.Ok)(secrets);
5385
5983
  }
5386
5984
  async healthCheck() {
5387
5985
  try {
5388
5986
  await vaultExec(["version"]);
5389
- return (0, import_types20.Ok)(void 0);
5987
+ return (0, import_types22.Ok)(void 0);
5390
5988
  } catch (error) {
5391
- return (0, import_types20.Err)({
5989
+ return (0, import_types22.Err)({
5392
5990
  category: "provider_unavailable",
5393
5991
  message: `Vault CLI is not available: ${error instanceof Error ? error.message : String(error)}`
5394
5992
  });
@@ -5525,6 +6123,8 @@ function buildAnalysisProvider(args) {
5525
6123
  return buildClaudeCliProvider(def, args, layerModel);
5526
6124
  case "mock":
5527
6125
  case "gemini":
6126
+ case "ssh":
6127
+ case "serverless":
5528
6128
  logger.warn(
5529
6129
  `Intelligence pipeline disabled for layer '${layer}': routed backend '${backendName}' has type '${def.type}' which has no AnalysisProvider implementation.`
5530
6130
  );
@@ -5707,8 +6307,8 @@ function buildExplicitProvider(provider, selModel, config) {
5707
6307
 
5708
6308
  // src/server/http.ts
5709
6309
  var http = __toESM(require("http"));
5710
- var path14 = __toESM(require("path"));
5711
- var import_core8 = require("@harness-engineering/core");
6310
+ var path15 = __toESM(require("path"));
6311
+ var import_core11 = require("@harness-engineering/core");
5712
6312
 
5713
6313
  // src/server/websocket.ts
5714
6314
  var import_ws = require("ws");
@@ -5770,7 +6370,7 @@ var import_zod3 = require("zod");
5770
6370
  // src/server/utils.ts
5771
6371
  var DEFAULT_MAX_BYTES = 1048576;
5772
6372
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
5773
- return new Promise((resolve6, reject) => {
6373
+ return new Promise((resolve7, reject) => {
5774
6374
  let body = "";
5775
6375
  let bytes = 0;
5776
6376
  req.on("data", (chunk) => {
@@ -5782,7 +6382,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
5782
6382
  }
5783
6383
  body += String(chunk);
5784
6384
  });
5785
- req.on("end", () => resolve6(body));
6385
+ req.on("end", () => resolve7(body));
5786
6386
  req.on("error", reject);
5787
6387
  });
5788
6388
  }
@@ -5948,7 +6548,7 @@ function handlePlansRoute(req, res, plansDir) {
5948
6548
  }
5949
6549
 
5950
6550
  // src/server/routes/chat-proxy.ts
5951
- var import_node_child_process8 = require("child_process");
6551
+ var import_node_child_process10 = require("child_process");
5952
6552
  var import_node_crypto5 = require("crypto");
5953
6553
  var readline2 = __toESM(require("readline"));
5954
6554
  var import_zod6 = require("zod");
@@ -6034,7 +6634,7 @@ async function handleChatRequest(req, res, command) {
6034
6634
  });
6035
6635
  emit(res, { type: "session", sessionId });
6036
6636
  const args = buildArgs(parsed.prompt, sessionId, isFirstTurn, parsed.system);
6037
- child = (0, import_node_child_process8.spawn)(command, args, { env: buildChildEnv(), stdio: "pipe" });
6637
+ child = (0, import_node_child_process10.spawn)(command, args, { env: buildChildEnv(), stdio: "pipe" });
6038
6638
  child.stdin?.end();
6039
6639
  let clientDisconnected = false;
6040
6640
  res.on("close", () => {
@@ -6762,7 +7362,7 @@ function isPrivateHost(hostname) {
6762
7362
  }
6763
7363
 
6764
7364
  // src/server/routes/v1/webhooks.ts
6765
- var import_types21 = require("@harness-engineering/types");
7365
+ var import_types23 = require("@harness-engineering/types");
6766
7366
  function isAdminAuth(authContext) {
6767
7367
  if (!authContext) return false;
6768
7368
  if (authContext.scopes.includes("admin")) return true;
@@ -6809,7 +7409,7 @@ function handleV1WebhooksRoute(req, res, deps) {
6809
7409
  const subs = await deps.store.list();
6810
7410
  const authContext = getAuthContext(req);
6811
7411
  const visible = isAdminAuth(authContext) ? subs : subs.filter((s) => s.tokenId === authContext?.id);
6812
- const publicView = visible.map((s) => import_types21.WebhookSubscriptionPublicSchema.parse(s));
7412
+ const publicView = visible.map((s) => import_types23.WebhookSubscriptionPublicSchema.parse(s));
6813
7413
  sendJSON6(res, 200, publicView);
6814
7414
  })();
6815
7415
  return true;
@@ -6910,35 +7510,561 @@ function handleV1TelemetryRoute(req, res, deps) {
6910
7510
  return false;
6911
7511
  }
6912
7512
 
6913
- // src/server/routes/sessions.ts
6914
- var fs11 = __toESM(require("fs/promises"));
6915
- var path11 = __toESM(require("path"));
7513
+ // src/server/routes/v1/proposals.ts
6916
7514
  var import_zod13 = require("zod");
6917
- var SessionCreateSchema = import_zod13.z.object({
6918
- sessionId: import_zod13.z.string().min(1)
6919
- }).passthrough();
6920
- var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6921
- function isSafeId(id) {
6922
- return UUID_RE2.test(id) || path11.basename(id) === id && !id.includes("..");
6923
- }
7515
+ var import_core10 = require("@harness-engineering/core");
7516
+ var import_types24 = require("@harness-engineering/types");
7517
+
7518
+ // src/proposals/gate.ts
7519
+ var import_yaml2 = require("yaml");
7520
+ var import_core8 = require("@harness-engineering/core");
7521
+ var GateRunError = class extends Error {
7522
+ constructor(message) {
7523
+ super(message);
7524
+ this.name = "GateRunError";
7525
+ }
7526
+ };
7527
+ var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
7528
+ function checkSkillYaml(yaml) {
7529
+ const findings = [];
7530
+ let doc;
7531
+ try {
7532
+ doc = (0, import_yaml2.parse)(yaml);
7533
+ } catch (err) {
7534
+ findings.push({
7535
+ severity: "error",
7536
+ title: "skill.yaml does not parse",
7537
+ detail: err instanceof Error ? err.message : String(err)
7538
+ });
7539
+ return findings;
7540
+ }
7541
+ if (!doc || typeof doc !== "object") {
7542
+ findings.push({
7543
+ severity: "error",
7544
+ title: "skill.yaml top-level is not a mapping",
7545
+ detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
7546
+ });
7547
+ return findings;
7548
+ }
7549
+ const obj = doc;
7550
+ if (typeof obj["name"] !== "string") {
7551
+ findings.push({
7552
+ severity: "error",
7553
+ title: "skill.yaml missing `name`",
7554
+ detail: "Every skill must declare its kebab-case name."
7555
+ });
7556
+ }
7557
+ if (typeof obj["version"] !== "string") {
7558
+ findings.push({
7559
+ severity: "error",
7560
+ title: "skill.yaml missing `version`",
7561
+ detail: "Every skill must declare a semver version string."
7562
+ });
7563
+ }
7564
+ if (typeof obj["description"] !== "string") {
7565
+ findings.push({
7566
+ severity: "warning",
7567
+ title: "skill.yaml missing `description`",
7568
+ detail: "Description is strongly recommended for discoverability."
7569
+ });
7570
+ }
7571
+ return findings;
7572
+ }
7573
+ function checkSkillMd(md) {
7574
+ const findings = [];
7575
+ if (md.trim().length < 40) {
7576
+ findings.push({
7577
+ severity: "error",
7578
+ title: "SKILL.md is too short",
7579
+ detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
7580
+ });
7581
+ }
7582
+ if (!/^#\s+\S/m.test(md)) {
7583
+ findings.push({
7584
+ severity: "warning",
7585
+ title: "SKILL.md has no top-level heading",
7586
+ detail: "Convention: open SKILL.md with `# <Skill Name>`."
7587
+ });
7588
+ }
7589
+ return findings;
7590
+ }
7591
+ function checkName(name) {
7592
+ if (SKILL_NAME_RE.test(name)) return [];
7593
+ return [
7594
+ {
7595
+ severity: "error",
7596
+ title: "skill name violates the kebab-case rule",
7597
+ detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
7598
+ }
7599
+ ];
7600
+ }
7601
+ function checkDiff(diff) {
7602
+ const findings = [];
7603
+ if (!diff.includes("---") || !diff.includes("+++")) {
7604
+ findings.push({
7605
+ severity: "error",
7606
+ title: "Refinement diff is not in unified-diff format",
7607
+ detail: "Diffs must include both `---` and `+++` headers."
7608
+ });
7609
+ }
7610
+ if (!/^@@\s/m.test(diff)) {
7611
+ findings.push({
7612
+ severity: "warning",
7613
+ title: "Refinement diff has no hunk marker",
7614
+ detail: "A unified diff typically contains at least one `@@` line."
7615
+ });
7616
+ }
7617
+ return findings;
7618
+ }
7619
+ function deriveFindings(proposal) {
7620
+ const findings = [];
7621
+ findings.push(...checkName(proposal.content.name));
7622
+ if (proposal.kind === "new-skill") {
7623
+ findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
7624
+ findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
7625
+ } else if (proposal.kind === "refinement") {
7626
+ findings.push(...checkDiff(proposal.content.diff ?? ""));
7627
+ }
7628
+ return findings;
7629
+ }
7630
+ async function runGate(projectPath, proposalId) {
7631
+ const proposal = await (0, import_core8.getProposal)(projectPath, proposalId);
7632
+ if (!proposal) throw new import_core8.ProposalNotFoundError(proposalId);
7633
+ if (proposal.status === "approved" || proposal.status === "rejected") {
7634
+ throw new GateRunError(
7635
+ `proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
7636
+ );
7637
+ }
7638
+ const findings = deriveFindings(proposal);
7639
+ const runAt = (/* @__PURE__ */ new Date()).toISOString();
7640
+ const hasError = findings.some((f) => f.severity === "error");
7641
+ const nextStatus = hasError ? "gate-failed" : "gate-running";
7642
+ const updated = await (0, import_core8.updateProposal)(projectPath, proposalId, {
7643
+ status: nextStatus,
7644
+ gate: { lastRunAt: runAt, findings }
7645
+ });
7646
+ return {
7647
+ proposalId: updated.id,
7648
+ status: updated.status,
7649
+ findings,
7650
+ runAt
7651
+ };
7652
+ }
7653
+
7654
+ // src/proposals/promote.ts
7655
+ var fs11 = __toESM(require("fs"));
7656
+ var path11 = __toESM(require("path"));
7657
+ var import_yaml3 = require("yaml");
7658
+ var import_core9 = require("@harness-engineering/core");
7659
+ var GateNotReadyError = class extends Error {
7660
+ constructor(message) {
7661
+ super(message);
7662
+ this.name = "GateNotReadyError";
7663
+ }
7664
+ };
7665
+ var PromotionError = class extends Error {
7666
+ constructor(message) {
7667
+ super(message);
7668
+ this.name = "PromotionError";
7669
+ }
7670
+ };
7671
+ var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7672
+ function skillDir(projectPath, name) {
7673
+ return path11.join(projectPath, "agents", "skills", "claude-code", name);
7674
+ }
7675
+ function readIfExists(p) {
7676
+ try {
7677
+ return fs11.readFileSync(p, "utf-8");
7678
+ } catch {
7679
+ return null;
7680
+ }
7681
+ }
7682
+ function injectProvenanceIntoYaml(yamlText, proposalId) {
7683
+ let doc;
7684
+ try {
7685
+ doc = (0, import_yaml3.parse)(yamlText);
7686
+ } catch (err) {
7687
+ throw new PromotionError(
7688
+ `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
7689
+ );
7690
+ }
7691
+ if (!doc || typeof doc !== "object") {
7692
+ throw new PromotionError("skill.yaml top-level is not a mapping");
7693
+ }
7694
+ const obj = doc;
7695
+ obj["provenance"] = "agent-proposed";
7696
+ obj["originatingProposalId"] = proposalId;
7697
+ return (0, import_yaml3.stringify)(obj);
7698
+ }
7699
+ function assertGateReady(proposal) {
7700
+ if (proposal.status !== "gate-running") {
7701
+ throw new GateNotReadyError(
7702
+ `proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
7703
+ );
7704
+ }
7705
+ const findings = proposal.gate?.findings ?? [];
7706
+ if (findings.some((f) => f.severity === "error")) {
7707
+ throw new GateNotReadyError(
7708
+ `proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
7709
+ );
7710
+ }
7711
+ if (!proposal.gate?.lastRunAt) {
7712
+ throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
7713
+ }
7714
+ const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
7715
+ if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
7716
+ throw new GateNotReadyError(
7717
+ `proposal ${proposal.id} gate run is older than 24h; re-run before approving`
7718
+ );
7719
+ }
7720
+ }
7721
+ async function promoteNewSkill(projectPath, proposal) {
7722
+ const target = skillDir(projectPath, proposal.content.name);
7723
+ if (fs11.existsSync(target)) {
7724
+ throw new PromotionError(
7725
+ `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7726
+ );
7727
+ }
7728
+ fs11.mkdirSync(target, { recursive: true });
7729
+ const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
7730
+ fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
7731
+ fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7732
+ return { skillPath: target };
7733
+ }
7734
+ async function promoteRefinement(projectPath, proposal) {
7735
+ if (!proposal.targetSkill) {
7736
+ throw new PromotionError("refinement proposal is missing targetSkill");
7737
+ }
7738
+ const target = skillDir(projectPath, proposal.targetSkill);
7739
+ if (!fs11.existsSync(target)) {
7740
+ throw new PromotionError(
7741
+ `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7742
+ );
7743
+ }
7744
+ const yamlPath = path11.join(target, "skill.yaml");
7745
+ const before = readIfExists(yamlPath) ?? "";
7746
+ const after = injectProvenanceIntoYaml(before, proposal.id);
7747
+ if (after === before) {
7748
+ throw new PromotionError(
7749
+ "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7750
+ );
7751
+ }
7752
+ fs11.writeFileSync(yamlPath, after);
7753
+ return { skillPath: target };
7754
+ }
7755
+ async function promote(projectPath, proposalId, decidedBy) {
7756
+ const proposal = await (0, import_core9.getProposal)(projectPath, proposalId);
7757
+ if (!proposal) throw new import_core9.ProposalNotFoundError(proposalId);
7758
+ assertGateReady(proposal);
7759
+ const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
7760
+ await (0, import_core9.updateProposal)(projectPath, proposalId, {
7761
+ status: "approved",
7762
+ decision: {
7763
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
7764
+ decidedBy,
7765
+ action: "approved"
7766
+ }
7767
+ });
7768
+ return {
7769
+ proposalId,
7770
+ skillPath: out.skillPath,
7771
+ provenance: "agent-proposed"
7772
+ };
7773
+ }
7774
+
7775
+ // src/proposals/events.ts
7776
+ function emit3(bus, topic, data) {
7777
+ bus.emit(topic, data);
7778
+ }
7779
+ function emitProposalCreated(bus, proposal) {
7780
+ const data = {
7781
+ id: proposal.id,
7782
+ kind: proposal.kind,
7783
+ name: proposal.content.name,
7784
+ proposedBy: proposal.proposedBy,
7785
+ justification: proposal.source.justification
7786
+ };
7787
+ if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
7788
+ emit3(bus, "proposal.created", data);
7789
+ }
7790
+ function emitProposalApproved(bus, proposal) {
7791
+ const data = {
7792
+ id: proposal.id,
7793
+ kind: proposal.kind,
7794
+ name: proposal.content.name,
7795
+ decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
7796
+ };
7797
+ if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
7798
+ emit3(bus, "proposal.approved", data);
7799
+ }
7800
+ function emitProposalRejected(bus, proposal) {
7801
+ const data = {
7802
+ id: proposal.id,
7803
+ kind: proposal.kind,
7804
+ name: proposal.content.name,
7805
+ decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
7806
+ reason: proposal.decision?.reason ?? "(no reason given)"
7807
+ };
7808
+ emit3(bus, "proposal.rejected", data);
7809
+ }
7810
+
7811
+ // src/server/routes/v1/proposals.ts
7812
+ var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
7813
+ var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
7814
+ var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
7815
+ var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
7816
+ var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
7817
+ var ProposalStatusValues = [
7818
+ "open",
7819
+ "gate-running",
7820
+ "gate-failed",
7821
+ "approved",
7822
+ "rejected"
7823
+ ];
7824
+ var RejectBody = import_zod13.z.object({
7825
+ reason: import_zod13.z.string().min(1).max(280)
7826
+ });
7827
+ function sendJSON8(res, status, body) {
7828
+ res.writeHead(status, { "Content-Type": "application/json" });
7829
+ res.end(JSON.stringify(body));
7830
+ }
7831
+ function getDecidedBy(req, deps) {
7832
+ if (deps.decidedByResolver) return deps.decidedByResolver(req);
7833
+ const token = req._authToken;
7834
+ return token?.id ?? "unknown";
7835
+ }
7836
+ function parseStatusFromQuery(url) {
7837
+ const queryIdx = url.indexOf("?");
7838
+ if (queryIdx === -1) return void 0;
7839
+ const params = new URLSearchParams(url.slice(queryIdx + 1));
7840
+ const raw = params.get("status");
7841
+ if (!raw) return void 0;
7842
+ if (raw === "all") return "all";
7843
+ if (ProposalStatusValues.includes(raw)) return raw;
7844
+ return void 0;
7845
+ }
7846
+ async function handleList(req, res, deps) {
7847
+ const url = req.url ?? "";
7848
+ const status = parseStatusFromQuery(url);
7849
+ const proposals = await (0, import_core10.listProposals)(deps.projectPath, status ? { status } : {});
7850
+ sendJSON8(res, 200, proposals);
7851
+ }
7852
+ async function handleGet(res, deps, id) {
7853
+ const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
7854
+ if (!proposal) {
7855
+ sendJSON8(res, 404, { error: "Proposal not found" });
7856
+ return;
7857
+ }
7858
+ sendJSON8(res, 200, proposal);
7859
+ }
7860
+ async function handleRunGate(res, deps, id) {
7861
+ try {
7862
+ const result = await runGate(deps.projectPath, id);
7863
+ sendJSON8(res, 200, result);
7864
+ } catch (err) {
7865
+ if (err instanceof import_core10.ProposalNotFoundError) {
7866
+ sendJSON8(res, 404, { error: err.message });
7867
+ return;
7868
+ }
7869
+ if (err instanceof GateRunError) {
7870
+ sendJSON8(res, 409, { error: err.message });
7871
+ return;
7872
+ }
7873
+ sendJSON8(res, 500, {
7874
+ error: "gate run failed",
7875
+ detail: err instanceof Error ? err.message : "unknown"
7876
+ });
7877
+ }
7878
+ }
7879
+ async function handleApprove(req, res, deps, id) {
7880
+ const decidedBy = getDecidedBy(req, deps);
7881
+ try {
7882
+ const result = await promote(deps.projectPath, id, decidedBy);
7883
+ const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
7884
+ if (proposal) emitProposalApproved(deps.bus, proposal);
7885
+ sendJSON8(res, 200, { promotion: result, proposal });
7886
+ } catch (err) {
7887
+ if (err instanceof import_core10.ProposalNotFoundError) {
7888
+ sendJSON8(res, 404, { error: err.message });
7889
+ return;
7890
+ }
7891
+ if (err instanceof GateNotReadyError) {
7892
+ sendJSON8(res, 409, { error: err.message });
7893
+ return;
7894
+ }
7895
+ if (err instanceof PromotionError) {
7896
+ sendJSON8(res, 422, { error: err.message });
7897
+ return;
7898
+ }
7899
+ sendJSON8(res, 500, {
7900
+ error: "approve failed",
7901
+ detail: err instanceof Error ? err.message : "unknown"
7902
+ });
7903
+ }
7904
+ }
7905
+ async function handleReject(req, res, deps, id) {
7906
+ let raw;
7907
+ try {
7908
+ raw = await readBody(req);
7909
+ } catch (err) {
7910
+ sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
7911
+ return;
7912
+ }
7913
+ let json;
7914
+ try {
7915
+ json = raw.length > 0 ? JSON.parse(raw) : {};
7916
+ } catch {
7917
+ sendJSON8(res, 400, { error: "Invalid JSON body" });
7918
+ return;
7919
+ }
7920
+ const parsed = RejectBody.safeParse(json);
7921
+ if (!parsed.success) {
7922
+ sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
7923
+ return;
7924
+ }
7925
+ const proposal = await (0, import_core10.getProposal)(deps.projectPath, id);
7926
+ if (!proposal) {
7927
+ sendJSON8(res, 404, { error: "Proposal not found" });
7928
+ return;
7929
+ }
7930
+ if (proposal.status === "approved" || proposal.status === "rejected") {
7931
+ sendJSON8(res, 409, {
7932
+ error: `proposal already ${proposal.status}; cannot reject`
7933
+ });
7934
+ return;
7935
+ }
7936
+ const decidedBy = getDecidedBy(req, deps);
7937
+ const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
7938
+ status: "rejected",
7939
+ decision: {
7940
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
7941
+ decidedBy,
7942
+ action: "rejected",
7943
+ reason: parsed.data.reason
7944
+ }
7945
+ });
7946
+ emitProposalRejected(deps.bus, updated);
7947
+ sendJSON8(res, 200, updated);
7948
+ }
7949
+ async function handleEdit(req, res, deps, id) {
7950
+ let raw;
7951
+ try {
7952
+ raw = await readBody(req);
7953
+ } catch (err) {
7954
+ sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
7955
+ return;
7956
+ }
7957
+ let json;
7958
+ try {
7959
+ json = JSON.parse(raw);
7960
+ } catch {
7961
+ sendJSON8(res, 400, { error: "Invalid JSON body" });
7962
+ return;
7963
+ }
7964
+ const parsed = import_types24.EditProposalInputSchema.safeParse(json);
7965
+ if (!parsed.success) {
7966
+ sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
7967
+ return;
7968
+ }
7969
+ const existing = await (0, import_core10.getProposal)(deps.projectPath, id);
7970
+ if (!existing) {
7971
+ sendJSON8(res, 404, { error: "Proposal not found" });
7972
+ return;
7973
+ }
7974
+ if (existing.status === "approved" || existing.status === "rejected") {
7975
+ sendJSON8(res, 409, {
7976
+ error: `proposal already ${existing.status}; cannot edit`
7977
+ });
7978
+ return;
7979
+ }
7980
+ const mergedContent = {
7981
+ ...existing.content,
7982
+ ...parsed.data.content,
7983
+ name: parsed.data.content.name ?? existing.content.name,
7984
+ description: parsed.data.content.description ?? existing.content.description
7985
+ };
7986
+ try {
7987
+ const updated = await (0, import_core10.updateProposal)(deps.projectPath, id, {
7988
+ content: mergedContent,
7989
+ status: "open",
7990
+ gate: void 0
7991
+ });
7992
+ sendJSON8(res, 200, updated);
7993
+ } catch (err) {
7994
+ sendJSON8(res, 422, {
7995
+ error: "edit failed",
7996
+ detail: err instanceof Error ? err.message : "unknown"
7997
+ });
7998
+ }
7999
+ }
8000
+ function handleV1ProposalsRoute(req, res, deps) {
8001
+ const url = req.url ?? "";
8002
+ const method = req.method ?? "GET";
8003
+ if (method === "GET" && LIST_RE.test(url)) {
8004
+ void handleList(req, res, deps);
8005
+ return true;
8006
+ }
8007
+ const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
8008
+ if (runGateMatch) {
8009
+ void handleRunGate(res, deps, runGateMatch[1]);
8010
+ return true;
8011
+ }
8012
+ const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
8013
+ if (approveMatch) {
8014
+ void handleApprove(req, res, deps, approveMatch[1]);
8015
+ return true;
8016
+ }
8017
+ const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
8018
+ if (rejectMatch) {
8019
+ void handleReject(req, res, deps, rejectMatch[1]);
8020
+ return true;
8021
+ }
8022
+ if (method === "PATCH") {
8023
+ const m = SINGLE_RE.exec(url);
8024
+ if (m) {
8025
+ void handleEdit(req, res, deps, m[1]);
8026
+ return true;
8027
+ }
8028
+ }
8029
+ if (method === "GET") {
8030
+ const m = SINGLE_RE.exec(url);
8031
+ if (m) {
8032
+ void handleGet(res, deps, m[1]);
8033
+ return true;
8034
+ }
8035
+ }
8036
+ return false;
8037
+ }
8038
+
8039
+ // src/server/routes/sessions.ts
8040
+ var fs12 = __toESM(require("fs/promises"));
8041
+ var path12 = __toESM(require("path"));
8042
+ var import_zod14 = require("zod");
8043
+ var SessionCreateSchema = import_zod14.z.object({
8044
+ sessionId: import_zod14.z.string().min(1)
8045
+ }).passthrough();
8046
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
8047
+ function isSafeId(id) {
8048
+ return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
8049
+ }
6924
8050
  function jsonResponse(res, status, data) {
6925
8051
  res.writeHead(status, { "Content-Type": "application/json" });
6926
8052
  res.end(JSON.stringify(data));
6927
8053
  }
6928
8054
  function extractSessionId(url) {
6929
- const segments = new URL(url, "http://localhost").pathname.split(path11.posix.sep);
8055
+ const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
6930
8056
  const id = segments.pop();
6931
8057
  return id && id !== "sessions" ? id : null;
6932
8058
  }
6933
- async function handleList(res, sessionsDir) {
8059
+ async function handleList2(res, sessionsDir) {
6934
8060
  try {
6935
- const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8061
+ const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
6936
8062
  const sessions = [];
6937
8063
  for (const entry of entries) {
6938
8064
  if (!entry.isDirectory()) continue;
6939
8065
  try {
6940
- const content = await fs11.readFile(
6941
- path11.join(sessionsDir, entry.name, "session.json"),
8066
+ const content = await fs12.readFile(
8067
+ path12.join(sessionsDir, entry.name, "session.json"),
6942
8068
  "utf-8"
6943
8069
  );
6944
8070
  sessions.push(JSON.parse(content));
@@ -6957,13 +8083,13 @@ async function handleList(res, sessionsDir) {
6957
8083
  jsonResponse(res, 500, { error: "Failed to list sessions" });
6958
8084
  }
6959
8085
  }
6960
- async function handleGet(res, id, sessionsDir) {
8086
+ async function handleGet2(res, id, sessionsDir) {
6961
8087
  if (!isSafeId(id)) {
6962
8088
  jsonResponse(res, 400, { error: "Invalid sessionId" });
6963
8089
  return;
6964
8090
  }
6965
8091
  try {
6966
- const content = await fs11.readFile(path11.join(sessionsDir, id, "session.json"), "utf-8");
8092
+ const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
6967
8093
  jsonResponse(res, 200, JSON.parse(content));
6968
8094
  } catch (err) {
6969
8095
  if (err.code === "ENOENT") {
@@ -6986,9 +8112,9 @@ async function handleCreate(req, res, sessionsDir) {
6986
8112
  jsonResponse(res, 400, { error: "Invalid sessionId" });
6987
8113
  return;
6988
8114
  }
6989
- const sessionDir = path11.join(sessionsDir, session.sessionId);
6990
- await fs11.mkdir(sessionDir, { recursive: true });
6991
- await fs11.writeFile(path11.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8115
+ const sessionDir = path12.join(sessionsDir, session.sessionId);
8116
+ await fs12.mkdir(sessionDir, { recursive: true });
8117
+ await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
6992
8118
  jsonResponse(res, 200, { ok: true });
6993
8119
  } catch {
6994
8120
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -7002,10 +8128,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
7002
8128
  return;
7003
8129
  }
7004
8130
  const body = await readBody(req);
7005
- const updates = import_zod13.z.record(import_zod13.z.unknown()).parse(JSON.parse(body));
7006
- const sessionFilePath = path11.join(sessionsDir, id, "session.json");
7007
- const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
7008
- await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8131
+ const updates = import_zod14.z.record(import_zod14.z.unknown()).parse(JSON.parse(body));
8132
+ const sessionFilePath = path12.join(sessionsDir, id, "session.json");
8133
+ const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
8134
+ await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
7009
8135
  jsonResponse(res, 200, { ok: true });
7010
8136
  } catch {
7011
8137
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -7018,7 +8144,7 @@ async function handleDelete(res, url, sessionsDir) {
7018
8144
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
7019
8145
  return;
7020
8146
  }
7021
- await fs11.rm(path11.join(sessionsDir, id), { recursive: true, force: true });
8147
+ await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
7022
8148
  jsonResponse(res, 200, { ok: true });
7023
8149
  } catch {
7024
8150
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -7031,8 +8157,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
7031
8157
  switch (method) {
7032
8158
  case "GET": {
7033
8159
  const id = extractSessionId(url);
7034
- if (id) void handleGet(res, id, sessionsDir);
7035
- else void handleList(res, sessionsDir);
8160
+ if (id) void handleGet2(res, id, sessionsDir);
8161
+ else void handleList2(res, sessionsDir);
7036
8162
  return true;
7037
8163
  }
7038
8164
  case "POST":
@@ -7122,16 +8248,16 @@ function handleStreamsRoute(req, res, recorder) {
7122
8248
  }
7123
8249
 
7124
8250
  // src/server/routes/auth.ts
7125
- var import_zod14 = require("zod");
7126
- var import_types22 = require("@harness-engineering/types");
7127
- var CreateBodySchema = import_zod14.z.object({
7128
- 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(),
7131
- tenantId: import_zod14.z.string().optional(),
7132
- expiresAt: import_zod14.z.string().datetime().optional()
8251
+ var import_zod15 = require("zod");
8252
+ var import_types25 = require("@harness-engineering/types");
8253
+ var CreateBodySchema = import_zod15.z.object({
8254
+ name: import_zod15.z.string().min(1).max(100),
8255
+ scopes: import_zod15.z.array(import_types25.TokenScopeSchema).min(1),
8256
+ bridgeKind: import_types25.BridgeKindSchema.optional(),
8257
+ tenantId: import_zod15.z.string().optional(),
8258
+ expiresAt: import_zod15.z.string().datetime().optional()
7133
8259
  });
7134
- function sendJSON8(res, status, body) {
8260
+ function sendJSON9(res, status, body) {
7135
8261
  res.writeHead(status, { "Content-Type": "application/json" });
7136
8262
  res.end(JSON.stringify(body));
7137
8263
  }
@@ -7141,19 +8267,19 @@ async function handlePost(req, res, store) {
7141
8267
  raw = await readBody(req);
7142
8268
  } catch (err) {
7143
8269
  const msg = err instanceof Error ? err.message : "Failed to read body";
7144
- sendJSON8(res, 413, { error: msg });
8270
+ sendJSON9(res, 413, { error: msg });
7145
8271
  return;
7146
8272
  }
7147
8273
  let json;
7148
8274
  try {
7149
8275
  json = JSON.parse(raw);
7150
8276
  } catch {
7151
- sendJSON8(res, 400, { error: "Invalid JSON body" });
8277
+ sendJSON9(res, 400, { error: "Invalid JSON body" });
7152
8278
  return;
7153
8279
  }
7154
8280
  const parsed = CreateBodySchema.safeParse(json);
7155
8281
  if (!parsed.success) {
7156
- sendJSON8(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8282
+ sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
7157
8283
  return;
7158
8284
  }
7159
8285
  try {
@@ -7165,38 +8291,38 @@ async function handlePost(req, res, store) {
7165
8291
  if (parsed.data.tenantId !== void 0) input.tenantId = parsed.data.tenantId;
7166
8292
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
7167
8293
  const result = await store.create(input);
7168
- const publicRecord = import_types22.AuthTokenPublicSchema.parse(result.record);
7169
- sendJSON8(res, 200, {
8294
+ const publicRecord = import_types25.AuthTokenPublicSchema.parse(result.record);
8295
+ sendJSON9(res, 200, {
7170
8296
  ...publicRecord,
7171
8297
  token: result.token
7172
8298
  });
7173
8299
  } catch (err) {
7174
8300
  const msg = err instanceof Error ? err.message : "Failed to create token";
7175
8301
  if (msg.includes("already exists")) {
7176
- sendJSON8(res, 409, { error: msg });
8302
+ sendJSON9(res, 409, { error: msg });
7177
8303
  return;
7178
8304
  }
7179
- sendJSON8(res, 500, { error: "Internal error creating token" });
8305
+ sendJSON9(res, 500, { error: "Internal error creating token" });
7180
8306
  }
7181
8307
  }
7182
- async function handleList2(res, store) {
8308
+ async function handleList3(res, store) {
7183
8309
  try {
7184
8310
  const list = await store.list();
7185
- sendJSON8(res, 200, list);
8311
+ sendJSON9(res, 200, list);
7186
8312
  } catch {
7187
- sendJSON8(res, 500, { error: "Internal error listing tokens" });
8313
+ sendJSON9(res, 500, { error: "Internal error listing tokens" });
7188
8314
  }
7189
8315
  }
7190
8316
  async function handleDelete2(res, store, id) {
7191
8317
  try {
7192
8318
  const ok = await store.revoke(id);
7193
8319
  if (!ok) {
7194
- sendJSON8(res, 404, { error: "Token not found" });
8320
+ sendJSON9(res, 404, { error: "Token not found" });
7195
8321
  return;
7196
8322
  }
7197
- sendJSON8(res, 200, { deleted: true });
8323
+ sendJSON9(res, 200, { deleted: true });
7198
8324
  } catch {
7199
- sendJSON8(res, 500, { error: "Internal error revoking token" });
8325
+ sendJSON9(res, 500, { error: "Internal error revoking token" });
7200
8326
  }
7201
8327
  }
7202
8328
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -7210,7 +8336,7 @@ function handleAuthRoute(req, res, store) {
7210
8336
  return true;
7211
8337
  }
7212
8338
  if (method === "GET" && pathname === "/api/v1/auth/tokens") {
7213
- void handleList2(res, store);
8339
+ void handleList3(res, store);
7214
8340
  return true;
7215
8341
  }
7216
8342
  if (method === "DELETE") {
@@ -7221,12 +8347,12 @@ function handleAuthRoute(req, res, store) {
7221
8347
  return true;
7222
8348
  }
7223
8349
  }
7224
- sendJSON8(res, 405, { error: "Method not allowed" });
8350
+ sendJSON9(res, 405, { error: "Method not allowed" });
7225
8351
  return true;
7226
8352
  }
7227
8353
 
7228
8354
  // src/server/routes/local-model.ts
7229
- function sendJSON9(res, status, body) {
8355
+ function sendJSON10(res, status, body) {
7230
8356
  res.writeHead(status, { "Content-Type": "application/json" });
7231
8357
  res.end(JSON.stringify(body));
7232
8358
  }
@@ -7234,36 +8360,36 @@ function handleLocalModelRoute(req, res, getStatus) {
7234
8360
  const { method, url } = req;
7235
8361
  if (url !== "/api/v1/local-model/status") return false;
7236
8362
  if (method !== "GET") {
7237
- sendJSON9(res, 405, { error: "Method not allowed" });
8363
+ sendJSON10(res, 405, { error: "Method not allowed" });
7238
8364
  return true;
7239
8365
  }
7240
8366
  if (!getStatus) {
7241
- sendJSON9(res, 503, { error: "Local backend not configured" });
8367
+ sendJSON10(res, 503, { error: "Local backend not configured" });
7242
8368
  return true;
7243
8369
  }
7244
8370
  const status = getStatus();
7245
8371
  if (!status) {
7246
- sendJSON9(res, 503, { error: "Local backend not configured" });
8372
+ sendJSON10(res, 503, { error: "Local backend not configured" });
7247
8373
  return true;
7248
8374
  }
7249
- sendJSON9(res, 200, status);
8375
+ sendJSON10(res, 200, status);
7250
8376
  return true;
7251
8377
  }
7252
8378
  function handleLocalModelsRoute(req, res, getStatuses) {
7253
8379
  const { method, url } = req;
7254
8380
  if (url !== "/api/v1/local-models/status") return false;
7255
8381
  if (method !== "GET") {
7256
- sendJSON9(res, 405, { error: "Method not allowed" });
8382
+ sendJSON10(res, 405, { error: "Method not allowed" });
7257
8383
  return true;
7258
8384
  }
7259
8385
  const statuses = getStatuses ? getStatuses() : [];
7260
- sendJSON9(res, 200, statuses);
8386
+ sendJSON10(res, 200, statuses);
7261
8387
  return true;
7262
8388
  }
7263
8389
 
7264
8390
  // src/server/static.ts
7265
- var fs12 = __toESM(require("fs"));
7266
- var path12 = __toESM(require("path"));
8391
+ var fs13 = __toESM(require("fs"));
8392
+ var path13 = __toESM(require("path"));
7267
8393
  var MIME_TYPES = {
7268
8394
  ".html": "text/html; charset=utf-8",
7269
8395
  ".js": "application/javascript; charset=utf-8",
@@ -7283,29 +8409,29 @@ var MIME_TYPES = {
7283
8409
  function handleStaticFile(req, res, dashboardDir) {
7284
8410
  const { method, url } = req;
7285
8411
  if (method !== "GET") return false;
7286
- const apiPrefix = path12.posix.join(path12.posix.sep, "api", path12.posix.sep);
7287
- const wsPath = path12.posix.join(path12.posix.sep, "ws");
8412
+ const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
8413
+ const wsPath = path13.posix.join(path13.posix.sep, "ws");
7288
8414
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
7289
8415
  const urlPath = new URL(url ?? "/", "http://localhost").pathname;
7290
- const requestedPath = path12.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
7291
- const resolved = path12.resolve(requestedPath);
7292
- if (!resolved.startsWith(path12.resolve(dashboardDir))) {
7293
- return serveFile(path12.join(dashboardDir, "index.html"), res);
8416
+ const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8417
+ const resolved = path13.resolve(requestedPath);
8418
+ if (!resolved.startsWith(path13.resolve(dashboardDir))) {
8419
+ return serveFile(path13.join(dashboardDir, "index.html"), res);
7294
8420
  }
7295
- if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
8421
+ if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
7296
8422
  return serveFile(resolved, res);
7297
8423
  }
7298
- const indexPath = path12.join(dashboardDir, "index.html");
7299
- if (fs12.existsSync(indexPath)) {
8424
+ const indexPath = path13.join(dashboardDir, "index.html");
8425
+ if (fs13.existsSync(indexPath)) {
7300
8426
  return serveFile(indexPath, res);
7301
8427
  }
7302
8428
  return false;
7303
8429
  }
7304
8430
  function serveFile(filePath, res) {
7305
- const ext = path12.extname(filePath).toLowerCase();
8431
+ const ext = path13.extname(filePath).toLowerCase();
7306
8432
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7307
8433
  try {
7308
- const content = fs12.readFileSync(filePath);
8434
+ const content = fs13.readFileSync(filePath);
7309
8435
  res.writeHead(200, { "Content-Type": contentType });
7310
8436
  res.end(content);
7311
8437
  return true;
@@ -7315,8 +8441,8 @@ function serveFile(filePath, res) {
7315
8441
  }
7316
8442
 
7317
8443
  // src/server/plan-watcher.ts
7318
- var fs13 = __toESM(require("fs"));
7319
- var path13 = __toESM(require("path"));
8444
+ var fs14 = __toESM(require("fs"));
8445
+ var path14 = __toESM(require("path"));
7320
8446
  var PlanWatcher = class {
7321
8447
  plansDir;
7322
8448
  queue;
@@ -7330,11 +8456,11 @@ var PlanWatcher = class {
7330
8456
  * Creates the directory if it does not exist.
7331
8457
  */
7332
8458
  start() {
7333
- fs13.mkdirSync(this.plansDir, { recursive: true });
7334
- this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
8459
+ fs14.mkdirSync(this.plansDir, { recursive: true });
8460
+ this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
7335
8461
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
7336
- const filePath = path13.join(this.plansDir, filename);
7337
- if (fs13.existsSync(filePath)) {
8462
+ const filePath = path14.join(this.plansDir, filename);
8463
+ if (fs14.existsSync(filePath)) {
7338
8464
  void this.handleNewPlan(filename);
7339
8465
  }
7340
8466
  }
@@ -7369,7 +8495,7 @@ var import_node_crypto9 = require("crypto");
7369
8495
  var import_promises = require("fs/promises");
7370
8496
  var import_node_path = require("path");
7371
8497
  var import_bcryptjs = __toESM(require("bcryptjs"));
7372
- var import_types23 = require("@harness-engineering/types");
8498
+ var import_types26 = require("@harness-engineering/types");
7373
8499
  var BCRYPT_ROUNDS = 12;
7374
8500
  var LEGACY_ENV_ID = "tok_legacy_env";
7375
8501
  function genId() {
@@ -7384,8 +8510,8 @@ function parseToken(raw) {
7384
8510
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
7385
8511
  }
7386
8512
  var TokenStore = class {
7387
- constructor(path17) {
7388
- this.path = path17;
8513
+ constructor(path22) {
8514
+ this.path = path22;
7389
8515
  }
7390
8516
  path;
7391
8517
  cache = null;
@@ -7396,7 +8522,7 @@ var TokenStore = class {
7396
8522
  const parsed = JSON.parse(raw);
7397
8523
  const list = Array.isArray(parsed) ? parsed : [];
7398
8524
  this.cache = list.map((entry) => {
7399
- const r = import_types23.AuthTokenSchema.safeParse(entry);
8525
+ const r = import_types26.AuthTokenSchema.safeParse(entry);
7400
8526
  return r.success ? r.data : null;
7401
8527
  }).filter((x) => x !== null);
7402
8528
  } catch (err) {
@@ -7458,7 +8584,7 @@ var TokenStore = class {
7458
8584
  }
7459
8585
  async list() {
7460
8586
  const records = await this.load();
7461
- return records.map((r) => import_types23.AuthTokenPublicSchema.parse(r));
8587
+ return records.map((r) => import_types26.AuthTokenPublicSchema.parse(r));
7462
8588
  }
7463
8589
  async revoke(id) {
7464
8590
  const records = await this.load();
@@ -7490,10 +8616,10 @@ var TokenStore = class {
7490
8616
  // src/auth/audit.ts
7491
8617
  var import_promises2 = require("fs/promises");
7492
8618
  var import_node_path2 = require("path");
7493
- var import_types24 = require("@harness-engineering/types");
8619
+ var import_types27 = require("@harness-engineering/types");
7494
8620
  var AuditLogger = class {
7495
- constructor(path17, opts = {}) {
7496
- this.path = path17;
8621
+ constructor(path22, opts = {}) {
8622
+ this.path = path22;
7497
8623
  this.opts = opts;
7498
8624
  }
7499
8625
  path;
@@ -7501,7 +8627,7 @@ var AuditLogger = class {
7501
8627
  queue = Promise.resolve();
7502
8628
  dirEnsured = false;
7503
8629
  async append(input) {
7504
- const entry = import_types24.AuthAuditEntrySchema.parse({
8630
+ const entry = import_types27.AuthAuditEntrySchema.parse({
7505
8631
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7506
8632
  tokenId: input.tokenId,
7507
8633
  ...input.tenantId ? { tenantId: input.tenantId } : {},
@@ -7577,6 +8703,43 @@ var V1_BRIDGE_ROUTES = [
7577
8703
  scope: "subscribe-webhook",
7578
8704
  description: "Webhook delivery queue depth + DLQ stats."
7579
8705
  },
8706
+ // Hermes Phase 4 — skill proposal review queue.
8707
+ {
8708
+ method: "GET",
8709
+ pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
8710
+ scope: "read-status",
8711
+ description: "List skill proposals (open + decided)."
8712
+ },
8713
+ {
8714
+ method: "GET",
8715
+ pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
8716
+ scope: "read-status",
8717
+ description: "Get a single skill proposal."
8718
+ },
8719
+ {
8720
+ method: "POST",
8721
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
8722
+ scope: "manage-proposals",
8723
+ description: "Run the soundness-review gate against a proposal."
8724
+ },
8725
+ {
8726
+ method: "POST",
8727
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
8728
+ scope: "manage-proposals",
8729
+ description: "Approve a proposal \u2014 promotes the skill into the catalog."
8730
+ },
8731
+ {
8732
+ method: "POST",
8733
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
8734
+ scope: "manage-proposals",
8735
+ description: "Reject a proposal with a one-line reason."
8736
+ },
8737
+ {
8738
+ method: "PATCH",
8739
+ pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
8740
+ scope: "manage-proposals",
8741
+ description: "Edit proposal content (resets gate to not-run)."
8742
+ },
7580
8743
  // ── Phase 5 bridge primitives ──
7581
8744
  {
7582
8745
  method: "GET",
@@ -7588,9 +8751,9 @@ var V1_BRIDGE_ROUTES = [
7588
8751
  function isV1Bridge(method, url) {
7589
8752
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
7590
8753
  }
7591
- function requiredBridgeScope(method, path17) {
8754
+ function requiredBridgeScope(method, path22) {
7592
8755
  for (const r of V1_BRIDGE_ROUTES) {
7593
- if (r.method === method && r.pattern.test(path17)) return r.scope;
8756
+ if (r.method === method && r.pattern.test(path22)) return r.scope;
7594
8757
  }
7595
8758
  return null;
7596
8759
  }
@@ -7600,24 +8763,24 @@ function hasScope(held, required) {
7600
8763
  if (held.includes("admin")) return true;
7601
8764
  return held.includes(required);
7602
8765
  }
7603
- function requiredScopeForRoute(method, path17) {
7604
- const bridgeScope = requiredBridgeScope(method, path17);
8766
+ function requiredScopeForRoute(method, path22) {
8767
+ const bridgeScope = requiredBridgeScope(method, path22);
7605
8768
  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"))
8769
+ if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
8770
+ if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
8771
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
8772
+ if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
8773
+ if (path22.startsWith("/api/interactions")) return "resolve-interaction";
8774
+ if (path22.startsWith("/api/plans")) return "read-status";
8775
+ if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
8776
+ if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
8777
+ if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
8778
+ if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
7616
8779
  return "read-status";
7617
- if (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";
8780
+ if (path22.startsWith("/api/maintenance")) return "trigger-job";
8781
+ if (path22.startsWith("/api/streams")) return "read-status";
8782
+ if (path22.startsWith("/api/sessions")) return "read-status";
8783
+ if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
7621
8784
  return null;
7622
8785
  }
7623
8786
 
@@ -7671,6 +8834,11 @@ var OrchestratorServer = class {
7671
8834
  roadmapPath;
7672
8835
  dispatchAdHoc;
7673
8836
  sessionsDir;
8837
+ /**
8838
+ * Project root used by file-backed routes (Phase 4 proposals at
8839
+ * `.harness/proposals/`). Defaults to process.cwd().
8840
+ */
8841
+ projectPath;
7674
8842
  maintenanceDeps = null;
7675
8843
  getLocalModelStatus = null;
7676
8844
  getLocalModelStatuses = null;
@@ -7688,8 +8856,8 @@ var OrchestratorServer = class {
7688
8856
  this.orchestrator = orchestrator;
7689
8857
  this.port = port;
7690
8858
  this.initDependencies(deps);
7691
- const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path14.resolve(".harness", "tokens.json");
7692
- const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path14.resolve(".harness", "audit.log");
8859
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
8860
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
7693
8861
  this.tokenStore = new TokenStore(tokensPath);
7694
8862
  this.auditLogger = new AuditLogger(auditPath);
7695
8863
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -7702,14 +8870,15 @@ var OrchestratorServer = class {
7702
8870
  }
7703
8871
  initDependencies(deps) {
7704
8872
  this.interactionQueue = deps?.interactionQueue;
7705
- this.plansDir = deps?.plansDir ?? path14.resolve("docs", "plans");
7706
- this.dashboardDir = deps?.dashboardDir ?? path14.resolve("packages", "dashboard", "dist", "client");
8873
+ this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8874
+ this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
7707
8875
  this.claudeCommand = deps?.claudeCommand ?? "claude";
7708
8876
  this.pipeline = deps?.pipeline ?? null;
7709
8877
  this.analysisArchive = deps?.analysisArchive;
7710
8878
  this.roadmapPath = deps?.roadmapPath ?? null;
7711
8879
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
7712
- this.sessionsDir = deps?.sessionsDir ?? path14.resolve(".harness", "sessions");
8880
+ this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
8881
+ this.projectPath = deps?.projectPath ?? process.cwd();
7713
8882
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
7714
8883
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
7715
8884
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
@@ -7880,6 +9049,15 @@ var OrchestratorServer = class {
7880
9049
  (req, res) => handleV1TelemetryRoute(req, res, {
7881
9050
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
7882
9051
  }),
9052
+ // Hermes Phase 4 — skill proposal review queue. Read scopes
9053
+ // (`read-status`) and write scopes (`manage-proposals`) are enforced
9054
+ // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
9055
+ // business logic. `projectPath` defaults to process.cwd() — that is
9056
+ // where `.harness/proposals/` lives in every deployment we ship.
9057
+ (req, res) => handleV1ProposalsRoute(req, res, {
9058
+ projectPath: this.projectPath,
9059
+ bus: this.orchestrator
9060
+ }),
7883
9061
  // Chat proxy route (spawns Claude Code CLI — no API key required)
7884
9062
  (req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
7885
9063
  ];
@@ -7962,16 +9140,16 @@ var OrchestratorServer = class {
7962
9140
  return this.broadcaster.clientCount;
7963
9141
  }
7964
9142
  async start() {
7965
- (0, import_core8.assertPortUsable)(this.port, "orchestrator");
9143
+ (0, import_core11.assertPortUsable)(this.port, "orchestrator");
7966
9144
  if (this.interactionQueue) {
7967
9145
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
7968
9146
  this.planWatcher.start();
7969
9147
  }
7970
- return new Promise((resolve6) => {
9148
+ return new Promise((resolve7) => {
7971
9149
  const host = getBindHost();
7972
9150
  this.httpServer.listen(this.port, host, () => {
7973
9151
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
7974
- resolve6();
9152
+ resolve7();
7975
9153
  });
7976
9154
  });
7977
9155
  }
@@ -7991,7 +9169,7 @@ var OrchestratorServer = class {
7991
9169
  var import_node_crypto11 = require("crypto");
7992
9170
  var import_promises3 = require("fs/promises");
7993
9171
  var import_node_path3 = require("path");
7994
- var import_types25 = require("@harness-engineering/types");
9172
+ var import_types28 = require("@harness-engineering/types");
7995
9173
 
7996
9174
  // src/gateway/webhooks/signer.ts
7997
9175
  var import_node_crypto10 = require("crypto");
@@ -8021,8 +9199,8 @@ function genSecret2() {
8021
9199
  return (0, import_node_crypto11.randomBytes)(32).toString("base64url");
8022
9200
  }
8023
9201
  var WebhookStore = class {
8024
- constructor(path17) {
8025
- this.path = path17;
9202
+ constructor(path22) {
9203
+ this.path = path22;
8026
9204
  }
8027
9205
  path;
8028
9206
  cache = null;
@@ -8033,7 +9211,7 @@ var WebhookStore = class {
8033
9211
  const parsed = JSON.parse(raw);
8034
9212
  const list = Array.isArray(parsed) ? parsed : [];
8035
9213
  this.cache = list.map((entry) => {
8036
- const r = import_types25.WebhookSubscriptionSchema.safeParse(entry);
9214
+ const r = import_types28.WebhookSubscriptionSchema.safeParse(entry);
8037
9215
  return r.success ? r.data : null;
8038
9216
  }).filter((x) => x !== null);
8039
9217
  } catch (err) {
@@ -8413,7 +9591,12 @@ var WEBHOOK_TOPICS = [
8413
9591
  "maintenance:completed",
8414
9592
  "maintenance:error",
8415
9593
  "webhook.subscription.created",
8416
- "webhook.subscription.deleted"
9594
+ "webhook.subscription.deleted",
9595
+ // Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
9596
+ // `proposal.*` glob pattern to receive all three.
9597
+ "proposal.created",
9598
+ "proposal.approved",
9599
+ "proposal.rejected"
8417
9600
  ];
8418
9601
  function newEventId2() {
8419
9602
  return `evt_${(0, import_node_crypto13.randomBytes)(8).toString("hex")}`;
@@ -8447,7 +9630,7 @@ function wireWebhookFanout({ bus, store, delivery }) {
8447
9630
 
8448
9631
  // src/gateway/telemetry/fanout.ts
8449
9632
  var import_node_crypto14 = require("crypto");
8450
- var import_core9 = require("@harness-engineering/core");
9633
+ var import_core12 = require("@harness-engineering/core");
8451
9634
  var TOPICS = {
8452
9635
  MAINTENANCE_STARTED: "maintenance:started",
8453
9636
  MAINTENANCE_COMPLETED: "maintenance:completed",
@@ -8582,7 +9765,7 @@ function wireTelemetryFanout(params) {
8582
9765
  spanId,
8583
9766
  ...parentSpanId !== void 0 ? { parentSpanId } : {},
8584
9767
  name: SPAN_NAME[topic],
8585
- kind: import_core9.SpanKind.INTERNAL,
9768
+ kind: import_core12.SpanKind.INTERNAL,
8586
9769
  startTimeNs: startNs,
8587
9770
  endTimeNs: startNs,
8588
9771
  attributes: buildAttributes(payload, { "harness.topic": topic }),
@@ -8608,18 +9791,378 @@ function wireTelemetryFanout(params) {
8608
9791
  };
8609
9792
  }
8610
9793
 
8611
- // src/orchestrator.ts
8612
- var import_core13 = require("@harness-engineering/core");
8613
-
8614
- // src/logging/logger.ts
8615
- var StructuredLogger = class {
8616
- debug(message, context) {
8617
- this.log("debug", message, context);
9794
+ // src/notifications/slack-sink.ts
9795
+ var SEVERITY_PREFIX = {
9796
+ info: ":information_source:",
9797
+ success: ":white_check_mark:",
9798
+ warning: ":warning:",
9799
+ error: ":x:"
9800
+ };
9801
+ var SlackSink = class {
9802
+ kind = "slack";
9803
+ id;
9804
+ webhookUrl;
9805
+ fetchImpl;
9806
+ timeoutMs;
9807
+ constructor(opts) {
9808
+ this.id = opts.id;
9809
+ this.webhookUrl = opts.webhookUrl;
9810
+ this.fetchImpl = opts.fetchImpl ?? fetch;
9811
+ this.timeoutMs = opts.timeoutMs ?? 5e3;
8618
9812
  }
8619
- info(message, context) {
8620
- this.log("info", message, context);
9813
+ async deliver(input) {
9814
+ const body = input.wrapped ? this.renderEnvelope(input.payload) : this.renderRawEvent(input.payload);
9815
+ const ctrl = new AbortController();
9816
+ const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
9817
+ try {
9818
+ const res = await this.fetchImpl(this.webhookUrl, {
9819
+ method: "POST",
9820
+ headers: { "Content-Type": "application/json" },
9821
+ body: JSON.stringify(body),
9822
+ signal: ctrl.signal
9823
+ });
9824
+ if (res.ok) {
9825
+ return { ok: true, deliveredAt: Date.now() };
9826
+ }
9827
+ return { ok: false, error: `HTTP ${res.status}`, httpStatus: res.status };
9828
+ } catch (err) {
9829
+ const msg = err instanceof Error ? err.message : String(err);
9830
+ return { ok: false, error: ctrl.signal.aborted ? "timeout" : msg };
9831
+ } finally {
9832
+ clearTimeout(timer);
9833
+ }
8621
9834
  }
8622
- warn(message, context) {
9835
+ renderEnvelope(env) {
9836
+ const prefix = SEVERITY_PREFIX[env.severity] ?? "";
9837
+ const headline = `${prefix} ${env.title}`.trim();
9838
+ const blocks = [
9839
+ { type: "section", text: { type: "mrkdwn", text: `*${headline}*
9840
+ ${env.summary}` } }
9841
+ ];
9842
+ if (env.actions && env.actions.length > 0) {
9843
+ blocks.push({
9844
+ type: "actions",
9845
+ elements: env.actions.map((a) => ({
9846
+ type: "button",
9847
+ text: { type: "plain_text", text: a.label },
9848
+ url: a.url
9849
+ }))
9850
+ });
9851
+ }
9852
+ if (env.permalink) {
9853
+ blocks.push({
9854
+ type: "section",
9855
+ text: { type: "mrkdwn", text: `<${env.permalink}|View details>` }
9856
+ });
9857
+ }
9858
+ return { text: headline, blocks };
9859
+ }
9860
+ renderRawEvent(event) {
9861
+ const dump = (() => {
9862
+ try {
9863
+ return JSON.stringify(event.data, null, 2);
9864
+ } catch {
9865
+ return String(event.data);
9866
+ }
9867
+ })();
9868
+ const text = `harness event: \`${event.type}\``;
9869
+ return {
9870
+ text,
9871
+ blocks: [
9872
+ { type: "section", text: { type: "mrkdwn", text: `*${text}*
9873
+ \`\`\`
9874
+ ${dump}
9875
+ \`\`\`` } }
9876
+ ]
9877
+ };
9878
+ }
9879
+ };
9880
+
9881
+ // src/notifications/registry.ts
9882
+ var SinkConfigError = class extends Error {
9883
+ constructor(sinkId, message) {
9884
+ super(`[sink:${sinkId}] ${message}`);
9885
+ this.sinkId = sinkId;
9886
+ this.name = "SinkConfigError";
9887
+ }
9888
+ sinkId;
9889
+ };
9890
+ var SinkRegistry = class _SinkRegistry {
9891
+ entries;
9892
+ constructor(entries) {
9893
+ this.entries = entries;
9894
+ }
9895
+ static fromConfig(config, options) {
9896
+ const entries = [];
9897
+ for (const sinkConfig of config.sinks) {
9898
+ entries.push({
9899
+ config: sinkConfig,
9900
+ adapter: buildSink(sinkConfig, options)
9901
+ });
9902
+ }
9903
+ return new _SinkRegistry(entries);
9904
+ }
9905
+ list() {
9906
+ return this.entries;
9907
+ }
9908
+ get(id) {
9909
+ return this.entries.find((e) => e.config.id === id) ?? null;
9910
+ }
9911
+ ids() {
9912
+ return this.entries.map((e) => e.config.id);
9913
+ }
9914
+ async dispose() {
9915
+ for (const entry of this.entries) {
9916
+ if (entry.adapter.dispose) {
9917
+ await entry.adapter.dispose();
9918
+ }
9919
+ }
9920
+ }
9921
+ };
9922
+ function buildSink(config, options) {
9923
+ const kind = config.kind;
9924
+ switch (kind) {
9925
+ case "slack":
9926
+ return buildSlackSink(config, options);
9927
+ default: {
9928
+ const _exhaustive = kind;
9929
+ throw new SinkConfigError(config.id, `unknown sink kind '${String(_exhaustive)}'`);
9930
+ }
9931
+ }
9932
+ }
9933
+ function buildSlackSink(config, options) {
9934
+ const rawConfig = config.config;
9935
+ const envKey = typeof rawConfig.webhookUrlEnv === "string" ? rawConfig.webhookUrlEnv : null;
9936
+ const inlineUrl = typeof rawConfig.webhookUrl === "string" ? rawConfig.webhookUrl : null;
9937
+ let url;
9938
+ if (envKey) {
9939
+ const v = options.env[envKey];
9940
+ if (!v) {
9941
+ throw new SinkConfigError(
9942
+ config.id,
9943
+ `Slack webhook env var '${envKey}' is not set in the environment`
9944
+ );
9945
+ }
9946
+ url = v;
9947
+ } else if (inlineUrl) {
9948
+ url = inlineUrl;
9949
+ } else {
9950
+ throw new SinkConfigError(
9951
+ config.id,
9952
+ `Slack sink requires 'config.webhookUrlEnv' (preferred) or 'config.webhookUrl'`
9953
+ );
9954
+ }
9955
+ if (!/^https:\/\/hooks\.slack\.com\//.test(url)) {
9956
+ throw new SinkConfigError(
9957
+ config.id,
9958
+ `Slack webhook URL must be an https://hooks.slack.com/ URL`
9959
+ );
9960
+ }
9961
+ const sinkOpts = {
9962
+ id: config.id,
9963
+ webhookUrl: url
9964
+ };
9965
+ if (options.fetchImpl) sinkOpts.fetchImpl = options.fetchImpl;
9966
+ return new SlackSink(sinkOpts);
9967
+ }
9968
+
9969
+ // src/notifications/events.ts
9970
+ var import_node_crypto15 = require("crypto");
9971
+
9972
+ // src/notifications/envelope.ts
9973
+ function asObj(data) {
9974
+ return typeof data === "object" && data !== null ? data : {};
9975
+ }
9976
+ var ENVELOPE_DERIVERS = {
9977
+ "maintenance.started": (event) => {
9978
+ const data = asObj(event.data);
9979
+ return {
9980
+ title: `Maintenance started: ${data.taskId ?? "(unknown task)"}`,
9981
+ summary: `Task \`${data.taskId ?? "(unknown)"}\` is running.`,
9982
+ severity: "info"
9983
+ };
9984
+ },
9985
+ "maintenance.completed": (event) => {
9986
+ const data = asObj(event.data);
9987
+ return {
9988
+ title: `Maintenance done: ${data.taskId ?? "(unknown task)"}`,
9989
+ summary: `Task \`${data.taskId ?? "(unknown)"}\` completed successfully.`,
9990
+ severity: "success"
9991
+ };
9992
+ },
9993
+ "maintenance.error": (event) => {
9994
+ const data = asObj(event.data);
9995
+ return {
9996
+ title: `Maintenance failed: ${data.taskId ?? "(unknown task)"}`,
9997
+ summary: data.error ?? "No error message provided.",
9998
+ severity: "error"
9999
+ };
10000
+ },
10001
+ "interaction.created": (event) => {
10002
+ const data = asObj(event.data);
10003
+ return {
10004
+ title: `Action required: ${truncate(data.question ?? "pending interaction", 80)}`,
10005
+ summary: data.question ?? "(no question text)",
10006
+ severity: "warning"
10007
+ };
10008
+ },
10009
+ "interaction.resolved": (event) => {
10010
+ const data = asObj(event.data);
10011
+ return {
10012
+ title: `Interaction resolved`,
10013
+ summary: data.resolution ?? "(no resolution text)",
10014
+ severity: "info"
10015
+ };
10016
+ },
10017
+ "notification.test": (event) => {
10018
+ const data = asObj(event.data);
10019
+ return {
10020
+ title: "Test notification from harness",
10021
+ summary: data.message ?? "If you see this, your notification sink is working.",
10022
+ severity: "info"
10023
+ };
10024
+ },
10025
+ // Hermes Phase 4 — skill proposal lifecycle events.
10026
+ "proposal.created": (event) => {
10027
+ const data = asObj(event.data);
10028
+ const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
10029
+ return {
10030
+ title: `New skill proposal: ${label}`,
10031
+ summary: truncate(data.justification ?? "No justification provided.", 240),
10032
+ severity: "info"
10033
+ };
10034
+ },
10035
+ "proposal.approved": (event) => {
10036
+ const data = asObj(event.data);
10037
+ const label = data.name ?? data.targetSkill ?? "(unknown skill)";
10038
+ return {
10039
+ title: `Skill proposal approved: ${label}`,
10040
+ summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
10041
+ severity: "success"
10042
+ };
10043
+ },
10044
+ "proposal.rejected": (event) => {
10045
+ const data = asObj(event.data);
10046
+ return {
10047
+ title: "Skill proposal rejected",
10048
+ summary: truncate(data.reason ?? "No reason provided.", 240),
10049
+ severity: "warning"
10050
+ };
10051
+ }
10052
+ };
10053
+ function truncate(s, max) {
10054
+ return s.length <= max ? s : `${s.slice(0, max - 1)}\u2026`;
10055
+ }
10056
+ function fallbackTitle(event) {
10057
+ return event.type;
10058
+ }
10059
+ function fallbackSummary(event) {
10060
+ try {
10061
+ return "```\n" + JSON.stringify(event.data, null, 2) + "\n```";
10062
+ } catch {
10063
+ return String(event.data);
10064
+ }
10065
+ }
10066
+ function severityFromType(type) {
10067
+ if (type.endsWith(".error") || type.endsWith(".failed")) return "error";
10068
+ if (type.endsWith(".completed") || type.endsWith(".resolved")) return "success";
10069
+ if (type.endsWith(".created") || type.startsWith("interaction.")) return "warning";
10070
+ return "info";
10071
+ }
10072
+ function backfillEnvelope(event, partial) {
10073
+ return {
10074
+ title: truncate(partial.title ?? fallbackTitle(event), 280),
10075
+ summary: partial.summary ?? fallbackSummary(event),
10076
+ severity: partial.severity ?? severityFromType(event.type)
10077
+ };
10078
+ }
10079
+ function wrapAsEnvelope(event) {
10080
+ const deriver = ENVELOPE_DERIVERS[event.type];
10081
+ const partial = deriver ? deriver(event) : {};
10082
+ const envelope = backfillEnvelope(event, partial);
10083
+ if (partial.actions) envelope.actions = partial.actions;
10084
+ if (partial.permalink) envelope.permalink = partial.permalink;
10085
+ if (event.correlationId) envelope.correlationId = event.correlationId;
10086
+ return envelope;
10087
+ }
10088
+
10089
+ // src/notifications/events.ts
10090
+ var NOTIFICATION_TOPICS = [
10091
+ "interaction.created",
10092
+ "interaction.resolved",
10093
+ "maintenance:started",
10094
+ "maintenance:completed",
10095
+ "maintenance:error",
10096
+ // Hermes Phase 4 — skill proposal lifecycle.
10097
+ "proposal.created",
10098
+ "proposal.approved",
10099
+ "proposal.rejected"
10100
+ ];
10101
+ function newEventId4() {
10102
+ return `evt_${(0, import_node_crypto15.randomBytes)(8).toString("hex")}`;
10103
+ }
10104
+ function dispatchToEntry(bus, entry, event) {
10105
+ const eventType = event.type;
10106
+ const matches = entry.config.events.some((p) => eventMatches(p, eventType));
10107
+ if (!matches) return;
10108
+ const payload = entry.config.wrap_response ? wrapAsEnvelope(event) : event;
10109
+ const summaryBase = {
10110
+ sinkId: entry.adapter.id,
10111
+ kind: entry.adapter.kind,
10112
+ eventType,
10113
+ eventId: event.id
10114
+ };
10115
+ void entry.adapter.deliver({ payload, wrapped: entry.config.wrap_response }).then((result) => {
10116
+ bus.emit("notification.delivery.attempted", { ...summaryBase, ok: result.ok });
10117
+ if (!result.ok) {
10118
+ bus.emit("notification.delivery.failed", {
10119
+ ...summaryBase,
10120
+ ok: false,
10121
+ error: result.error
10122
+ });
10123
+ }
10124
+ }).catch((err) => {
10125
+ const msg = err instanceof Error ? err.message : String(err);
10126
+ bus.emit("notification.delivery.failed", { ...summaryBase, ok: false, error: msg });
10127
+ });
10128
+ }
10129
+ function wireNotificationSinks({ bus, registry }) {
10130
+ const handlers = [];
10131
+ for (const topic of NOTIFICATION_TOPICS) {
10132
+ const eventType = topic.replace(":", ".");
10133
+ const fn = (data) => {
10134
+ const entries = registry.list();
10135
+ if (entries.length === 0) return;
10136
+ const event = {
10137
+ id: newEventId4(),
10138
+ type: eventType,
10139
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
10140
+ data
10141
+ };
10142
+ for (const entry of entries) {
10143
+ dispatchToEntry(bus, entry, event);
10144
+ }
10145
+ };
10146
+ bus.on(topic, fn);
10147
+ handlers.push({ topic, fn });
10148
+ }
10149
+ return () => {
10150
+ for (const { topic, fn } of handlers) bus.removeListener(topic, fn);
10151
+ };
10152
+ }
10153
+
10154
+ // src/orchestrator.ts
10155
+ var import_core16 = require("@harness-engineering/core");
10156
+
10157
+ // src/logging/logger.ts
10158
+ var StructuredLogger = class {
10159
+ debug(message, context) {
10160
+ this.log("debug", message, context);
10161
+ }
10162
+ info(message, context) {
10163
+ this.log("info", message, context);
10164
+ }
10165
+ warn(message, context) {
8623
10166
  this.log("warn", message, context);
8624
10167
  }
8625
10168
  error(message, context) {
@@ -8651,7 +10194,7 @@ var StructuredLogger = class {
8651
10194
  // src/workspace/config-scanner.ts
8652
10195
  var import_node_fs = require("fs");
8653
10196
  var import_node_path4 = require("path");
8654
- var import_core10 = require("@harness-engineering/core");
10197
+ var import_core13 = require("@harness-engineering/core");
8655
10198
  var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".gemini/settings.json", "skill.yaml"];
8656
10199
  var BLOCKING_INJECTION_PREFIXES = ["INJ-UNI-", "INJ-REROL-"];
8657
10200
  var DOWNGRADED_SECURITY_RULES = /* @__PURE__ */ new Set(["SEC-AGT-006"]);
@@ -8673,25 +10216,25 @@ async function scanSingleFile(filePath, targetDir, scanner) {
8673
10216
  } catch {
8674
10217
  return null;
8675
10218
  }
8676
- const injectionFindings = (0, import_core10.scanForInjection)(content);
8677
- const findings = (0, import_core10.mapInjectionFindings)(injectionFindings);
10219
+ const injectionFindings = (0, import_core13.scanForInjection)(content);
10220
+ const findings = (0, import_core13.mapInjectionFindings)(injectionFindings);
8678
10221
  const secFindings = await scanner.scanFile(filePath);
8679
- findings.push(...(0, import_core10.mapSecurityFindings)(secFindings, findings));
10222
+ findings.push(...(0, import_core13.mapSecurityFindings)(secFindings, findings));
8680
10223
  const adjusted = adjustFindingSeverity(findings);
8681
10224
  return {
8682
10225
  file: (0, import_node_path4.relative)(targetDir, filePath).replaceAll("\\", "/"),
8683
10226
  findings: adjusted,
8684
- overallSeverity: (0, import_core10.computeOverallSeverity)(adjusted)
10227
+ overallSeverity: (0, import_core13.computeOverallSeverity)(adjusted)
8685
10228
  };
8686
10229
  }
8687
10230
  async function scanWorkspaceConfig(workspacePath) {
8688
- const scanner = new import_core10.SecurityScanner((0, import_core10.parseSecurityConfig)({}));
10231
+ const scanner = new import_core13.SecurityScanner((0, import_core13.parseSecurityConfig)({}));
8689
10232
  const results = [];
8690
10233
  for (const configFile of CONFIG_FILES) {
8691
10234
  const result = await scanSingleFile((0, import_node_path4.join)(workspacePath, configFile), workspacePath, scanner);
8692
10235
  if (result) results.push(result);
8693
10236
  }
8694
- return { exitCode: (0, import_core10.computeScanExitCode)(results), results };
10237
+ return { exitCode: (0, import_core13.computeScanExitCode)(results), results };
8695
10238
  }
8696
10239
 
8697
10240
  // src/maintenance/task-registry.ts
@@ -8874,6 +10417,19 @@ var BUILT_IN_TASKS = [
8874
10417
  schedule: "*/15 * * * *",
8875
10418
  branch: null,
8876
10419
  checkCommand: ["harness", "sync-main", "--json"]
10420
+ },
10421
+ // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
10422
+ // on every existing catalog skill. Schedule is Feb 31 (a date that never
10423
+ // exists) so the cron loop never fires it automatically; operators trigger
10424
+ // it once via the dashboard "Run now" button or `harness backfill-skill-
10425
+ // provenance` after upgrading to Phase 4.
10426
+ {
10427
+ id: "proposal-provenance-backfill",
10428
+ type: "housekeeping",
10429
+ description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
10430
+ schedule: "0 0 31 2 *",
10431
+ branch: null,
10432
+ checkCommand: ["backfill-skill-provenance"]
8877
10433
  }
8878
10434
  ];
8879
10435
 
@@ -8966,24 +10522,49 @@ var MaintenanceScheduler = class {
8966
10522
  this.resolvedTasks = this.resolveTasks();
8967
10523
  }
8968
10524
  /**
8969
- * Merge built-in task definitions with config overrides.
8970
- * Tasks with `enabled: false` are filtered out.
8971
- * Schedule overrides replace the default cron expression.
10525
+ * Merge built-in task definitions with config overrides, then append
10526
+ * Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
10527
+ * overrides). Tasks with `enabled: false` are filtered out. Schedule
10528
+ * overrides replace the default cron expression.
8972
10529
  */
8973
10530
  resolveTasks() {
8974
10531
  const overrides = this.config.tasks ?? {};
8975
- return BUILT_IN_TASKS.filter((task) => {
10532
+ const customs = this.config.customTasks ?? {};
10533
+ const merged = [];
10534
+ for (const task of BUILT_IN_TASKS) {
8976
10535
  const override = overrides[task.id];
8977
- if (override?.enabled === false) return false;
8978
- return true;
8979
- }).map((task) => {
8980
- const override = overrides[task.id];
8981
- if (!override) return { ...task };
8982
- return {
10536
+ if (override?.enabled === false) continue;
10537
+ merged.push({
8983
10538
  ...task,
8984
- ...override.schedule !== void 0 && { schedule: override.schedule }
8985
- };
8986
- });
10539
+ ...override?.schedule !== void 0 && { schedule: override.schedule }
10540
+ });
10541
+ }
10542
+ for (const [id, def] of Object.entries(customs)) {
10543
+ const override = overrides[id];
10544
+ if (override?.enabled === false) continue;
10545
+ merged.push({
10546
+ id,
10547
+ type: def.type,
10548
+ description: def.description,
10549
+ schedule: override?.schedule ?? def.schedule,
10550
+ branch: def.branch,
10551
+ ...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
10552
+ ...def.checkScript !== void 0 && { checkScript: def.checkScript },
10553
+ ...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
10554
+ ...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
10555
+ ...def.inlineSkillsBudgetTokens !== void 0 && {
10556
+ inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
10557
+ },
10558
+ ...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
10559
+ ...def.contextFromMaxAgeMinutes !== void 0 && {
10560
+ contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
10561
+ },
10562
+ ...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
10563
+ ...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
10564
+ isCustom: true
10565
+ });
10566
+ }
10567
+ return merged;
8987
10568
  }
8988
10569
  /** Returns the resolved (merged) task list. Useful for testing and dashboard. */
8989
10570
  getResolvedTasks() {
@@ -9156,27 +10737,27 @@ var MaintenanceScheduler = class {
9156
10737
  };
9157
10738
 
9158
10739
  // src/maintenance/leader-elector.ts
9159
- var import_types26 = require("@harness-engineering/types");
10740
+ var import_types29 = require("@harness-engineering/types");
9160
10741
  var SingleProcessLeaderElector = class {
9161
10742
  async electLeader() {
9162
- return (0, import_types26.Ok)("claimed");
10743
+ return (0, import_types29.Ok)("claimed");
9163
10744
  }
9164
10745
  };
9165
10746
 
9166
10747
  // src/maintenance/reporter.ts
9167
- var fs14 = __toESM(require("fs"));
9168
- var path15 = __toESM(require("path"));
9169
- var import_zod15 = require("zod");
9170
- var RunResultSchema = import_zod15.z.object({
9171
- taskId: import_zod15.z.string(),
9172
- startedAt: import_zod15.z.string(),
9173
- completedAt: import_zod15.z.string(),
9174
- status: import_zod15.z.enum(["success", "failure", "skipped", "no-issues"]),
9175
- findings: import_zod15.z.number(),
9176
- fixed: import_zod15.z.number(),
9177
- prUrl: import_zod15.z.string().nullable(),
9178
- prUpdated: import_zod15.z.boolean(),
9179
- error: import_zod15.z.string().optional()
10748
+ var fs15 = __toESM(require("fs"));
10749
+ var path16 = __toESM(require("path"));
10750
+ var import_zod16 = require("zod");
10751
+ var RunResultSchema = import_zod16.z.object({
10752
+ taskId: import_zod16.z.string(),
10753
+ startedAt: import_zod16.z.string(),
10754
+ completedAt: import_zod16.z.string(),
10755
+ status: import_zod16.z.enum(["success", "failure", "skipped", "no-issues"]),
10756
+ findings: import_zod16.z.number(),
10757
+ fixed: import_zod16.z.number(),
10758
+ prUrl: import_zod16.z.string().nullable(),
10759
+ prUpdated: import_zod16.z.boolean(),
10760
+ error: import_zod16.z.string().optional()
9180
10761
  });
9181
10762
  var MAX_HISTORY = 500;
9182
10763
  var fallbackLogger = {
@@ -9200,10 +10781,10 @@ var MaintenanceReporter = class {
9200
10781
  */
9201
10782
  async load() {
9202
10783
  try {
9203
- await fs14.promises.mkdir(this.persistDir, { recursive: true });
9204
- const filePath = path15.join(this.persistDir, "history.json");
9205
- const data = await fs14.promises.readFile(filePath, "utf-8");
9206
- const parsed = import_zod15.z.array(RunResultSchema).safeParse(JSON.parse(data));
10784
+ await fs15.promises.mkdir(this.persistDir, { recursive: true });
10785
+ const filePath = path16.join(this.persistDir, "history.json");
10786
+ const data = await fs15.promises.readFile(filePath, "utf-8");
10787
+ const parsed = import_zod16.z.array(RunResultSchema).safeParse(JSON.parse(data));
9207
10788
  if (parsed.success) {
9208
10789
  this.history = parsed.data.slice(0, MAX_HISTORY);
9209
10790
  }
@@ -9236,9 +10817,9 @@ var MaintenanceReporter = class {
9236
10817
  */
9237
10818
  async persist() {
9238
10819
  try {
9239
- await fs14.promises.mkdir(this.persistDir, { recursive: true });
9240
- const filePath = path15.join(this.persistDir, "history.json");
9241
- await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10820
+ await fs15.promises.mkdir(this.persistDir, { recursive: true });
10821
+ const filePath = path16.join(this.persistDir, "history.json");
10822
+ await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
9242
10823
  } catch (err) {
9243
10824
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
9244
10825
  }
@@ -9254,6 +10835,9 @@ var TaskRunner = class {
9254
10835
  cwd;
9255
10836
  prManager;
9256
10837
  baseBranch;
10838
+ checkScriptRunner;
10839
+ contextResolver;
10840
+ outputStore;
9257
10841
  constructor(options) {
9258
10842
  this.config = options.config;
9259
10843
  this.checkRunner = options.checkRunner;
@@ -9262,27 +10846,49 @@ var TaskRunner = class {
9262
10846
  this.cwd = options.cwd;
9263
10847
  this.prManager = options.prManager ?? null;
9264
10848
  this.baseBranch = options.baseBranch ?? "main";
10849
+ this.checkScriptRunner = options.checkScriptRunner ?? null;
10850
+ this.contextResolver = options.contextResolver ?? null;
10851
+ this.outputStore = options.outputStore ?? null;
9265
10852
  }
9266
10853
  /**
9267
10854
  * Run a maintenance task and return the result.
9268
10855
  * Dispatches to the appropriate execution path based on task type.
9269
10856
  * Never throws -- errors are captured in the RunResult.
10857
+ *
10858
+ * @param task - Resolved task definition.
10859
+ * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
10860
+ * when called from the scheduler path.
9270
10861
  */
9271
- async run(task) {
10862
+ async run(task, origin = "cron") {
9272
10863
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10864
+ let result;
10865
+ let captured;
9273
10866
  try {
9274
10867
  switch (task.type) {
9275
- case "mechanical-ai":
9276
- return await this.runMechanicalAI(task, startedAt);
10868
+ case "mechanical-ai": {
10869
+ const out = await this.runMechanicalAI(task, startedAt);
10870
+ result = out.result;
10871
+ captured = out.captured;
10872
+ break;
10873
+ }
9277
10874
  case "pure-ai":
9278
- return await this.runPureAI(task, startedAt);
9279
- case "report-only":
9280
- return await this.runReportOnly(task, startedAt);
9281
- case "housekeeping":
9282
- return await this.runHousekeeping(task, startedAt);
10875
+ result = await this.runPureAI(task, startedAt);
10876
+ break;
10877
+ case "report-only": {
10878
+ const out = await this.runReportOnly(task, startedAt);
10879
+ result = out.result;
10880
+ captured = out.captured;
10881
+ break;
10882
+ }
10883
+ case "housekeeping": {
10884
+ const out = await this.runHousekeeping(task, startedAt);
10885
+ result = out.result;
10886
+ captured = out.captured;
10887
+ break;
10888
+ }
9283
10889
  default: {
9284
10890
  const _exhaustive = task.type;
9285
- return this.failureResult(
10891
+ result = this.failureResult(
9286
10892
  task.id,
9287
10893
  startedAt,
9288
10894
  `Unknown task type: ${String(_exhaustive)}`
@@ -9290,69 +10896,174 @@ var TaskRunner = class {
9290
10896
  }
9291
10897
  }
9292
10898
  } catch (err) {
9293
- return this.failureResult(task.id, startedAt, String(err));
10899
+ result = this.failureResult(task.id, startedAt, String(err));
10900
+ }
10901
+ result.origin = origin;
10902
+ await this.persistOutput(task, result, captured, origin);
10903
+ return result;
10904
+ }
10905
+ async persistOutput(task, result, captured, origin) {
10906
+ if (!this.outputStore) return;
10907
+ const entry = {
10908
+ taskId: result.taskId,
10909
+ startedAt: result.startedAt,
10910
+ completedAt: result.completedAt,
10911
+ status: result.status,
10912
+ findings: result.findings,
10913
+ fixed: result.fixed,
10914
+ prUrl: result.prUrl,
10915
+ prUpdated: result.prUpdated,
10916
+ origin,
10917
+ ...result.error !== void 0 && { error: result.error },
10918
+ ...result.costUsd !== void 0 && { costUsd: result.costUsd },
10919
+ ...captured?.stdout !== void 0 && { stdout: captured.stdout },
10920
+ ...captured?.stderr !== void 0 && { stderr: captured.stderr },
10921
+ ...captured?.structured !== void 0 && { structured: captured.structured },
10922
+ ...captured?.context !== void 0 && { context: captured.context }
10923
+ };
10924
+ try {
10925
+ await this.outputStore.write(task.id, entry, task.outputRetention);
10926
+ } catch {
9294
10927
  }
9295
10928
  }
9296
10929
  /**
9297
- * Mechanical-AI: run check command, dispatch AI agent only if fixable findings exist.
10930
+ * Run the check step using whichever runner the task asks for. Custom
10931
+ * tasks that declare `checkScript` go through the Hermes Phase 2
10932
+ * `CheckScriptRunner`; built-ins (and customs that use the legacy
10933
+ * `checkCommand` shape) go through the original heuristic runner.
9298
10934
  */
9299
- async runMechanicalAI(task, startedAt) {
10935
+ async runCheckStep(task) {
10936
+ if (task.checkScript) {
10937
+ if (!this.checkScriptRunner) {
10938
+ throw new Error(
10939
+ `task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
10940
+ );
10941
+ }
10942
+ const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
10943
+ return {
10944
+ passed: r2.passed,
10945
+ findings: r2.findings,
10946
+ stdout: r2.output,
10947
+ stderr: r2.stderr,
10948
+ structured: r2.structured ? r2.structured : null
10949
+ };
10950
+ }
9300
10951
  if (!task.checkCommand || task.checkCommand.length === 0) {
9301
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing checkCommand");
10952
+ throw new Error(`task '${task.id}' is missing checkCommand`);
9302
10953
  }
10954
+ const r = await this.checkRunner.run(task.checkCommand, this.cwd);
10955
+ return {
10956
+ passed: r.passed,
10957
+ findings: r.findings,
10958
+ stdout: r.output,
10959
+ stderr: "",
10960
+ structured: null
10961
+ };
10962
+ }
10963
+ /**
10964
+ * Hermes Phase 2 — Compose the agent prompt-context block from inlined
10965
+ * skills + upstream task outputs. Returns an empty string when nothing
10966
+ * is configured (or when the resolver is absent), which is the safe
10967
+ * no-op default.
10968
+ */
10969
+ async composePromptContext(task) {
10970
+ if (!this.contextResolver) return "";
10971
+ const skills = await this.contextResolver.resolveInlineSkills(
10972
+ task.inlineSkills,
10973
+ task.inlineSkillsBudgetTokens ?? 8e3
10974
+ );
10975
+ const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
10976
+ maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
10977
+ });
10978
+ return [skills, upstream].filter(Boolean).join("\n");
10979
+ }
10980
+ /**
10981
+ * Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
10982
+ * only if fixable findings exist; persist captured stdout/stderr/context
10983
+ * via the output store on the way out.
10984
+ */
10985
+ async runMechanicalAI(task, startedAt) {
9303
10986
  if (!task.fixSkill) {
9304
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
10987
+ return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
9305
10988
  }
9306
10989
  if (!task.branch) {
9307
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
10990
+ return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
10991
+ }
10992
+ if (!task.checkCommand && !task.checkScript) {
10993
+ return wrap(
10994
+ this.failureResult(
10995
+ task.id,
10996
+ startedAt,
10997
+ "mechanical-ai task missing checkCommand or checkScript"
10998
+ )
10999
+ );
9308
11000
  }
9309
- const checkResult = await this.checkRunner.run(task.checkCommand, this.cwd);
9310
- if (checkResult.findings === 0) {
11001
+ let check;
11002
+ try {
11003
+ check = await this.runCheckStep(task);
11004
+ } catch (err) {
11005
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11006
+ }
11007
+ const promptContext = await this.composePromptContext(task);
11008
+ const baseCaptured = {
11009
+ stdout: check.stdout,
11010
+ stderr: check.stderr,
11011
+ structured: check.structured,
11012
+ ...promptContext ? { context: promptContext } : {}
11013
+ };
11014
+ const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
11015
+ if (check.findings === 0 || wakeAgentExplicitlyFalse) {
9311
11016
  return {
9312
- taskId: task.id,
9313
- startedAt,
9314
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9315
- status: "no-issues",
9316
- findings: 0,
9317
- fixed: 0,
9318
- prUrl: null,
9319
- prUpdated: false
11017
+ result: {
11018
+ taskId: task.id,
11019
+ startedAt,
11020
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11021
+ status: "no-issues",
11022
+ findings: check.findings,
11023
+ fixed: 0,
11024
+ prUrl: null,
11025
+ prUpdated: false
11026
+ },
11027
+ captured: baseCaptured
9320
11028
  };
9321
11029
  }
9322
11030
  if (this.prManager) {
9323
11031
  try {
9324
11032
  await this.prManager.ensureBranch(task.branch, this.baseBranch);
9325
11033
  } catch (err) {
9326
- return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
11034
+ return wrap(
11035
+ this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
11036
+ baseCaptured
11037
+ );
9327
11038
  }
9328
11039
  }
9329
11040
  const backendName = this.resolveBackend(task.id);
9330
11041
  let agentResult;
9331
11042
  try {
9332
- agentResult = await this.agentDispatcher.dispatch(
9333
- task.fixSkill,
9334
- task.branch,
9335
- backendName,
9336
- this.cwd
9337
- );
11043
+ agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
11044
+ promptContext
11045
+ }) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
9338
11046
  } catch (err) {
9339
11047
  return {
9340
- taskId: task.id,
9341
- startedAt,
9342
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9343
- status: "failure",
9344
- findings: checkResult.findings,
9345
- fixed: 0,
9346
- prUrl: null,
9347
- prUpdated: false,
9348
- error: `Agent dispatch failed: ${String(err)}`
11048
+ result: {
11049
+ taskId: task.id,
11050
+ startedAt,
11051
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11052
+ status: "failure",
11053
+ findings: check.findings,
11054
+ fixed: 0,
11055
+ prUrl: null,
11056
+ prUpdated: false,
11057
+ error: `Agent dispatch failed: ${String(err)}`
11058
+ },
11059
+ captured: baseCaptured
9349
11060
  };
9350
11061
  }
9351
11062
  let prUrl = null;
9352
11063
  let prUpdated = false;
9353
11064
  if (this.prManager && agentResult.producedCommits) {
9354
11065
  try {
9355
- const summary = `Findings: ${checkResult.findings}, Fixed: ${agentResult.fixed}`;
11066
+ const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
9356
11067
  const prResult = await this.prManager.ensurePR(task, summary);
9357
11068
  prUrl = prResult.prUrl;
9358
11069
  prUpdated = prResult.prUpdated;
@@ -9361,14 +11072,17 @@ var TaskRunner = class {
9361
11072
  }
9362
11073
  }
9363
11074
  return {
9364
- taskId: task.id,
9365
- startedAt,
9366
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9367
- status: "success",
9368
- findings: checkResult.findings,
9369
- fixed: agentResult.fixed,
9370
- prUrl,
9371
- prUpdated
11075
+ result: {
11076
+ taskId: task.id,
11077
+ startedAt,
11078
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11079
+ status: "success",
11080
+ findings: check.findings,
11081
+ fixed: agentResult.fixed,
11082
+ prUrl,
11083
+ prUpdated
11084
+ },
11085
+ captured: baseCaptured
9372
11086
  };
9373
11087
  }
9374
11088
  /**
@@ -9388,15 +11102,13 @@ var TaskRunner = class {
9388
11102
  return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
9389
11103
  }
9390
11104
  }
11105
+ const promptContext = await this.composePromptContext(task);
9391
11106
  const backendName = this.resolveBackend(task.id);
9392
11107
  let agentResult;
9393
11108
  try {
9394
- agentResult = await this.agentDispatcher.dispatch(
9395
- task.fixSkill,
9396
- task.branch,
9397
- backendName,
9398
- this.cwd
9399
- );
11109
+ agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
11110
+ promptContext
11111
+ }) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
9400
11112
  } catch (err) {
9401
11113
  return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
9402
11114
  }
@@ -9424,7 +11136,7 @@ var TaskRunner = class {
9424
11136
  };
9425
11137
  }
9426
11138
  /**
9427
- * Report-only: run check command, record metrics, no AI dispatch.
11139
+ * Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
9428
11140
  *
9429
11141
  * Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
9430
11142
  * and `harness compound scan-candidates` in `--non-interactive` mode):
@@ -9434,122 +11146,715 @@ var TaskRunner = class {
9434
11146
  * Legacy report-only tasks emit free-form output and fall through to 'success'.
9435
11147
  */
9436
11148
  async runReportOnly(task, startedAt) {
9437
- if (!task.checkCommand || task.checkCommand.length === 0) {
9438
- return this.failureResult(task.id, startedAt, "report-only task missing checkCommand");
11149
+ if (!task.checkCommand && !task.checkScript) {
11150
+ return wrap(
11151
+ this.failureResult(
11152
+ task.id,
11153
+ startedAt,
11154
+ "report-only task missing checkCommand or checkScript"
11155
+ )
11156
+ );
11157
+ }
11158
+ let check;
11159
+ try {
11160
+ check = await this.runCheckStep(task);
11161
+ } catch (err) {
11162
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11163
+ }
11164
+ const parsed = parseStatusLine(check.stdout);
11165
+ const status = parsed?.status ?? "success";
11166
+ const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11167
+ const result = {
11168
+ taskId: task.id,
11169
+ startedAt,
11170
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11171
+ status,
11172
+ findings,
11173
+ fixed: 0,
11174
+ prUrl: null,
11175
+ prUpdated: false
11176
+ };
11177
+ if (parsed?.error) {
11178
+ result.error = parsed.error;
11179
+ }
11180
+ return {
11181
+ result,
11182
+ captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
11183
+ };
11184
+ }
11185
+ /**
11186
+ * Housekeeping: run command directly, no AI, no PR.
11187
+ *
11188
+ * Captures stdout and parses a trailing JSON status line if present.
11189
+ * Recognized contracts:
11190
+ * - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
11191
+ * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
11192
+ * Legacy housekeeping commands that emit no JSON keep the prior behavior:
11193
+ * status: 'success', findings: 0.
11194
+ *
11195
+ * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11196
+ * tasks; the runner falls through to the same JSON-status parsing path.
11197
+ */
11198
+ async runHousekeeping(task, startedAt) {
11199
+ if (!task.checkCommand && !task.checkScript) {
11200
+ return wrap(
11201
+ this.failureResult(
11202
+ task.id,
11203
+ startedAt,
11204
+ "housekeeping task missing checkCommand or checkScript"
11205
+ )
11206
+ );
11207
+ }
11208
+ let stdout;
11209
+ let stderr = "";
11210
+ let structured = null;
11211
+ if (task.checkScript) {
11212
+ try {
11213
+ const r = await this.runCheckStep(task);
11214
+ stdout = r.stdout;
11215
+ stderr = r.stderr;
11216
+ structured = r.structured;
11217
+ } catch (err) {
11218
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11219
+ }
11220
+ } else {
11221
+ try {
11222
+ const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
11223
+ stdout = out.stdout ?? "";
11224
+ } catch (err) {
11225
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11226
+ }
11227
+ }
11228
+ const parsed = parseStatusLine(stdout);
11229
+ const status = parsed?.status ?? "success";
11230
+ const result = {
11231
+ taskId: task.id,
11232
+ startedAt,
11233
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11234
+ status,
11235
+ findings: 0,
11236
+ fixed: 0,
11237
+ prUrl: null,
11238
+ prUpdated: false
11239
+ };
11240
+ if (parsed?.error) result.error = parsed.error;
11241
+ return { result, captured: { stdout, stderr, structured } };
11242
+ }
11243
+ /**
11244
+ * Resolve which AI backend name to use for a given task.
11245
+ * Priority: per-task override > global config > 'local' default.
11246
+ */
11247
+ resolveBackend(taskId) {
11248
+ const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
11249
+ if (taskOverride) return taskOverride;
11250
+ return this.config.aiBackend ?? "local";
11251
+ }
11252
+ failureResult(taskId, startedAt, error) {
11253
+ return {
11254
+ taskId,
11255
+ startedAt,
11256
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11257
+ status: "failure",
11258
+ findings: 0,
11259
+ fixed: 0,
11260
+ prUrl: null,
11261
+ prUpdated: false,
11262
+ error
11263
+ };
11264
+ }
11265
+ };
11266
+ function wrap(result, captured) {
11267
+ return captured ? { result, captured } : { result };
11268
+ }
11269
+ function parseStatusLine(output) {
11270
+ const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
11271
+ for (let i = lines.length - 1; i >= 0; i--) {
11272
+ const line = lines[i];
11273
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
11274
+ try {
11275
+ const obj = JSON.parse(line);
11276
+ const s = obj.status;
11277
+ if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
11278
+ const parsed = { status: s, rawStatus: s };
11279
+ if (typeof obj.candidatesFound === "number") {
11280
+ parsed.candidatesFound = obj.candidatesFound;
11281
+ }
11282
+ if (typeof obj.error === "string") {
11283
+ parsed.error = obj.error;
11284
+ }
11285
+ if (typeof obj.reason === "string") {
11286
+ parsed.reason = obj.reason;
11287
+ }
11288
+ if (typeof obj.detail === "string" && !parsed.error) {
11289
+ parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
11290
+ }
11291
+ return parsed;
11292
+ }
11293
+ if (s === "updated" || s === "no-op") {
11294
+ return { status: "success", rawStatus: s };
11295
+ }
11296
+ if (s === "error") {
11297
+ const message = typeof obj.message === "string" ? obj.message : "unknown error";
11298
+ return { status: "failure", error: message, rawStatus: "error" };
11299
+ }
11300
+ } catch {
11301
+ }
11302
+ }
11303
+ return null;
11304
+ }
11305
+
11306
+ // src/maintenance/check-script-runner.ts
11307
+ var import_node_child_process11 = require("child_process");
11308
+ var import_node_util3 = require("util");
11309
+ var path17 = __toESM(require("path"));
11310
+ var execFileAsync = (0, import_node_util3.promisify)(import_node_child_process11.execFile);
11311
+ var CheckScriptRunner = class {
11312
+ constructor(cwd) {
11313
+ this.cwd = cwd;
11314
+ }
11315
+ cwd;
11316
+ async run(spec, cwd) {
11317
+ const projectRoot = cwd ?? this.cwd;
11318
+ const captured = await captureScript(spec, projectRoot);
11319
+ const parseJson = spec.parseStdoutJson !== false;
11320
+ const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
11321
+ if (structured) {
11322
+ return mapStructured(structured, captured.stdout, captured.stderr);
11323
+ }
11324
+ return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
11325
+ }
11326
+ };
11327
+ async function captureScript(spec, projectRoot) {
11328
+ const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11329
+ const args = spec.args ?? [];
11330
+ const timeoutMs = spec.timeoutMs ?? 12e4;
11331
+ try {
11332
+ const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
11333
+ return {
11334
+ stdout: String(result.stdout ?? ""),
11335
+ stderr: String(result.stderr ?? ""),
11336
+ exitedAbnormally: false
11337
+ };
11338
+ } catch (err) {
11339
+ const e = err;
11340
+ return {
11341
+ stdout: String(e.stdout ?? ""),
11342
+ stderr: String(e.stderr ?? ""),
11343
+ exitedAbnormally: true
11344
+ };
11345
+ }
11346
+ }
11347
+ function parseStatusEnvelope(stdout) {
11348
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
11349
+ for (let i = lines.length - 1; i >= 0; i--) {
11350
+ const env = classifyLine2(lines[i]);
11351
+ if (env) return env;
11352
+ }
11353
+ return null;
11354
+ }
11355
+ var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
11356
+ function classifyLine2(line) {
11357
+ const obj = tryParseJsonObject(line);
11358
+ if (!obj) return null;
11359
+ const s = obj.status;
11360
+ if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
11361
+ return buildEnvelope(s, obj);
11362
+ }
11363
+ function tryParseJsonObject(line) {
11364
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
11365
+ try {
11366
+ return JSON.parse(line);
11367
+ } catch {
11368
+ return null;
11369
+ }
11370
+ }
11371
+ function buildEnvelope(status, obj) {
11372
+ const env = { status };
11373
+ if (typeof obj.findings === "number") env.findings = obj.findings;
11374
+ if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
11375
+ if (typeof obj.message === "string") env.message = obj.message;
11376
+ if (obj.outputs && typeof obj.outputs === "object") {
11377
+ env.outputs = obj.outputs;
11378
+ }
11379
+ return env;
11380
+ }
11381
+ function mapStructured(env, stdout, stderr) {
11382
+ const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
11383
+ switch (env.status) {
11384
+ case "ok":
11385
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11386
+ case "findings": {
11387
+ const wake = env.wakeAgent ?? findings > 0;
11388
+ return { passed: !wake, findings, output: stdout, stderr, structured: env };
11389
+ }
11390
+ case "skip":
11391
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11392
+ case "error":
11393
+ return {
11394
+ passed: false,
11395
+ findings: Math.max(findings, 1),
11396
+ output: stdout,
11397
+ stderr,
11398
+ structured: env
11399
+ };
11400
+ default:
11401
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11402
+ }
11403
+ }
11404
+ function heuristicResult(stdout, stderr, exitedAbnormally) {
11405
+ const combined = [stdout, stderr].filter(Boolean).join("\n");
11406
+ const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
11407
+ const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
11408
+ return {
11409
+ passed: findings === 0 && !exitedAbnormally,
11410
+ findings,
11411
+ output: stdout,
11412
+ stderr,
11413
+ structured: null
11414
+ };
11415
+ }
11416
+
11417
+ // src/maintenance/output-store.ts
11418
+ var fs16 = __toESM(require("fs"));
11419
+ var path18 = __toESM(require("path"));
11420
+ var DEFAULT_RETENTION = {
11421
+ runs: 50,
11422
+ maxAgeDays: 30
11423
+ };
11424
+ var fallbackLogger2 = {
11425
+ info: () => {
11426
+ },
11427
+ warn: (m, c) => console.warn(m, c),
11428
+ error: (m, c) => console.error(m, c)
11429
+ };
11430
+ var TaskOutputStore = class {
11431
+ rootDir;
11432
+ retentionDefaults;
11433
+ logger;
11434
+ constructor(options) {
11435
+ this.rootDir = options.rootDir;
11436
+ this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
11437
+ this.logger = options.logger ?? fallbackLogger2;
11438
+ }
11439
+ /**
11440
+ * Reject task IDs that don't match the validator's kebab-case pattern —
11441
+ * defends `dirFor()` against caller-supplied path-traversal segments
11442
+ * (`'../foo'`) when the store is invoked from CLI surfaces that don't
11443
+ * round-trip through `validateCustomTasks`.
11444
+ */
11445
+ ensureSafeTaskId(taskId) {
11446
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
11447
+ throw new Error(
11448
+ `TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
11449
+ );
11450
+ }
11451
+ }
11452
+ /**
11453
+ * Persist a single run entry. Retention is applied after the write so
11454
+ * the latest record is durable even if pruning fails.
11455
+ */
11456
+ async write(taskId, entry, retention) {
11457
+ this.ensureSafeTaskId(taskId);
11458
+ const dir = this.dirFor(taskId);
11459
+ await fs16.promises.mkdir(dir, { recursive: true });
11460
+ const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11461
+ const filePath = path18.join(dir, fileName);
11462
+ const tmpPath = `${filePath}.tmp`;
11463
+ const payload = JSON.stringify(entry, null, 2);
11464
+ await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11465
+ await fs16.promises.rename(tmpPath, filePath);
11466
+ try {
11467
+ await this.applyRetention(taskId, retention);
11468
+ } catch (err) {
11469
+ this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
11470
+ }
11471
+ }
11472
+ /**
11473
+ * Return the most recent persisted entry for the task, or null if none.
11474
+ */
11475
+ async latest(taskId) {
11476
+ const entries = await this.list(taskId, 1, 0);
11477
+ return entries[0] ?? null;
11478
+ }
11479
+ /**
11480
+ * List entries newest-first with offset+limit pagination.
11481
+ */
11482
+ async list(taskId, limit, offset) {
11483
+ this.ensureSafeTaskId(taskId);
11484
+ const dir = this.dirFor(taskId);
11485
+ const fileNames = await listJsonFilesDescending(dir);
11486
+ const slice = fileNames.slice(offset, offset + limit);
11487
+ const out = [];
11488
+ for (const name of slice) {
11489
+ const entry = await this.readEntry(path18.join(dir, name));
11490
+ if (entry) out.push(entry);
11491
+ }
11492
+ return out;
11493
+ }
11494
+ /**
11495
+ * Lookup a specific run by its file name (without the `.json` suffix) or
11496
+ * by its raw completion timestamp.
11497
+ */
11498
+ async get(taskId, runId) {
11499
+ this.ensureSafeTaskId(taskId);
11500
+ if (/[\\/]|\.\./.test(runId)) {
11501
+ throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
11502
+ }
11503
+ const dir = this.dirFor(taskId);
11504
+ const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11505
+ return this.readEntry(path18.join(dir, fileName));
11506
+ }
11507
+ /**
11508
+ * The on-disk root for a given task. Exposed for tooling that needs to walk
11509
+ * outputs from outside the store API.
11510
+ */
11511
+ dirFor(taskId) {
11512
+ return path18.join(this.rootDir, taskId, "outputs");
11513
+ }
11514
+ async readEntry(filePath) {
11515
+ try {
11516
+ const buf = await fs16.promises.readFile(filePath, "utf-8");
11517
+ const parsed = JSON.parse(buf);
11518
+ return parsed;
11519
+ } catch {
11520
+ return null;
11521
+ }
11522
+ }
11523
+ async applyRetention(taskId, retention) {
11524
+ const runs = retention?.runs ?? this.retentionDefaults.runs;
11525
+ const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
11526
+ const dir = this.dirFor(taskId);
11527
+ const fileNames = await listJsonFilesDescending(dir);
11528
+ const overflow = fileNames.slice(runs);
11529
+ const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
11530
+ const aged = [];
11531
+ for (const name of fileNames) {
11532
+ const ts = parseIsoFromFileName(name);
11533
+ if (ts !== null && ts < cutoffMs) aged.push(name);
11534
+ }
11535
+ const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11536
+ for (const name of toRemove) {
11537
+ try {
11538
+ await fs16.promises.unlink(path18.join(dir, name));
11539
+ } catch {
11540
+ }
11541
+ }
11542
+ }
11543
+ };
11544
+ async function listJsonFilesDescending(dir) {
11545
+ let names;
11546
+ try {
11547
+ names = await fs16.promises.readdir(dir);
11548
+ } catch {
11549
+ return [];
11550
+ }
11551
+ return names.filter((n) => n.endsWith(".json")).sort().reverse();
11552
+ }
11553
+ function sanitizeIso(iso) {
11554
+ return iso.replace(/:/g, "-");
11555
+ }
11556
+ function parseIsoFromFileName(fileName) {
11557
+ const stem = fileName.replace(/\.json$/, "");
11558
+ const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
11559
+ const ms = Date.parse(restored);
11560
+ return Number.isFinite(ms) ? ms : null;
11561
+ }
11562
+
11563
+ // src/maintenance/context-resolver.ts
11564
+ var ContextResolver = class {
11565
+ outputStore;
11566
+ skillReader;
11567
+ logger;
11568
+ perUpstreamMaxChars;
11569
+ constructor(options) {
11570
+ this.outputStore = options.outputStore;
11571
+ this.skillReader = options.skillReader ?? null;
11572
+ this.logger = options.logger ?? fallbackLogger3;
11573
+ this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
11574
+ }
11575
+ async resolveContextFrom(upstreamTaskIds, options = {}) {
11576
+ if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
11577
+ const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
11578
+ const now = Date.now();
11579
+ const sections = [];
11580
+ for (const id of upstreamTaskIds) {
11581
+ const entry = await this.outputStore.latest(id);
11582
+ sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
11583
+ }
11584
+ return `## Upstream context
11585
+
11586
+ ${sections.join("\n\n")}
11587
+ `;
11588
+ }
11589
+ async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
11590
+ if (!skillNames || skillNames.length === 0) return "";
11591
+ if (!this.skillReader) return "";
11592
+ const charBudget = budgetTokens * 4;
11593
+ let used = 0;
11594
+ const sections = [];
11595
+ let truncatedAt = -1;
11596
+ for (let i = 0; i < skillNames.length; i++) {
11597
+ const name = skillNames[i];
11598
+ const body = await this.skillReader.read(name);
11599
+ if (body === null) {
11600
+ this.logger.warn("inlineSkills: skill not found in registry", { name });
11601
+ continue;
11602
+ }
11603
+ const block = `### ${name}
11604
+
11605
+ ${body}`;
11606
+ if (used + block.length > charBudget) {
11607
+ truncatedAt = i;
11608
+ break;
11609
+ }
11610
+ used += block.length;
11611
+ sections.push(block);
11612
+ }
11613
+ if (truncatedAt >= 0) {
11614
+ this.logger.warn(
11615
+ `inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
11616
+ );
11617
+ }
11618
+ if (sections.length === 0) return "";
11619
+ return `## Reference skills
11620
+
11621
+ ${sections.join("\n\n")}
11622
+ `;
11623
+ }
11624
+ formatUpstream(id, entry, now, maxAgeMs) {
11625
+ if (!entry) {
11626
+ return `### ${id}
11627
+
11628
+ _[no prior run]_`;
11629
+ }
11630
+ const completedMs = Date.parse(entry.completedAt);
11631
+ if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
11632
+ return `### ${id} (last run ${entry.completedAt}, stale)
11633
+
11634
+ _[stale: omitted]_`;
11635
+ }
11636
+ const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
11637
+ const body = (entry.stdout ?? "").trim();
11638
+ const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
11639
+
11640
+ _[truncated]_` : body;
11641
+ return `${head}
11642
+
11643
+ ${truncated || "_[no stdout captured]_"}`;
11644
+ }
11645
+ };
11646
+ var fallbackLogger3 = {
11647
+ info: () => {
11648
+ },
11649
+ warn: () => {
11650
+ },
11651
+ error: () => {
11652
+ }
11653
+ };
11654
+
11655
+ // src/maintenance/custom-task-validator.ts
11656
+ var import_types30 = require("@harness-engineering/types");
11657
+ var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
11658
+ var REQUIRED_FIELDS_BY_TYPE = {
11659
+ "mechanical-ai": ["branch", "fixSkill"],
11660
+ "pure-ai": ["branch", "fixSkill"],
11661
+ "report-only": [],
11662
+ housekeeping: []
11663
+ };
11664
+ function validateCustomTasks(customTasks, builtIns, deps = {}) {
11665
+ const errors = [];
11666
+ if (!customTasks) return (0, import_types30.Ok)(void 0);
11667
+ const builtInIds = new Set(builtIns.map((t) => t.id));
11668
+ const customIds = Object.keys(customTasks);
11669
+ const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
11670
+ for (const id of customIds) {
11671
+ const task = customTasks[id];
11672
+ if (!task) continue;
11673
+ validateOne(id, task, builtInIds, allIds, deps, errors);
11674
+ }
11675
+ detectCycles(customTasks, builtIns, errors);
11676
+ return errors.length === 0 ? (0, import_types30.Ok)(void 0) : (0, import_types30.Err)(errors);
11677
+ }
11678
+ function validateOne(id, task, builtInIds, allIds, deps, errors) {
11679
+ const prefix = `customTasks.${id}`;
11680
+ if (!ID_PATTERN.test(id)) {
11681
+ errors.push({
11682
+ path: prefix,
11683
+ message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
11684
+ });
11685
+ }
11686
+ if (builtInIds.has(id)) {
11687
+ errors.push({
11688
+ path: prefix,
11689
+ message: `task ID '${id}' collides with a built-in task; choose a different name`
11690
+ });
11691
+ }
11692
+ if (!task.description || task.description.trim().length === 0) {
11693
+ errors.push({ path: `${prefix}.description`, message: "description is required" });
11694
+ }
11695
+ if (!task.schedule || task.schedule.trim().length === 0) {
11696
+ errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
11697
+ }
11698
+ validateCheckShape(prefix, task, errors);
11699
+ validateRequiredByType(prefix, task, errors);
11700
+ validateContextFrom(prefix, id, task, allIds, errors);
11701
+ validateInlineSkills(prefix, task, deps, errors);
11702
+ validateScriptPath(prefix, task, deps, errors);
11703
+ }
11704
+ function validateCheckShape(prefix, task, errors) {
11705
+ const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
11706
+ const hasScript = task.checkScript !== void 0;
11707
+ if (hasCommand && hasScript) {
11708
+ errors.push({
11709
+ path: prefix,
11710
+ message: "a task may declare checkCommand OR checkScript, not both"
11711
+ });
11712
+ }
11713
+ const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
11714
+ if (needsCheck && !hasCommand && !hasScript) {
11715
+ errors.push({
11716
+ path: prefix,
11717
+ message: `${task.type} task must declare either checkCommand or checkScript`
11718
+ });
11719
+ }
11720
+ if (hasScript) {
11721
+ const path22 = task.checkScript?.path;
11722
+ if (!path22 || path22.trim().length === 0) {
11723
+ errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
9439
11724
  }
9440
- const checkResult = await this.checkRunner.run(task.checkCommand, this.cwd);
9441
- const parsed = parseStatusLine(checkResult.output);
9442
- const status = parsed?.status ?? "success";
9443
- const findings = parsed === null ? checkResult.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
9444
- const result = {
9445
- taskId: task.id,
9446
- startedAt,
9447
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9448
- status,
9449
- findings,
9450
- fixed: 0,
9451
- prUrl: null,
9452
- prUpdated: false
9453
- };
9454
- if (parsed?.error) {
9455
- result.error = parsed.error;
11725
+ if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
11726
+ errors.push({
11727
+ path: `${prefix}.checkScript.timeoutMs`,
11728
+ message: "timeoutMs must be a positive integer"
11729
+ });
9456
11730
  }
9457
- return result;
9458
11731
  }
9459
- /**
9460
- * Housekeeping: run command directly, no AI, no PR.
9461
- *
9462
- * Captures stdout and parses a trailing JSON status line if present.
9463
- * Recognized contracts:
9464
- * - Phase 4/5 status contract (e.g., harness pulse run): success/skipped/failure/no-issues
9465
- * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
9466
- * Legacy housekeeping commands that emit no JSON keep the prior behavior:
9467
- * status: 'success', findings: 0.
9468
- */
9469
- async runHousekeeping(task, startedAt) {
9470
- if (!task.checkCommand || task.checkCommand.length === 0) {
9471
- return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
9472
- }
9473
- let stdout;
9474
- try {
9475
- const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
9476
- stdout = out.stdout ?? "";
9477
- } catch (err) {
9478
- return this.failureResult(task.id, startedAt, String(err));
11732
+ }
11733
+ function validateRequiredByType(prefix, task, errors) {
11734
+ const required = REQUIRED_FIELDS_BY_TYPE[task.type];
11735
+ if (!required) {
11736
+ errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
11737
+ return;
11738
+ }
11739
+ for (const field of required) {
11740
+ const value = task[field];
11741
+ if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
11742
+ errors.push({
11743
+ path: `${prefix}.${String(field)}`,
11744
+ message: `${task.type} task requires ${String(field)}`
11745
+ });
9479
11746
  }
9480
- const parsed = parseStatusLine(stdout);
9481
- const status = parsed?.status ?? "success";
9482
- const result = {
9483
- taskId: task.id,
9484
- startedAt,
9485
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9486
- status,
9487
- findings: 0,
9488
- fixed: 0,
9489
- prUrl: null,
9490
- prUpdated: false
9491
- };
9492
- if (parsed?.error) result.error = parsed.error;
9493
- return result;
9494
11747
  }
9495
- /**
9496
- * Resolve which AI backend name to use for a given task.
9497
- * Priority: per-task override > global config > 'local' default.
9498
- */
9499
- resolveBackend(taskId) {
9500
- const taskOverride = this.config.tasks?.[taskId]?.aiBackend;
9501
- if (taskOverride) return taskOverride;
9502
- return this.config.aiBackend ?? "local";
11748
+ if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
11749
+ errors.push({
11750
+ path: `${prefix}.branch`,
11751
+ message: `${task.type} task requires a non-null branch`
11752
+ });
9503
11753
  }
9504
- failureResult(taskId, startedAt, error) {
9505
- return {
9506
- taskId,
9507
- startedAt,
9508
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
9509
- status: "failure",
9510
- findings: 0,
9511
- fixed: 0,
9512
- prUrl: null,
9513
- prUpdated: false,
9514
- error
9515
- };
11754
+ }
11755
+ function validateContextFrom(prefix, selfId, task, allIds, errors) {
11756
+ if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
11757
+ errors.push({
11758
+ path: `${prefix}.contextFromMaxAgeMinutes`,
11759
+ message: "contextFromMaxAgeMinutes must be a positive integer"
11760
+ });
9516
11761
  }
9517
- };
9518
- function parseStatusLine(output) {
9519
- const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
9520
- for (let i = lines.length - 1; i >= 0; i--) {
9521
- const line = lines[i];
9522
- if (!line || !line.startsWith("{") || !line.endsWith("}")) continue;
9523
- try {
9524
- const obj = JSON.parse(line);
9525
- const s = obj.status;
9526
- if (s === "success" || s === "skipped" || s === "failure" || s === "no-issues") {
9527
- const parsed = { status: s, rawStatus: s };
9528
- if (typeof obj.candidatesFound === "number") {
9529
- parsed.candidatesFound = obj.candidatesFound;
9530
- }
9531
- if (typeof obj.error === "string") {
9532
- parsed.error = obj.error;
9533
- }
9534
- if (typeof obj.reason === "string") {
9535
- parsed.reason = obj.reason;
9536
- }
9537
- if (typeof obj.detail === "string" && !parsed.error) {
9538
- parsed.error = `${parsed.reason ?? "skipped"}: ${obj.detail}`;
9539
- }
9540
- return parsed;
9541
- }
9542
- if (s === "updated" || s === "no-op") {
9543
- return { status: "success", rawStatus: s };
9544
- }
9545
- if (s === "error") {
9546
- const message = typeof obj.message === "string" ? obj.message : "unknown error";
9547
- return { status: "failure", error: message, rawStatus: "error" };
9548
- }
9549
- } catch {
11762
+ if (!task.contextFrom) return;
11763
+ for (let i = 0; i < task.contextFrom.length; i++) {
11764
+ const upstreamId = task.contextFrom[i];
11765
+ if (!upstreamId) continue;
11766
+ if (upstreamId === selfId) {
11767
+ errors.push({
11768
+ path: `${prefix}.contextFrom[${i}]`,
11769
+ message: `task '${selfId}' cannot reference itself in contextFrom`
11770
+ });
11771
+ }
11772
+ if (!allIds.has(upstreamId)) {
11773
+ errors.push({
11774
+ path: `${prefix}.contextFrom[${i}]`,
11775
+ message: `references unknown task '${upstreamId}'`
11776
+ });
9550
11777
  }
9551
11778
  }
9552
- return null;
11779
+ }
11780
+ function validateInlineSkills(prefix, task, deps, errors) {
11781
+ if (!task.inlineSkills) return;
11782
+ if (!deps.skillExists) return;
11783
+ for (let i = 0; i < task.inlineSkills.length; i++) {
11784
+ const name = task.inlineSkills[i];
11785
+ if (!name) continue;
11786
+ if (!deps.skillExists(name)) {
11787
+ errors.push({
11788
+ path: `${prefix}.inlineSkills[${i}]`,
11789
+ message: `skill '${name}' not found in the registry`
11790
+ });
11791
+ }
11792
+ }
11793
+ if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
11794
+ errors.push({
11795
+ path: `${prefix}.inlineSkillsBudgetTokens`,
11796
+ message: "inlineSkillsBudgetTokens must be a positive integer"
11797
+ });
11798
+ }
11799
+ }
11800
+ function validateScriptPath(prefix, task, deps, errors) {
11801
+ if (!task.checkScript?.path) return;
11802
+ if (!deps.scriptExists) return;
11803
+ if (!deps.scriptExists(task.checkScript.path)) {
11804
+ errors.push({
11805
+ path: `${prefix}.checkScript.path`,
11806
+ message: `executable not found: ${task.checkScript.path}`
11807
+ });
11808
+ }
11809
+ }
11810
+ function detectCycles(customTasks, builtIns, errors) {
11811
+ const adjacency = /* @__PURE__ */ new Map();
11812
+ for (const t of builtIns) adjacency.set(t.id, []);
11813
+ for (const [id, task] of Object.entries(customTasks)) {
11814
+ adjacency.set(id, (task.contextFrom ?? []).slice());
11815
+ }
11816
+ const color = /* @__PURE__ */ new Map();
11817
+ for (const id of adjacency.keys()) color.set(id, "white");
11818
+ const reported = /* @__PURE__ */ new Set();
11819
+ for (const id of Object.keys(customTasks)) {
11820
+ if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
11821
+ }
11822
+ }
11823
+ function visitFromRoot(start, adjacency, color, errors, reported) {
11824
+ const stack = [{ id: start, nextIdx: 0, path: [start] }];
11825
+ color.set(start, "grey");
11826
+ while (stack.length) {
11827
+ const top = stack[stack.length - 1];
11828
+ const neighbors = adjacency.get(top.id) ?? [];
11829
+ if (top.nextIdx >= neighbors.length) {
11830
+ color.set(top.id, "black");
11831
+ stack.pop();
11832
+ continue;
11833
+ }
11834
+ const next = neighbors[top.nextIdx++];
11835
+ if (!next || !adjacency.has(next)) continue;
11836
+ handleEdge(top, next, color, stack, errors, reported);
11837
+ }
11838
+ }
11839
+ function handleEdge(top, next, color, stack, errors, reported) {
11840
+ const nextColor = color.get(next);
11841
+ if (nextColor === "grey") {
11842
+ reportCycle(top.path, next, errors, reported);
11843
+ } else if (nextColor === "white") {
11844
+ color.set(next, "grey");
11845
+ stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11846
+ }
11847
+ }
11848
+ function reportCycle(path22, next, errors, reported) {
11849
+ const cycleStart = path22.indexOf(next);
11850
+ const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
11851
+ const key = cyclePath.join("\u2192");
11852
+ if (reported.has(key)) return;
11853
+ reported.add(key);
11854
+ errors.push({
11855
+ path: `customTasks.${cyclePath[0]}.contextFrom`,
11856
+ message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
11857
+ });
9553
11858
  }
9554
11859
 
9555
11860
  // src/orchestrator.ts
@@ -9639,13 +11944,20 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9639
11944
  cacheMetrics;
9640
11945
  otlpExporter;
9641
11946
  telemetryFanoutOff;
11947
+ // Hermes Phase 3: in-process notification sinks subscribe to the same
11948
+ // event bus (`this`) that webhook fanout uses, applying envelope
11949
+ // formatting before delivering to Slack/etc. The registry + unwire
11950
+ // handle are kept on the instance so stop() can detach listeners and
11951
+ // call adapter dispose() in deterministic order.
11952
+ notificationsRegistry;
11953
+ notificationFanoutOff;
9642
11954
  orchestratorIdPromise;
9643
11955
  recorder;
9644
11956
  intelligenceRunner;
9645
11957
  completionHandler;
9646
11958
  /** Project root directory, derived from workspace root. */
9647
11959
  get projectRoot() {
9648
- return path16.resolve(this.config.workspace.root, "..", "..");
11960
+ return path19.resolve(this.config.workspace.root, "..", "..");
9649
11961
  }
9650
11962
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
9651
11963
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -9700,10 +12012,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9700
12012
  this.renderer = new PromptRenderer();
9701
12013
  this.overrideBackend = overrides?.backend ?? null;
9702
12014
  this.interactionQueue = new InteractionQueue(
9703
- path16.join(config.workspace.root, "..", "interactions"),
12015
+ path19.join(config.workspace.root, "..", "interactions"),
9704
12016
  this
9705
12017
  );
9706
- this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
12018
+ this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
9707
12019
  const backendsMap = this.config.agent.backends ?? {};
9708
12020
  for (const [name, def] of Object.entries(backendsMap)) {
9709
12021
  if (def.type === "local" || def.type === "pi") {
@@ -9717,7 +12029,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9717
12029
  this.localResolvers.set(name, new LocalModelResolver(resolverOpts));
9718
12030
  }
9719
12031
  }
9720
- this.cacheMetrics = new import_core13.CacheMetricsRecorder();
12032
+ this.cacheMetrics = new import_core16.CacheMetricsRecorder();
9721
12033
  if (this.config.agent.backends !== void 0 && Object.keys(this.config.agent.backends).length > 0) {
9722
12034
  const sandboxPolicy = this.config.agent.sandboxPolicy === "docker" ? "docker" : "none";
9723
12035
  const firstBackendName = Object.keys(this.config.agent.backends)[0];
@@ -9747,7 +12059,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9747
12059
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
9748
12060
  });
9749
12061
  this.recorder = new StreamRecorder(
9750
- path16.resolve(config.workspace.root, "..", "streams"),
12062
+ path19.resolve(config.workspace.root, "..", "streams"),
9751
12063
  this.logger
9752
12064
  );
9753
12065
  const self = this;
@@ -9778,10 +12090,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9778
12090
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
9779
12091
  if (config.server?.port) {
9780
12092
  const webhookStore = new WebhookStore(
9781
- path16.join(this.projectRoot, ".harness", "webhooks.json")
12093
+ path19.join(this.projectRoot, ".harness", "webhooks.json")
9782
12094
  );
9783
12095
  this.webhookQueue = new WebhookQueue(
9784
- path16.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12096
+ path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
9785
12097
  );
9786
12098
  const webhookDelivery = new WebhookDelivery({
9787
12099
  queue: this.webhookQueue,
@@ -9794,9 +12106,10 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9794
12106
  delivery: webhookDelivery
9795
12107
  });
9796
12108
  webhookDelivery.start();
12109
+ this.setupNotifications(config.notifications);
9797
12110
  const otlpCfg = config.telemetry?.export?.otlp;
9798
12111
  if (otlpCfg) {
9799
- this.otlpExporter = new import_core13.OTLPExporter({
12112
+ this.otlpExporter = new import_core16.OTLPExporter({
9800
12113
  endpoint: otlpCfg.endpoint,
9801
12114
  ...otlpCfg.enabled !== void 0 ? { enabled: otlpCfg.enabled } : {},
9802
12115
  ...otlpCfg.headers !== void 0 ? { headers: otlpCfg.headers } : {},
@@ -9818,7 +12131,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9818
12131
  queue: this.webhookQueue
9819
12132
  },
9820
12133
  cacheMetrics: this.cacheMetrics,
9821
- plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
12134
+ plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
9822
12135
  pipeline: this.pipeline,
9823
12136
  analysisArchive: this.analysisArchive,
9824
12137
  roadmapPath: config.tracker.filePath ?? null,
@@ -9856,7 +12169,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9856
12169
  ...this.config.tracker.apiKey ? { token: this.config.tracker.apiKey } : {},
9857
12170
  ...this.config.tracker.endpoint ? { apiBase: this.config.tracker.endpoint } : {}
9858
12171
  };
9859
- const clientResult = (0, import_core12.createTrackerClient)(trackerCfg);
12172
+ const clientResult = (0, import_core15.createTrackerClient)(trackerCfg);
9860
12173
  if (!clientResult.ok) throw clientResult.error;
9861
12174
  return new GitHubIssuesIssueTrackerAdapter(clientResult.value, this.config.tracker);
9862
12175
  }
@@ -9874,13 +12187,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9874
12187
  const logger = this.logger;
9875
12188
  const checkRunner = {
9876
12189
  run: async (command, cwd) => {
9877
- const { execFile: execFile6 } = await import("child_process");
9878
- const { promisify: promisify4 } = await import("util");
9879
- const execFileAsync = promisify4(execFile6);
12190
+ const { execFile: execFile7 } = await import("child_process");
12191
+ const { promisify: promisify5 } = await import("util");
12192
+ const execFileAsync2 = promisify5(execFile7);
9880
12193
  const [cmd, ...args] = command;
9881
12194
  if (!cmd) return { passed: true, findings: 0, output: "" };
9882
12195
  try {
9883
- const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
12196
+ const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
9884
12197
  const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
9885
12198
  const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
9886
12199
  return { passed: findings === 0, findings, output: stdout };
@@ -9909,13 +12222,13 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9909
12222
  };
9910
12223
  const commandExecutor = {
9911
12224
  exec: async (command, cwd) => {
9912
- const { execFile: execFile6 } = await import("child_process");
9913
- const { promisify: promisify4 } = await import("util");
9914
- const execFileAsync = promisify4(execFile6);
12225
+ const { execFile: execFile7 } = await import("child_process");
12226
+ const { promisify: promisify5 } = await import("util");
12227
+ const execFileAsync2 = promisify5(execFile7);
9915
12228
  const [cmd, ...args] = command;
9916
12229
  if (!cmd) return { stdout: "" };
9917
12230
  try {
9918
- const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
12231
+ const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
9919
12232
  return { stdout: String(stdout) };
9920
12233
  } catch (err) {
9921
12234
  logger.warn("Maintenance command execution failed", {
@@ -9927,12 +12240,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9927
12240
  }
9928
12241
  }
9929
12242
  };
12243
+ const outputStore = new TaskOutputStore({
12244
+ rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12245
+ logger: this.logger
12246
+ });
12247
+ const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
12248
+ const skillReader = {
12249
+ // The orchestrator does not own the skill registry; CLI-side skill
12250
+ // resolution wires this in via direct injection. Default: skill not
12251
+ // resolvable from the orchestrator boundary.
12252
+ read: async () => null
12253
+ };
12254
+ const contextResolver = new ContextResolver({
12255
+ outputStore,
12256
+ skillReader,
12257
+ logger: this.logger
12258
+ });
9930
12259
  return new TaskRunner({
9931
12260
  config: maintenanceConfig,
9932
12261
  checkRunner,
9933
12262
  agentDispatcher,
9934
12263
  commandExecutor,
9935
- cwd: this.projectRoot
12264
+ cwd: this.projectRoot,
12265
+ checkScriptRunner,
12266
+ contextResolver,
12267
+ outputStore
9936
12268
  });
9937
12269
  }
9938
12270
  /**
@@ -9940,8 +12272,17 @@ var Orchestrator = class extends import_node_events.EventEmitter {
9940
12272
  * Extracted from start() to keep function length under threshold.
9941
12273
  */
9942
12274
  async initMaintenance(maintenanceConfig) {
12275
+ const validation = validateCustomTasks(
12276
+ maintenanceConfig.customTasks,
12277
+ BUILT_IN_TASKS
12278
+ );
12279
+ if (!validation.ok) {
12280
+ const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
12281
+ throw new Error(`Invalid maintenance.customTasks configuration:
12282
+ ${messages}`);
12283
+ }
9943
12284
  this.maintenanceReporter = new MaintenanceReporter({
9944
- persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
12285
+ persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
9945
12286
  logger: this.logger
9946
12287
  });
9947
12288
  await this.maintenanceReporter.load();
@@ -10208,7 +12549,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10208
12549
  { issueId }
10209
12550
  );
10210
12551
  await this.interactionQueue.push({
10211
- id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
12552
+ id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
10212
12553
  issueId,
10213
12554
  type: "needs-human",
10214
12555
  reasons: [`Agent pushed branch "${branch}" but did not create a PR. Worktree preserved.`],
@@ -10284,7 +12625,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10284
12625
  { issueId: effect.issueId }
10285
12626
  );
10286
12627
  await this.interactionQueue.push({
10287
- id: `interaction-${(0, import_node_crypto15.randomUUID)()}`,
12628
+ id: `interaction-${(0, import_node_crypto16.randomUUID)()}`,
10288
12629
  issueId: effect.issueId,
10289
12630
  type: "needs-human",
10290
12631
  reasons: effect.reasons,
@@ -10380,12 +12721,12 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10380
12721
  async postLifecycleComment(identifier, externalId, event) {
10381
12722
  try {
10382
12723
  if (!externalId) return;
10383
- const trackerConfig = (0, import_core12.loadTrackerSyncConfig)(this.projectRoot);
12724
+ const trackerConfig = (0, import_core15.loadTrackerSyncConfig)(this.projectRoot);
10384
12725
  if (!trackerConfig) return;
10385
12726
  const token = process.env.GITHUB_TOKEN;
10386
12727
  if (!token) return;
10387
12728
  const orchestratorId = await this.orchestratorIdPromise;
10388
- const adapter = new import_core12.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
12729
+ const adapter = new import_core15.GitHubIssuesSyncAdapter({ token, config: trackerConfig });
10389
12730
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").slice(0, 19);
10390
12731
  const actionMap = {
10391
12732
  claimed: "Dispatching agent for autonomous execution",
@@ -10458,7 +12799,7 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10458
12799
  ...f.line !== void 0 ? { line: f.line } : {}
10459
12800
  }))
10460
12801
  );
10461
- (0, import_core11.writeTaint)(
12802
+ (0, import_core14.writeTaint)(
10462
12803
  workspacePath,
10463
12804
  issue.id,
10464
12805
  "Medium-severity injection patterns found in workspace config files",
@@ -10629,6 +12970,31 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10629
12970
  );
10630
12971
  this.emit("state_change", this.getSnapshot());
10631
12972
  }
12973
+ /**
12974
+ * Hermes Phase 3: wire in-process notification sinks against the
12975
+ * orchestrator's event bus (`this`). A misconfigured sink (unknown kind,
12976
+ * missing env var) logs + skips rather than breaking startup — the
12977
+ * hardened doctor (`harness doctor`) surfaces the gap. Sinks subscribe
12978
+ * to the same topics as `wireWebhookFanout`; a slow Slack call cannot
12979
+ * block webhook delivery because the two paths fan out independently.
12980
+ */
12981
+ setupNotifications(notifConfig) {
12982
+ if (!notifConfig || !notifConfig.sinks || notifConfig.sinks.length === 0) return;
12983
+ try {
12984
+ this.notificationsRegistry = SinkRegistry.fromConfig(notifConfig, {
12985
+ env: process.env
12986
+ });
12987
+ this.notificationFanoutOff = wireNotificationSinks({
12988
+ bus: this,
12989
+ registry: this.notificationsRegistry
12990
+ });
12991
+ } catch (err) {
12992
+ this.logger.warn(
12993
+ `notifications sink registry failed: ${err instanceof Error ? err.message : String(err)}; sinks disabled`
12994
+ );
12995
+ delete this.notificationsRegistry;
12996
+ }
12997
+ }
10632
12998
  /**
10633
12999
  * Stops execution for a specific issue.
10634
13000
  *
@@ -10796,6 +13162,14 @@ var Orchestrator = class extends import_node_events.EventEmitter {
10796
13162
  this.webhookFanoutOff();
10797
13163
  delete this.webhookFanoutOff;
10798
13164
  }
13165
+ if (this.notificationFanoutOff) {
13166
+ this.notificationFanoutOff();
13167
+ delete this.notificationFanoutOff;
13168
+ }
13169
+ if (this.notificationsRegistry) {
13170
+ await this.notificationsRegistry.dispose();
13171
+ delete this.notificationsRegistry;
13172
+ }
10799
13173
  if (this.telemetryFanoutOff) {
10800
13174
  this.telemetryFanoutOff();
10801
13175
  delete this.telemetryFanoutOff;
@@ -11084,11 +13458,11 @@ function launchTUI(orchestrator) {
11084
13458
  }
11085
13459
 
11086
13460
  // src/maintenance/sync-main.ts
11087
- var import_node_child_process9 = require("child_process");
11088
- var import_node_util3 = require("util");
11089
- var DEFAULT_TIMEOUT_MS2 = 6e4;
13461
+ var import_node_child_process12 = require("child_process");
13462
+ var import_node_util4 = require("util");
13463
+ var DEFAULT_TIMEOUT_MS3 = 6e4;
11090
13464
  async function git(execFileFn, args, cwd, timeoutMs) {
11091
- const exec = (0, import_node_util3.promisify)(execFileFn);
13465
+ const exec = (0, import_node_util4.promisify)(execFileFn);
11092
13466
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
11093
13467
  return { stdout: String(stdout), stderr: String(stderr) };
11094
13468
  }
@@ -11150,8 +13524,8 @@ async function isAncestor(execFileFn, a, b, cwd, timeoutMs) {
11150
13524
  }
11151
13525
  }
11152
13526
  async function syncMain(repoRoot, opts = {}) {
11153
- const execFileFn = opts.execFileFn ?? import_node_child_process9.execFile;
11154
- const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
13527
+ const execFileFn = opts.execFileFn ?? import_node_child_process12.execFile;
13528
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
11155
13529
  try {
11156
13530
  const originRef = await resolveOriginDefault(execFileFn, repoRoot, timeoutMs);
11157
13531
  if (!originRef) {
@@ -11226,11 +13600,471 @@ async function syncMain(repoRoot, opts = {}) {
11226
13600
  };
11227
13601
  }
11228
13602
  }
13603
+
13604
+ // src/sessions/search-index.ts
13605
+ var fs17 = __toESM(require("fs"));
13606
+ var path20 = __toESM(require("path"));
13607
+ var import_better_sqlite32 = __toESM(require("better-sqlite3"));
13608
+ var import_types31 = require("@harness-engineering/types");
13609
+ var SEARCH_INDEX_FILE = "search-index.sqlite";
13610
+ var SCHEMA_SQL2 = `
13611
+ CREATE TABLE IF NOT EXISTS session_docs (
13612
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
13613
+ session_id TEXT NOT NULL,
13614
+ archived INTEGER NOT NULL,
13615
+ file_kind TEXT NOT NULL,
13616
+ path TEXT NOT NULL,
13617
+ mtime_ms INTEGER NOT NULL,
13618
+ body TEXT NOT NULL,
13619
+ UNIQUE (session_id, archived, file_kind)
13620
+ );
13621
+
13622
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_docs_fts USING fts5 (
13623
+ body,
13624
+ content='session_docs',
13625
+ content_rowid='id',
13626
+ tokenize='unicode61 remove_diacritics 2'
13627
+ );
13628
+
13629
+ CREATE TRIGGER IF NOT EXISTS session_docs_ai
13630
+ AFTER INSERT ON session_docs
13631
+ BEGIN INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body); END;
13632
+
13633
+ CREATE TRIGGER IF NOT EXISTS session_docs_ad
13634
+ AFTER DELETE ON session_docs
13635
+ BEGIN INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body); END;
13636
+
13637
+ CREATE TRIGGER IF NOT EXISTS session_docs_au
13638
+ AFTER UPDATE ON session_docs
13639
+ BEGIN
13640
+ INSERT INTO session_docs_fts(session_docs_fts, rowid, body) VALUES('delete', old.id, old.body);
13641
+ INSERT INTO session_docs_fts(rowid, body) VALUES (new.id, new.body);
13642
+ END;
13643
+ `;
13644
+ var DEFAULT_LIMIT = 20;
13645
+ function normalizeFts5Query(query) {
13646
+ const advancedSyntax = /["()*^+]|\bAND\b|\bOR\b|\bNOT\b|[A-Za-z_]+:/;
13647
+ if (advancedSyntax.test(query)) return query;
13648
+ return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
13649
+ }
13650
+ function searchIndexPath(projectPath) {
13651
+ return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13652
+ }
13653
+ var FILE_KIND_TO_FILENAME = {
13654
+ summary: "summary.md",
13655
+ learnings: "learnings.md",
13656
+ failures: "failures.md",
13657
+ sections: "session-sections.md",
13658
+ llm_summary: "llm-summary.md"
13659
+ };
13660
+ var SqliteSearchIndex = class {
13661
+ db;
13662
+ upsertStmt;
13663
+ removeSessionStmt;
13664
+ totalStmt;
13665
+ constructor(dbPath) {
13666
+ fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
13667
+ this.db = new import_better_sqlite32.default(dbPath);
13668
+ this.db.pragma("journal_mode = WAL");
13669
+ this.db.pragma("synchronous = NORMAL");
13670
+ this.db.exec(SCHEMA_SQL2);
13671
+ this.upsertStmt = this.db.prepare(
13672
+ `INSERT INTO session_docs (session_id, archived, file_kind, path, mtime_ms, body)
13673
+ VALUES (@sessionId, @archived, @fileKind, @path, @mtimeMs, @body)
13674
+ ON CONFLICT(session_id, archived, file_kind) DO UPDATE SET
13675
+ path = excluded.path,
13676
+ mtime_ms = excluded.mtime_ms,
13677
+ body = excluded.body`
13678
+ );
13679
+ this.removeSessionStmt = this.db.prepare(`DELETE FROM session_docs WHERE session_id = ?`);
13680
+ this.totalStmt = this.db.prepare(`SELECT COUNT(*) AS n FROM session_docs`);
13681
+ }
13682
+ upsertSessionDoc(doc) {
13683
+ this.upsertStmt.run({
13684
+ sessionId: doc.sessionId,
13685
+ archived: doc.archived ? 1 : 0,
13686
+ fileKind: doc.fileKind,
13687
+ path: doc.path,
13688
+ mtimeMs: Math.floor(doc.mtimeMs),
13689
+ body: doc.body
13690
+ });
13691
+ }
13692
+ removeSession(sessionId) {
13693
+ const info = this.removeSessionStmt.run(sessionId);
13694
+ return info.changes;
13695
+ }
13696
+ /**
13697
+ * Drop all `archived=1` rows. Used by `reindexFromArchive` before a full
13698
+ * re-walk. Live (archived=0) rows are preserved.
13699
+ */
13700
+ resetArchived() {
13701
+ this.db.prepare(`DELETE FROM session_docs WHERE archived = 1`).run();
13702
+ }
13703
+ /** Total rows currently indexed (across both live and archived). */
13704
+ totalIndexed() {
13705
+ const row = this.totalStmt.get();
13706
+ return row.n;
13707
+ }
13708
+ /**
13709
+ * Ranked FTS5 query. Returns BM25-sorted matches. The `query` is passed to
13710
+ * FTS5 as-is; FTS5 syntax (phrases with quotes, AND/OR/NOT, `column:term`)
13711
+ * is therefore the user-facing language. Errors from malformed queries
13712
+ * surface as thrown `SqliteError` so the CLI can catch + render them.
13713
+ */
13714
+ search(query, opts = {}) {
13715
+ const limit = opts.limit ?? DEFAULT_LIMIT;
13716
+ const filters = [];
13717
+ const params = { q: normalizeFts5Query(query), limit };
13718
+ if (opts.archivedOnly) {
13719
+ filters.push("d.archived = 1");
13720
+ }
13721
+ const fileKinds = opts.fileKinds && opts.fileKinds.length > 0 ? opts.fileKinds : null;
13722
+ if (fileKinds) {
13723
+ const placeholders = fileKinds.map((_, i) => `@fk${i}`).join(", ");
13724
+ filters.push(`d.file_kind IN (${placeholders})`);
13725
+ fileKinds.forEach((k, i) => {
13726
+ params[`fk${i}`] = k;
13727
+ });
13728
+ }
13729
+ const whereClause = filters.length > 0 ? `AND ${filters.join(" AND ")}` : "";
13730
+ const sql = `
13731
+ SELECT
13732
+ d.session_id AS sessionId,
13733
+ d.archived AS archived,
13734
+ d.file_kind AS fileKind,
13735
+ d.path AS path,
13736
+ bm25(session_docs_fts) AS bm25,
13737
+ snippet(session_docs_fts, 0, '\u2026', '\u2026', '\u2026', 16) AS snippet
13738
+ FROM session_docs_fts
13739
+ JOIN session_docs d ON d.id = session_docs_fts.rowid
13740
+ WHERE session_docs_fts MATCH @q
13741
+ ${whereClause}
13742
+ ORDER BY bm25 ASC
13743
+ LIMIT @limit
13744
+ `;
13745
+ const start = Date.now();
13746
+ const rows = this.db.prepare(sql).all(params);
13747
+ const durationMs = Date.now() - start;
13748
+ const matches = rows.map((r) => ({
13749
+ sessionId: r.sessionId,
13750
+ archived: r.archived === 1,
13751
+ fileKind: r.fileKind,
13752
+ path: r.path,
13753
+ bm25: r.bm25,
13754
+ snippet: r.snippet
13755
+ }));
13756
+ return { matches, durationMs, totalIndexed: this.totalIndexed() };
13757
+ }
13758
+ close() {
13759
+ this.db.close();
13760
+ }
13761
+ };
13762
+ function openSearchIndex(projectPath) {
13763
+ return new SqliteSearchIndex(searchIndexPath(projectPath));
13764
+ }
13765
+ function indexSessionDirectory(idx, args) {
13766
+ const kinds = args.fileKinds ?? [...import_types31.INDEXED_FILE_KINDS];
13767
+ const cap = args.maxBytesPerBody ?? 256 * 1024;
13768
+ let docsWritten = 0;
13769
+ for (const kind of kinds) {
13770
+ const fileName = FILE_KIND_TO_FILENAME[kind];
13771
+ const filePath = path20.join(args.sessionDir, fileName);
13772
+ if (!fs17.existsSync(filePath)) continue;
13773
+ let body = fs17.readFileSync(filePath, "utf8");
13774
+ if (Buffer.byteLength(body, "utf8") > cap) {
13775
+ body = body.slice(0, cap) + "\n\n[TRUNCATED]";
13776
+ }
13777
+ const stat = fs17.statSync(filePath);
13778
+ const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
13779
+ idx.upsertSessionDoc({
13780
+ sessionId: args.sessionId,
13781
+ archived: args.archived,
13782
+ fileKind: kind,
13783
+ path: relPath,
13784
+ mtimeMs: stat.mtimeMs,
13785
+ body
13786
+ });
13787
+ docsWritten++;
13788
+ }
13789
+ return { docsWritten };
13790
+ }
13791
+ function reindexFromArchive(projectPath, opts = {}) {
13792
+ const start = Date.now();
13793
+ const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
13794
+ const idx = openSearchIndex(projectPath);
13795
+ try {
13796
+ idx.resetArchived();
13797
+ let sessionsIndexed = 0;
13798
+ let docsWritten = 0;
13799
+ if (fs17.existsSync(archiveBase)) {
13800
+ const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
13801
+ for (const entry of entries) {
13802
+ if (!entry.isDirectory()) continue;
13803
+ const sessionDir = path20.join(archiveBase, entry.name);
13804
+ const result = indexSessionDirectory(idx, {
13805
+ sessionId: entry.name,
13806
+ sessionDir,
13807
+ archived: true,
13808
+ projectPath,
13809
+ ...opts.fileKinds && { fileKinds: opts.fileKinds },
13810
+ ...opts.maxBytesPerBody !== void 0 && { maxBytesPerBody: opts.maxBytesPerBody }
13811
+ });
13812
+ if (result.docsWritten > 0) sessionsIndexed++;
13813
+ docsWritten += result.docsWritten;
13814
+ }
13815
+ }
13816
+ return { sessionsIndexed, docsWritten, durationMs: Date.now() - start };
13817
+ } finally {
13818
+ idx.close();
13819
+ }
13820
+ }
13821
+
13822
+ // src/sessions/summarize.ts
13823
+ var fs18 = __toESM(require("fs"));
13824
+ var path21 = __toESM(require("path"));
13825
+ var import_types32 = require("@harness-engineering/types");
13826
+ var import_types33 = require("@harness-engineering/types");
13827
+ var LLM_SUMMARY_FILE = "llm-summary.md";
13828
+ var SUMMARY_INPUT_FILES = [
13829
+ { filename: "summary.md", kind: "summary" },
13830
+ { filename: "learnings.md", kind: "learnings" },
13831
+ { filename: "failures.md", kind: "failures" },
13832
+ { filename: "session-sections.md", kind: "sections" }
13833
+ ];
13834
+ var DEFAULT_INPUT_BUDGET_TOKENS = 16e3;
13835
+ var DEFAULT_TIMEOUT_MS4 = 6e4;
13836
+ var CHARS_PER_TOKEN = 4;
13837
+ var SYSTEM_PROMPT = `You produce concise, structured retrospectives of completed harness-engineering sessions.
13838
+
13839
+ Read the session's archived markdown files and emit a JSON object that conforms exactly to the provided schema. Be specific and grounded \u2014 quote artefacts (file names, skill names, error messages) verbatim when relevant. Do not invent. If a field has no content, return an empty array.`;
13840
+ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-engineering session. Produce a structured summary capturing:
13841
+ - headline: one-sentence retrospective (\u2264 120 chars)
13842
+ - keyOutcomes: concrete things that shipped / decisions made (\u2264 20 strings)
13843
+ - openQuestions: items still open (\u2264 20 strings)
13844
+ - relatedSessions: other session slugs referenced (may be empty)
13845
+
13846
+ ---
13847
+
13848
+ `;
13849
+ function readInputCorpus(archiveDir) {
13850
+ const parts = [];
13851
+ for (const { filename, kind } of SUMMARY_INPUT_FILES) {
13852
+ const p = path21.join(archiveDir, filename);
13853
+ if (!fs18.existsSync(p)) continue;
13854
+ try {
13855
+ const content = fs18.readFileSync(p, "utf8");
13856
+ if (content.trim().length === 0) continue;
13857
+ parts.push(`## FILE: ${kind}
13858
+
13859
+ ${content.trim()}`);
13860
+ } catch {
13861
+ }
13862
+ }
13863
+ return parts.join("\n\n");
13864
+ }
13865
+ function truncateForBudget(text, inputBudgetTokens) {
13866
+ const cap = Math.max(0, inputBudgetTokens * CHARS_PER_TOKEN);
13867
+ if (text.length <= cap) return text;
13868
+ return text.slice(0, cap) + "\n\n[TRUNCATED \u2014 input exceeded token budget]";
13869
+ }
13870
+ function renderLlmSummaryMarkdown(summary, meta) {
13871
+ const lines = [
13872
+ "---",
13873
+ `generatedAt: ${meta.generatedAt}`,
13874
+ `model: ${meta.model}`,
13875
+ `inputTokens: ${meta.inputTokens}`,
13876
+ `outputTokens: ${meta.outputTokens}`,
13877
+ `schemaVersion: ${meta.schemaVersion}`,
13878
+ "---",
13879
+ "",
13880
+ "## Headline",
13881
+ summary.headline,
13882
+ "",
13883
+ "## Key outcomes"
13884
+ ];
13885
+ if (summary.keyOutcomes.length === 0) {
13886
+ lines.push("_(none)_");
13887
+ } else {
13888
+ for (const item of summary.keyOutcomes) lines.push(`- ${item}`);
13889
+ }
13890
+ lines.push("", "## Open questions");
13891
+ if (summary.openQuestions.length === 0) {
13892
+ lines.push("_(none)_");
13893
+ } else {
13894
+ for (const item of summary.openQuestions) lines.push(`- ${item}`);
13895
+ }
13896
+ lines.push("", "## Related sessions");
13897
+ if (summary.relatedSessions.length === 0) {
13898
+ lines.push("_(none)_");
13899
+ } else {
13900
+ for (const item of summary.relatedSessions) lines.push(`- ${item}`);
13901
+ }
13902
+ lines.push("");
13903
+ return lines.join("\n");
13904
+ }
13905
+ function writeStubMarkdown(archiveDir, reason) {
13906
+ const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
13907
+ const body = `---
13908
+ generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
13909
+ schemaVersion: 1
13910
+ status: failed
13911
+ ---
13912
+
13913
+ ## Summary unavailable
13914
+
13915
+ - reason: ${reason}
13916
+ `;
13917
+ fs18.writeFileSync(filePath, body, "utf8");
13918
+ return filePath;
13919
+ }
13920
+ async function summarizeArchivedSession(ctx) {
13921
+ const writeStubOnError = ctx.writeStubOnError ?? true;
13922
+ if (!fs18.existsSync(ctx.archiveDir)) {
13923
+ return (0, import_types33.Err)(new Error(`archive directory not found: ${ctx.archiveDir}`));
13924
+ }
13925
+ const corpus = readInputCorpus(ctx.archiveDir);
13926
+ if (corpus.trim().length === 0) {
13927
+ return (0, import_types33.Err)(new Error(`no summary input files found in ${ctx.archiveDir}`));
13928
+ }
13929
+ const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
13930
+ const truncated = truncateForBudget(corpus, inputBudgetTokens);
13931
+ const prompt = USER_PROMPT_PREAMBLE + truncated;
13932
+ const timeoutMs = ctx.config?.timeoutMs ?? DEFAULT_TIMEOUT_MS4;
13933
+ const analyzeOpts = {
13934
+ prompt,
13935
+ systemPrompt: SYSTEM_PROMPT,
13936
+ responseSchema: import_types32.SessionSummarySchema,
13937
+ ...ctx.config?.model && { model: ctx.config.model }
13938
+ };
13939
+ let response;
13940
+ try {
13941
+ response = await Promise.race([
13942
+ ctx.provider.analyze(analyzeOpts),
13943
+ new Promise(
13944
+ (_, reject) => setTimeout(
13945
+ () => reject(new Error(`provider call timed out after ${timeoutMs}ms`)),
13946
+ timeoutMs
13947
+ )
13948
+ )
13949
+ ]);
13950
+ } catch (e) {
13951
+ const reason = e instanceof Error ? e.message : String(e);
13952
+ ctx.logger?.warn?.("session summary: provider call failed", { reason });
13953
+ let stubPath;
13954
+ if (writeStubOnError) {
13955
+ try {
13956
+ stubPath = writeStubMarkdown(ctx.archiveDir, reason);
13957
+ } catch {
13958
+ }
13959
+ }
13960
+ return (0, import_types33.Err)(
13961
+ new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
13962
+ );
13963
+ }
13964
+ const parsed = import_types32.SessionSummarySchema.safeParse(response.result);
13965
+ if (!parsed.success) {
13966
+ const reason = `schema validation failed: ${parsed.error.message}`;
13967
+ ctx.logger?.warn?.("session summary: invalid provider payload", { reason });
13968
+ if (writeStubOnError) {
13969
+ try {
13970
+ writeStubMarkdown(ctx.archiveDir, reason);
13971
+ } catch {
13972
+ }
13973
+ }
13974
+ return (0, import_types33.Err)(new Error(reason));
13975
+ }
13976
+ const meta = {
13977
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
13978
+ model: response.model,
13979
+ inputTokens: response.tokenUsage.inputTokens,
13980
+ outputTokens: response.tokenUsage.outputTokens,
13981
+ schemaVersion: 1
13982
+ };
13983
+ const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13984
+ const body = renderLlmSummaryMarkdown(parsed.data, meta);
13985
+ fs18.writeFileSync(filePath, body, "utf8");
13986
+ return (0, import_types33.Ok)({ summary: parsed.data, meta, filePath });
13987
+ }
13988
+ function isSummaryEnabled(config) {
13989
+ if (!config) return false;
13990
+ if (config.enabled === false) return false;
13991
+ return true;
13992
+ }
13993
+
13994
+ // src/sessions/archive-hooks.ts
13995
+ var defaultLogger = {
13996
+ warn: (msg, meta) => console.warn(`[sessions] ${msg}`, meta)
13997
+ };
13998
+ async function runSummaryStep(opts, logger, sessionId, archiveDir) {
13999
+ const enabled = isSummaryEnabled(opts.config?.summary) && opts.provider != null;
14000
+ if (!enabled || !opts.provider) return;
14001
+ const ctx = {
14002
+ archiveDir,
14003
+ provider: opts.provider,
14004
+ ...opts.config?.summary && { config: opts.config.summary },
14005
+ ...logger && { logger }
14006
+ };
14007
+ try {
14008
+ const result = await summarizeArchivedSession(ctx);
14009
+ if (!result.ok) {
14010
+ logger.warn?.("session summary: failed", {
14011
+ sessionId,
14012
+ error: result.error.message
14013
+ });
14014
+ }
14015
+ } catch (e) {
14016
+ logger.warn?.("session summary: threw", {
14017
+ sessionId,
14018
+ error: e instanceof Error ? e.message : String(e)
14019
+ });
14020
+ }
14021
+ }
14022
+ function runIndexStep(opts, logger, sessionId, archiveDir) {
14023
+ try {
14024
+ const idx = openSearchIndex(opts.projectPath);
14025
+ try {
14026
+ const result = indexSessionDirectory(idx, {
14027
+ sessionId,
14028
+ sessionDir: archiveDir,
14029
+ archived: true,
14030
+ projectPath: opts.projectPath,
14031
+ ...opts.config?.search?.indexedFileKinds && {
14032
+ fileKinds: opts.config.search.indexedFileKinds
14033
+ },
14034
+ ...opts.config?.search?.maxIndexBytesPerFile !== void 0 && {
14035
+ maxBytesPerBody: opts.config.search.maxIndexBytesPerFile
14036
+ }
14037
+ });
14038
+ if (result.docsWritten === 0) {
14039
+ logger.warn?.("session index: no docs written", { sessionId, archiveDir });
14040
+ }
14041
+ } finally {
14042
+ idx.close();
14043
+ }
14044
+ } catch (e) {
14045
+ logger.warn?.("session index: failed", {
14046
+ sessionId,
14047
+ error: e instanceof Error ? e.message : String(e)
14048
+ });
14049
+ }
14050
+ }
14051
+ function buildArchiveHooks(opts) {
14052
+ const logger = opts.logger ?? defaultLogger;
14053
+ return {
14054
+ async onArchived({ sessionId, archiveDir }) {
14055
+ await runSummaryStep(opts, logger, sessionId, archiveDir);
14056
+ runIndexStep(opts, logger, sessionId, archiveDir);
14057
+ }
14058
+ };
14059
+ }
11229
14060
  // Annotate the CommonJS export names for ESM import in node:
11230
14061
  0 && (module.exports = {
11231
14062
  AnalysisArchive,
14063
+ BUILT_IN_TASKS,
11232
14064
  BackendRouter,
11233
14065
  ClaimManager,
14066
+ GateNotReadyError,
14067
+ GateRunError,
11234
14068
  InteractionQueue,
11235
14069
  LinearGraphQLStub,
11236
14070
  MAX_ATTEMPTS,
@@ -11239,10 +14073,16 @@ async function syncMain(repoRoot, opts = {}) {
11239
14073
  Orchestrator,
11240
14074
  OrchestratorBackendFactory,
11241
14075
  PRDetector,
14076
+ PromotionError,
11242
14077
  PromptRenderer,
11243
14078
  RETRY_DELAYS_MS,
11244
14079
  RoadmapTrackerAdapter,
14080
+ SinkConfigError,
14081
+ SinkRegistry,
14082
+ SlackSink,
14083
+ SqliteSearchIndex,
11245
14084
  StreamRecorder,
14085
+ TaskOutputStore,
11246
14086
  TokenStore,
11247
14087
  WebhookQueue,
11248
14088
  WorkflowLoader,
@@ -11250,31 +14090,49 @@ async function syncMain(repoRoot, opts = {}) {
11250
14090
  WorkspaceManager,
11251
14091
  applyEvent,
11252
14092
  artifactPresenceFromIssue,
14093
+ buildArchiveHooks,
11253
14094
  calculateRetryDelay,
11254
14095
  canDispatch,
11255
14096
  computeRateLimitDelay,
11256
14097
  createBackend,
11257
14098
  createEmptyState,
11258
14099
  detectScopeTier,
14100
+ emitProposalApproved,
14101
+ emitProposalCreated,
14102
+ emitProposalRejected,
11259
14103
  extractHighlights,
11260
14104
  extractTitlePrefix,
11261
14105
  getAvailableSlots,
11262
14106
  getDefaultConfig,
11263
14107
  getPerStateCount,
14108
+ indexSessionDirectory,
11264
14109
  isEligible,
14110
+ isSummaryEnabled,
11265
14111
  launchTUI,
11266
14112
  loadPublishedIndex,
11267
14113
  migrateAgentConfig,
14114
+ normalizeFts5Query,
14115
+ openSearchIndex,
14116
+ promote,
11268
14117
  reconcile,
14118
+ reindexFromArchive,
11269
14119
  renderAnalysisComment,
14120
+ renderLlmSummaryMarkdown,
11270
14121
  renderPRComment,
11271
14122
  resolveEscalationConfig,
11272
14123
  resolveOrchestratorId,
11273
14124
  routeIssue,
14125
+ runGate,
11274
14126
  savePublishedIndex,
14127
+ searchIndexPath,
11275
14128
  selectCandidates,
11276
14129
  sortCandidates,
14130
+ summarizeArchivedSession,
11277
14131
  syncMain,
11278
14132
  triageIssue,
11279
- validateWorkflowConfig
14133
+ truncateForBudget,
14134
+ validateCustomTasks,
14135
+ validateWorkflowConfig,
14136
+ wireNotificationSinks,
14137
+ wrapAsEnvelope
11280
14138
  });