@harness-engineering/orchestrator 0.5.0 → 0.6.0

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