@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.mjs CHANGED
@@ -1144,7 +1144,7 @@ var ClaimManager = class {
1144
1144
  const claimResult = await this.tracker.claimIssue(issueId, this.orchestratorId);
1145
1145
  if (!claimResult.ok) return claimResult;
1146
1146
  if (this.verifyDelayMs > 0) {
1147
- await new Promise((resolve6) => setTimeout(resolve6, this.verifyDelayMs));
1147
+ await new Promise((resolve7) => setTimeout(resolve7, this.verifyDelayMs));
1148
1148
  }
1149
1149
  const statesResult = await this.tracker.fetchIssueStatesByIds([issueId]);
1150
1150
  if (!statesResult.ok) return statesResult;
@@ -1870,11 +1870,11 @@ var BackendsMapSchema = z2.record(z2.string(), BackendDefSchema);
1870
1870
  function crossFieldRoutingIssues(backends, routing) {
1871
1871
  const issues = [];
1872
1872
  const names = new Set(Object.keys(backends));
1873
- const checkRef = (path19, name) => {
1873
+ const checkRef = (path22, name) => {
1874
1874
  if (name !== void 0 && !names.has(name)) {
1875
1875
  issues.push({
1876
- path: path19,
1877
- message: `routing.${path19.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1876
+ path: path22,
1877
+ message: `routing.${path22.join(".")} references unknown backend '${name}'. Defined: [${[...names].join(", ")}].`
1878
1878
  });
1879
1879
  }
1880
1880
  };
@@ -2544,7 +2544,7 @@ var WorkspaceHooks = class {
2544
2544
  if (!command) {
2545
2545
  return Ok7(void 0);
2546
2546
  }
2547
- return new Promise((resolve6) => {
2547
+ return new Promise((resolve7) => {
2548
2548
  const filteredEnv = {};
2549
2549
  for (const [k, v] of Object.entries(process.env)) {
2550
2550
  if (v != null && !k.includes("SECRET") && !k.includes("TOKEN") && !k.includes("PASSWORD")) {
@@ -2557,19 +2557,19 @@ var WorkspaceHooks = class {
2557
2557
  });
2558
2558
  const timeout = setTimeout(() => {
2559
2559
  child.kill();
2560
- resolve6(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2560
+ resolve7(Err5(new Error(`Hook ${hookName} timed out after ${this.config.timeoutMs}ms`)));
2561
2561
  }, this.config.timeoutMs);
2562
2562
  child.on("exit", (code) => {
2563
2563
  clearTimeout(timeout);
2564
2564
  if (code === 0) {
2565
- resolve6(Ok7(void 0));
2565
+ resolve7(Ok7(void 0));
2566
2566
  } else {
2567
- resolve6(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2567
+ resolve7(Err5(new Error(`Hook ${hookName} failed with exit code ${code}`)));
2568
2568
  }
2569
2569
  });
2570
2570
  child.on("error", (error) => {
2571
2571
  clearTimeout(timeout);
2572
- resolve6(Err5(error));
2572
+ resolve7(Err5(error));
2573
2573
  });
2574
2574
  });
2575
2575
  }
@@ -2609,7 +2609,7 @@ var MockBackend = class {
2609
2609
  content: "Thinking...",
2610
2610
  sessionId: session.sessionId
2611
2611
  };
2612
- await new Promise((resolve6) => setTimeout(resolve6, 100));
2612
+ await new Promise((resolve7) => setTimeout(resolve7, 100));
2613
2613
  yield {
2614
2614
  type: "thought",
2615
2615
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -2661,7 +2661,7 @@ var PromptRenderer = class {
2661
2661
 
2662
2662
  // src/orchestrator.ts
2663
2663
  import { EventEmitter } from "events";
2664
- import * as path16 from "path";
2664
+ import * as path19 from "path";
2665
2665
  import { randomUUID as randomUUID5 } from "crypto";
2666
2666
  import { writeTaint } from "@harness-engineering/core";
2667
2667
 
@@ -3635,11 +3635,11 @@ function detectLegacyFields(agent) {
3635
3635
  }
3636
3636
  function buildCase1Warnings(presentLegacy, suppressLocalGroup) {
3637
3637
  const warnings = [];
3638
- for (const path19 of presentLegacy) {
3639
- if (CASE1_ALWAYS_SUPPRESS.has(path19)) continue;
3640
- if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path19)) continue;
3638
+ for (const path22 of presentLegacy) {
3639
+ if (CASE1_ALWAYS_SUPPRESS.has(path22)) continue;
3640
+ if (suppressLocalGroup && CASE1_LOCAL_GROUP.has(path22)) continue;
3641
3641
  warnings.push(
3642
- `Ignoring legacy field '${path19}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3642
+ `Ignoring legacy field '${path22}': 'agent.backends' is set and takes precedence. See ${MIGRATION_GUIDE}.`
3643
3643
  );
3644
3644
  }
3645
3645
  return warnings;
@@ -3667,7 +3667,7 @@ function migrateAgentConfig(agent) {
3667
3667
  }
3668
3668
  const { backends, routing } = synthesizeBackendsAndRouting(agent);
3669
3669
  const warnings = presentLegacy.map(
3670
- (path19) => `Deprecated config field '${path19}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3670
+ (path22) => `Deprecated config field '${path22}' is in use. Migrate to 'agent.backends' / 'agent.routing'. See ${MIGRATION_GUIDE}.`
3671
3671
  );
3672
3672
  return {
3673
3673
  config: { ...agent, backends, routing },
@@ -3783,8 +3783,8 @@ var BackendRouter = class {
3783
3783
  validateReferences() {
3784
3784
  const known = new Set(Object.keys(this.backends));
3785
3785
  const missing = [];
3786
- const check = (path19, name) => {
3787
- if (name !== void 0 && !known.has(name)) missing.push({ path: path19, name });
3786
+ const check = (path22, name) => {
3787
+ if (name !== void 0 && !known.has(name)) missing.push({ path: path22, name });
3788
3788
  };
3789
3789
  check("default", this.routing.default);
3790
3790
  check("quick-fix", this.routing["quick-fix"]);
@@ -3797,7 +3797,7 @@ var BackendRouter = class {
3797
3797
  check("isolation.container", this.routing.isolation?.container);
3798
3798
  check("isolation.remote-sandbox", this.routing.isolation?.["remote-sandbox"]);
3799
3799
  if (missing.length > 0) {
3800
- const detail = missing.map(({ path: path19, name }) => `routing.${path19} -> '${name}'`).join("; ");
3800
+ const detail = missing.map(({ path: path22, name }) => `routing.${path22} -> '${name}'`).join("; ");
3801
3801
  const known_ = [...known].join(", ") || "(none)";
3802
3802
  throw new Error(
3803
3803
  `BackendRouter: routing references unknown backend(s): ${detail}. Defined backends: [${known_}].`
@@ -3814,11 +3814,11 @@ import {
3814
3814
  Ok as Ok10,
3815
3815
  Err as Err7
3816
3816
  } from "@harness-engineering/types";
3817
- function resolveExitCode(code, command, resolve6) {
3817
+ function resolveExitCode(code, command, resolve7) {
3818
3818
  if (code === 0) {
3819
- resolve6(Ok10(void 0));
3819
+ resolve7(Ok10(void 0));
3820
3820
  } else {
3821
- resolve6(
3821
+ resolve7(
3822
3822
  Err7({
3823
3823
  category: "agent_not_found",
3824
3824
  message: `Claude command '${command}' not found or failed`
@@ -3826,8 +3826,8 @@ function resolveExitCode(code, command, resolve6) {
3826
3826
  );
3827
3827
  }
3828
3828
  }
3829
- function resolveSpawnError(command, resolve6) {
3830
- resolve6(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3829
+ function resolveSpawnError(command, resolve7) {
3830
+ resolve7(Err7({ category: "agent_not_found", message: `Claude command '${command}' not found` }));
3831
3831
  }
3832
3832
  var JUST_PAST_GRACE_MS = 5 * 6e4;
3833
3833
  var PRIMARY_LIMIT_RE = /You[\u2019']ve hit your limit.*resets\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm))\s*\(([^)]+)\)/i;
@@ -4140,10 +4140,10 @@ var ClaudeBackend = class {
4140
4140
  errRl.close();
4141
4141
  }
4142
4142
  if (exitCode === null) {
4143
- await new Promise((resolve6) => {
4143
+ await new Promise((resolve7) => {
4144
4144
  child.on("exit", (code) => {
4145
4145
  exitCode = code;
4146
- resolve6(null);
4146
+ resolve7(null);
4147
4147
  });
4148
4148
  });
4149
4149
  }
@@ -4165,10 +4165,10 @@ var ClaudeBackend = class {
4165
4165
  return Ok10(void 0);
4166
4166
  }
4167
4167
  async healthCheck() {
4168
- return new Promise((resolve6) => {
4168
+ return new Promise((resolve7) => {
4169
4169
  const child = spawn2(this.command, ["--version"]);
4170
- child.on("exit", (code) => resolveExitCode(code, this.command, resolve6));
4171
- child.on("error", () => resolveSpawnError(this.command, resolve6));
4170
+ child.on("exit", (code) => resolveExitCode(code, this.command, resolve7));
4171
+ child.on("error", () => resolveSpawnError(this.command, resolve7));
4172
4172
  });
4173
4173
  }
4174
4174
  };
@@ -4791,7 +4791,7 @@ var PiBackend = class {
4791
4791
  } else {
4792
4792
  resolvedModelName = this.config.model;
4793
4793
  }
4794
- const piSdk = await import("@mariozechner/pi-coding-agent");
4794
+ const piSdk = await import("@earendil-works/pi-coding-agent");
4795
4795
  const model = buildLocalModel({
4796
4796
  model: resolvedModelName,
4797
4797
  endpoint: this.config.endpoint,
@@ -4946,7 +4946,7 @@ var PiBackend = class {
4946
4946
  }
4947
4947
  async healthCheck() {
4948
4948
  try {
4949
- await import("@mariozechner/pi-coding-agent");
4949
+ await import("@earendil-works/pi-coding-agent");
4950
4950
  return Ok15(void 0);
4951
4951
  } catch (err) {
4952
4952
  return Err12({
@@ -5100,14 +5100,14 @@ var SshBackend = class {
5100
5100
  async healthCheck() {
5101
5101
  const args = [...this.buildSshArgs()];
5102
5102
  args[args.length - 1] = "true";
5103
- return new Promise((resolve6) => {
5103
+ return new Promise((resolve7) => {
5104
5104
  let child;
5105
5105
  try {
5106
5106
  child = this.spawnImpl(this.config.sshBinary, args, {
5107
5107
  stdio: ["ignore", "ignore", "pipe"]
5108
5108
  });
5109
5109
  } catch (err) {
5110
- resolve6(
5110
+ resolve7(
5111
5111
  Err13({
5112
5112
  category: "agent_not_found",
5113
5113
  message: err instanceof Error ? err.message : "failed to spawn ssh"
@@ -5128,9 +5128,9 @@ var SshBackend = class {
5128
5128
  child.on("close", (code) => {
5129
5129
  clearTimeout(timer);
5130
5130
  if (code === 0) {
5131
- resolve6(Ok16(void 0));
5131
+ resolve7(Ok16(void 0));
5132
5132
  } else {
5133
- resolve6(
5133
+ resolve7(
5134
5134
  Err13({
5135
5135
  category: "agent_not_found",
5136
5136
  message: `ssh health check failed (exit=${code ?? "null"}): ${stderr.slice(0, 500)}`
@@ -5140,7 +5140,7 @@ var SshBackend = class {
5140
5140
  });
5141
5141
  child.on("error", (err) => {
5142
5142
  clearTimeout(timer);
5143
- resolve6(Err13({ category: "agent_not_found", message: err.message }));
5143
+ resolve7(Err13({ category: "agent_not_found", message: err.message }));
5144
5144
  });
5145
5145
  });
5146
5146
  }
@@ -5188,13 +5188,13 @@ async function* readLines(stream) {
5188
5188
  if (buffer.length > 0) yield buffer;
5189
5189
  }
5190
5190
  function waitForExit(child) {
5191
- return new Promise((resolve6) => {
5191
+ return new Promise((resolve7) => {
5192
5192
  if (child.exitCode !== null) {
5193
- resolve6(child.exitCode);
5193
+ resolve7(child.exitCode);
5194
5194
  return;
5195
5195
  }
5196
- child.once("close", (code) => resolve6(code));
5197
- child.once("error", () => resolve6(null));
5196
+ child.once("close", (code) => resolve7(code));
5197
+ child.once("error", () => resolve7(null));
5198
5198
  });
5199
5199
  }
5200
5200
 
@@ -5384,14 +5384,14 @@ var OciServerlessBackend = class extends ServerlessBackend {
5384
5384
  return out;
5385
5385
  }
5386
5386
  runOneShot(binary, args) {
5387
- return new Promise((resolve6) => {
5387
+ return new Promise((resolve7) => {
5388
5388
  let child;
5389
5389
  try {
5390
5390
  child = this.spawnImpl(binary, args, {
5391
5391
  stdio: ["ignore", "pipe", "pipe"]
5392
5392
  });
5393
5393
  } catch (err) {
5394
- resolve6(
5394
+ resolve7(
5395
5395
  Err14({
5396
5396
  category: "agent_not_found",
5397
5397
  message: err instanceof Error ? err.message : "failed to spawn runtime"
@@ -5416,9 +5416,9 @@ var OciServerlessBackend = class extends ServerlessBackend {
5416
5416
  child.on("close", (code) => {
5417
5417
  clearTimeout(timer);
5418
5418
  if (code === 0) {
5419
- resolve6(Ok17(stdout));
5419
+ resolve7(Ok17(stdout));
5420
5420
  } else {
5421
- resolve6(
5421
+ resolve7(
5422
5422
  Err14({
5423
5423
  category: "response_error",
5424
5424
  message: `runtime '${binary} ${args.join(" ")}' exited ${code ?? "null"}: ${stderr.slice(0, 500)}`
@@ -5428,7 +5428,7 @@ var OciServerlessBackend = class extends ServerlessBackend {
5428
5428
  });
5429
5429
  child.on("error", (err) => {
5430
5430
  clearTimeout(timer);
5431
- resolve6(Err14({ category: "agent_not_found", message: err.message }));
5431
+ resolve7(Err14({ category: "agent_not_found", message: err.message }));
5432
5432
  });
5433
5433
  });
5434
5434
  }
@@ -5488,13 +5488,13 @@ async function* readLines2(stream) {
5488
5488
  if (buffer.length > 0) yield buffer;
5489
5489
  }
5490
5490
  function waitForExit2(child) {
5491
- return new Promise((resolve6) => {
5491
+ return new Promise((resolve7) => {
5492
5492
  if (child.exitCode !== null) {
5493
- resolve6(child.exitCode);
5493
+ resolve7(child.exitCode);
5494
5494
  return;
5495
5495
  }
5496
- child.once("close", (code) => resolve6(code));
5497
- child.once("error", () => resolve6(null));
5496
+ child.once("close", (code) => resolve7(code));
5497
+ child.once("error", () => resolve7(null));
5498
5498
  });
5499
5499
  }
5500
5500
 
@@ -5694,13 +5694,13 @@ var ContainerBackend = class {
5694
5694
  import { execFile as execFile3, spawn as spawn5 } from "child_process";
5695
5695
  import { Ok as Ok18, Err as Err16 } from "@harness-engineering/types";
5696
5696
  function dockerExec(args) {
5697
- return new Promise((resolve6, reject) => {
5697
+ return new Promise((resolve7, reject) => {
5698
5698
  execFile3("docker", args, (error, stdout) => {
5699
5699
  if (error) {
5700
5700
  reject(error);
5701
5701
  return;
5702
5702
  }
5703
- resolve6(stdout.trim());
5703
+ resolve7(stdout.trim());
5704
5704
  });
5705
5705
  });
5706
5706
  }
@@ -5759,11 +5759,11 @@ var DockerRuntime = class {
5759
5759
  } finally {
5760
5760
  rl.close();
5761
5761
  }
5762
- const exitCode = await new Promise((resolve6) => {
5762
+ const exitCode = await new Promise((resolve7) => {
5763
5763
  if (child.exitCode !== null) {
5764
- resolve6(child.exitCode);
5764
+ resolve7(child.exitCode);
5765
5765
  } else {
5766
- child.on("exit", (code) => resolve6(code ?? 1));
5766
+ child.on("exit", (code) => resolve7(code ?? 1));
5767
5767
  }
5768
5768
  });
5769
5769
  return exitCode;
@@ -5822,13 +5822,13 @@ var EnvSecretBackend = class {
5822
5822
  import { execFile as execFile4 } from "child_process";
5823
5823
  import { Ok as Ok20, Err as Err18 } from "@harness-engineering/types";
5824
5824
  function opExec(args) {
5825
- return new Promise((resolve6, reject) => {
5825
+ return new Promise((resolve7, reject) => {
5826
5826
  execFile4("op", args, (error, stdout) => {
5827
5827
  if (error) {
5828
5828
  reject(error);
5829
5829
  return;
5830
5830
  }
5831
- resolve6(stdout.trim());
5831
+ resolve7(stdout.trim());
5832
5832
  });
5833
5833
  });
5834
5834
  }
@@ -5871,13 +5871,13 @@ var OnePasswordSecretBackend = class {
5871
5871
  import { execFile as execFile5 } from "child_process";
5872
5872
  import { Ok as Ok21, Err as Err19 } from "@harness-engineering/types";
5873
5873
  function vaultExec(args, env) {
5874
- return new Promise((resolve6, reject) => {
5874
+ return new Promise((resolve7, reject) => {
5875
5875
  execFile5("vault", args, { env: { ...process.env, ...env } }, (error, stdout) => {
5876
5876
  if (error) {
5877
5877
  reject(error);
5878
5878
  return;
5879
5879
  }
5880
- resolve6(stdout.trim());
5880
+ resolve7(stdout.trim());
5881
5881
  });
5882
5882
  });
5883
5883
  }
@@ -6249,7 +6249,7 @@ function buildExplicitProvider(provider, selModel, config) {
6249
6249
 
6250
6250
  // src/server/http.ts
6251
6251
  import * as http from "http";
6252
- import * as path14 from "path";
6252
+ import * as path15 from "path";
6253
6253
  import { assertPortUsable } from "@harness-engineering/core";
6254
6254
 
6255
6255
  // src/server/websocket.ts
@@ -6312,7 +6312,7 @@ import { z as z3 } from "zod";
6312
6312
  // src/server/utils.ts
6313
6313
  var DEFAULT_MAX_BYTES = 1048576;
6314
6314
  function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6315
- return new Promise((resolve6, reject) => {
6315
+ return new Promise((resolve7, reject) => {
6316
6316
  let body = "";
6317
6317
  let bytes = 0;
6318
6318
  req.on("data", (chunk) => {
@@ -6324,7 +6324,7 @@ function readBody(req, maxBytes = DEFAULT_MAX_BYTES) {
6324
6324
  }
6325
6325
  body += String(chunk);
6326
6326
  });
6327
- req.on("end", () => resolve6(body));
6327
+ req.on("end", () => resolve7(body));
6328
6328
  req.on("error", reject);
6329
6329
  });
6330
6330
  }
@@ -7460,35 +7460,576 @@ function handleV1TelemetryRoute(req, res, deps) {
7460
7460
  return false;
7461
7461
  }
7462
7462
 
7463
- // src/server/routes/sessions.ts
7464
- import * as fs11 from "fs/promises";
7465
- import * as path11 from "path";
7463
+ // src/server/routes/v1/proposals.ts
7466
7464
  import { z as z13 } from "zod";
7467
- var SessionCreateSchema = z13.object({
7468
- sessionId: z13.string().min(1)
7465
+ import {
7466
+ getProposal as getProposal3,
7467
+ listProposals,
7468
+ updateProposal as updateProposal3,
7469
+ ProposalNotFoundError as ProposalNotFoundError3
7470
+ } from "@harness-engineering/core";
7471
+ import {
7472
+ EditProposalInputSchema
7473
+ } from "@harness-engineering/types";
7474
+
7475
+ // src/proposals/gate.ts
7476
+ import { parse as parseYaml } from "yaml";
7477
+ import {
7478
+ getProposal,
7479
+ updateProposal,
7480
+ ProposalNotFoundError
7481
+ } from "@harness-engineering/core";
7482
+ var GateRunError = class extends Error {
7483
+ constructor(message) {
7484
+ super(message);
7485
+ this.name = "GateRunError";
7486
+ }
7487
+ };
7488
+ var SKILL_NAME_RE = /^[a-z][a-z0-9-]*$/;
7489
+ function checkSkillYaml(yaml) {
7490
+ const findings = [];
7491
+ let doc;
7492
+ try {
7493
+ doc = parseYaml(yaml);
7494
+ } catch (err) {
7495
+ findings.push({
7496
+ severity: "error",
7497
+ title: "skill.yaml does not parse",
7498
+ detail: err instanceof Error ? err.message : String(err)
7499
+ });
7500
+ return findings;
7501
+ }
7502
+ if (!doc || typeof doc !== "object") {
7503
+ findings.push({
7504
+ severity: "error",
7505
+ title: "skill.yaml top-level is not a mapping",
7506
+ detail: "Expected a YAML document with keys at the root (name, version, description, \u2026)."
7507
+ });
7508
+ return findings;
7509
+ }
7510
+ const obj = doc;
7511
+ if (typeof obj["name"] !== "string") {
7512
+ findings.push({
7513
+ severity: "error",
7514
+ title: "skill.yaml missing `name`",
7515
+ detail: "Every skill must declare its kebab-case name."
7516
+ });
7517
+ }
7518
+ if (typeof obj["version"] !== "string") {
7519
+ findings.push({
7520
+ severity: "error",
7521
+ title: "skill.yaml missing `version`",
7522
+ detail: "Every skill must declare a semver version string."
7523
+ });
7524
+ }
7525
+ if (typeof obj["description"] !== "string") {
7526
+ findings.push({
7527
+ severity: "warning",
7528
+ title: "skill.yaml missing `description`",
7529
+ detail: "Description is strongly recommended for discoverability."
7530
+ });
7531
+ }
7532
+ return findings;
7533
+ }
7534
+ function checkSkillMd(md) {
7535
+ const findings = [];
7536
+ if (md.trim().length < 40) {
7537
+ findings.push({
7538
+ severity: "error",
7539
+ title: "SKILL.md is too short",
7540
+ detail: "A skill needs a meaningful description (at least 40 non-whitespace characters)."
7541
+ });
7542
+ }
7543
+ if (!/^#\s+\S/m.test(md)) {
7544
+ findings.push({
7545
+ severity: "warning",
7546
+ title: "SKILL.md has no top-level heading",
7547
+ detail: "Convention: open SKILL.md with `# <Skill Name>`."
7548
+ });
7549
+ }
7550
+ return findings;
7551
+ }
7552
+ function checkName(name) {
7553
+ if (SKILL_NAME_RE.test(name)) return [];
7554
+ return [
7555
+ {
7556
+ severity: "error",
7557
+ title: "skill name violates the kebab-case rule",
7558
+ detail: `"${name}" must match /^[a-z][a-z0-9-]*$/. Use only lowercase letters, digits, and hyphens; start with a letter.`
7559
+ }
7560
+ ];
7561
+ }
7562
+ function checkDiff(diff) {
7563
+ const findings = [];
7564
+ if (!diff.includes("---") || !diff.includes("+++")) {
7565
+ findings.push({
7566
+ severity: "error",
7567
+ title: "Refinement diff is not in unified-diff format",
7568
+ detail: "Diffs must include both `---` and `+++` headers."
7569
+ });
7570
+ }
7571
+ if (!/^@@\s/m.test(diff)) {
7572
+ findings.push({
7573
+ severity: "warning",
7574
+ title: "Refinement diff has no hunk marker",
7575
+ detail: "A unified diff typically contains at least one `@@` line."
7576
+ });
7577
+ }
7578
+ return findings;
7579
+ }
7580
+ function deriveFindings(proposal) {
7581
+ const findings = [];
7582
+ findings.push(...checkName(proposal.content.name));
7583
+ if (proposal.kind === "new-skill") {
7584
+ findings.push(...checkSkillYaml(proposal.content.skillYaml ?? ""));
7585
+ findings.push(...checkSkillMd(proposal.content.skillMd ?? ""));
7586
+ } else if (proposal.kind === "refinement") {
7587
+ findings.push(...checkDiff(proposal.content.diff ?? ""));
7588
+ }
7589
+ return findings;
7590
+ }
7591
+ async function runGate(projectPath, proposalId) {
7592
+ const proposal = await getProposal(projectPath, proposalId);
7593
+ if (!proposal) throw new ProposalNotFoundError(proposalId);
7594
+ if (proposal.status === "approved" || proposal.status === "rejected") {
7595
+ throw new GateRunError(
7596
+ `proposal ${proposalId} is already ${proposal.status}; cannot re-run the gate`
7597
+ );
7598
+ }
7599
+ const findings = deriveFindings(proposal);
7600
+ const runAt = (/* @__PURE__ */ new Date()).toISOString();
7601
+ const hasError = findings.some((f) => f.severity === "error");
7602
+ const nextStatus = hasError ? "gate-failed" : "gate-running";
7603
+ const updated = await updateProposal(projectPath, proposalId, {
7604
+ status: nextStatus,
7605
+ gate: { lastRunAt: runAt, findings }
7606
+ });
7607
+ return {
7608
+ proposalId: updated.id,
7609
+ status: updated.status,
7610
+ findings,
7611
+ runAt
7612
+ };
7613
+ }
7614
+
7615
+ // src/proposals/promote.ts
7616
+ import * as fs11 from "fs";
7617
+ import * as path11 from "path";
7618
+ import { parse as parseYaml2, stringify as stringifyYaml } from "yaml";
7619
+ import {
7620
+ getProposal as getProposal2,
7621
+ updateProposal as updateProposal2,
7622
+ ProposalNotFoundError as ProposalNotFoundError2
7623
+ } from "@harness-engineering/core";
7624
+ var GateNotReadyError = class extends Error {
7625
+ constructor(message) {
7626
+ super(message);
7627
+ this.name = "GateNotReadyError";
7628
+ }
7629
+ };
7630
+ var PromotionError = class extends Error {
7631
+ constructor(message) {
7632
+ super(message);
7633
+ this.name = "PromotionError";
7634
+ }
7635
+ };
7636
+ var GATE_FRESHNESS_MS = 24 * 60 * 60 * 1e3;
7637
+ function skillDir(projectPath, name) {
7638
+ return path11.join(projectPath, "agents", "skills", "claude-code", name);
7639
+ }
7640
+ function readIfExists(p) {
7641
+ try {
7642
+ return fs11.readFileSync(p, "utf-8");
7643
+ } catch {
7644
+ return null;
7645
+ }
7646
+ }
7647
+ function injectProvenanceIntoYaml(yamlText, proposalId) {
7648
+ let doc;
7649
+ try {
7650
+ doc = parseYaml2(yamlText);
7651
+ } catch (err) {
7652
+ throw new PromotionError(
7653
+ `skill.yaml does not parse: ${err instanceof Error ? err.message : String(err)}`
7654
+ );
7655
+ }
7656
+ if (!doc || typeof doc !== "object") {
7657
+ throw new PromotionError("skill.yaml top-level is not a mapping");
7658
+ }
7659
+ const obj = doc;
7660
+ obj["provenance"] = "agent-proposed";
7661
+ obj["originatingProposalId"] = proposalId;
7662
+ return stringifyYaml(obj);
7663
+ }
7664
+ function assertGateReady(proposal) {
7665
+ if (proposal.status !== "gate-running") {
7666
+ throw new GateNotReadyError(
7667
+ `proposal ${proposal.id} is in status "${proposal.status}"; the gate must pass before promotion`
7668
+ );
7669
+ }
7670
+ const findings = proposal.gate?.findings ?? [];
7671
+ if (findings.some((f) => f.severity === "error")) {
7672
+ throw new GateNotReadyError(
7673
+ `proposal ${proposal.id} has unresolved gate errors; re-run the gate after edits`
7674
+ );
7675
+ }
7676
+ if (!proposal.gate?.lastRunAt) {
7677
+ throw new GateNotReadyError(`proposal ${proposal.id} has no gate run on record`);
7678
+ }
7679
+ const ageMs = Date.now() - Date.parse(proposal.gate.lastRunAt);
7680
+ if (!Number.isFinite(ageMs) || ageMs > GATE_FRESHNESS_MS) {
7681
+ throw new GateNotReadyError(
7682
+ `proposal ${proposal.id} gate run is older than 24h; re-run before approving`
7683
+ );
7684
+ }
7685
+ }
7686
+ async function promoteNewSkill(projectPath, proposal) {
7687
+ const target = skillDir(projectPath, proposal.content.name);
7688
+ if (fs11.existsSync(target)) {
7689
+ throw new PromotionError(
7690
+ `a catalog skill already exists at ${target}; use a refinement proposal to update it`
7691
+ );
7692
+ }
7693
+ fs11.mkdirSync(target, { recursive: true });
7694
+ const yamlOut = injectProvenanceIntoYaml(proposal.content.skillYaml ?? "", proposal.id);
7695
+ fs11.writeFileSync(path11.join(target, "skill.yaml"), yamlOut);
7696
+ fs11.writeFileSync(path11.join(target, "SKILL.md"), proposal.content.skillMd ?? "");
7697
+ return { skillPath: target };
7698
+ }
7699
+ async function promoteRefinement(projectPath, proposal) {
7700
+ if (!proposal.targetSkill) {
7701
+ throw new PromotionError("refinement proposal is missing targetSkill");
7702
+ }
7703
+ const target = skillDir(projectPath, proposal.targetSkill);
7704
+ if (!fs11.existsSync(target)) {
7705
+ throw new PromotionError(
7706
+ `target skill ${proposal.targetSkill} does not exist at ${target}; cannot refine`
7707
+ );
7708
+ }
7709
+ const yamlPath = path11.join(target, "skill.yaml");
7710
+ const before = readIfExists(yamlPath) ?? "";
7711
+ const after = injectProvenanceIntoYaml(before, proposal.id);
7712
+ if (after === before) {
7713
+ throw new PromotionError(
7714
+ "no metadata changes detected; check that the reviewer applied the proposed diff before approving"
7715
+ );
7716
+ }
7717
+ fs11.writeFileSync(yamlPath, after);
7718
+ return { skillPath: target };
7719
+ }
7720
+ async function promote(projectPath, proposalId, decidedBy) {
7721
+ const proposal = await getProposal2(projectPath, proposalId);
7722
+ if (!proposal) throw new ProposalNotFoundError2(proposalId);
7723
+ assertGateReady(proposal);
7724
+ const out = proposal.kind === "new-skill" ? await promoteNewSkill(projectPath, proposal) : await promoteRefinement(projectPath, proposal);
7725
+ await updateProposal2(projectPath, proposalId, {
7726
+ status: "approved",
7727
+ decision: {
7728
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
7729
+ decidedBy,
7730
+ action: "approved"
7731
+ }
7732
+ });
7733
+ return {
7734
+ proposalId,
7735
+ skillPath: out.skillPath,
7736
+ provenance: "agent-proposed"
7737
+ };
7738
+ }
7739
+
7740
+ // src/proposals/events.ts
7741
+ function emit3(bus, topic, data) {
7742
+ bus.emit(topic, data);
7743
+ }
7744
+ function emitProposalCreated(bus, proposal) {
7745
+ const data = {
7746
+ id: proposal.id,
7747
+ kind: proposal.kind,
7748
+ name: proposal.content.name,
7749
+ proposedBy: proposal.proposedBy,
7750
+ justification: proposal.source.justification
7751
+ };
7752
+ if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
7753
+ emit3(bus, "proposal.created", data);
7754
+ }
7755
+ function emitProposalApproved(bus, proposal) {
7756
+ const data = {
7757
+ id: proposal.id,
7758
+ kind: proposal.kind,
7759
+ name: proposal.content.name,
7760
+ decidedBy: proposal.decision?.decidedBy ?? "(unknown)"
7761
+ };
7762
+ if (proposal.targetSkill) data.targetSkill = proposal.targetSkill;
7763
+ emit3(bus, "proposal.approved", data);
7764
+ }
7765
+ function emitProposalRejected(bus, proposal) {
7766
+ const data = {
7767
+ id: proposal.id,
7768
+ kind: proposal.kind,
7769
+ name: proposal.content.name,
7770
+ decidedBy: proposal.decision?.decidedBy ?? "(unknown)",
7771
+ reason: proposal.decision?.reason ?? "(no reason given)"
7772
+ };
7773
+ emit3(bus, "proposal.rejected", data);
7774
+ }
7775
+
7776
+ // src/server/routes/v1/proposals.ts
7777
+ var LIST_RE = /^\/api\/v1\/proposals(?:\?.*)?$/;
7778
+ var SINGLE_RE = /^\/api\/v1\/proposals\/([^/?]+)(?:\?.*)?$/;
7779
+ var RUN_GATE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/run-gate(?:\?.*)?$/;
7780
+ var APPROVE_RE = /^\/api\/v1\/proposals\/([^/?]+)\/approve(?:\?.*)?$/;
7781
+ var REJECT_RE = /^\/api\/v1\/proposals\/([^/?]+)\/reject(?:\?.*)?$/;
7782
+ var ProposalStatusValues = [
7783
+ "open",
7784
+ "gate-running",
7785
+ "gate-failed",
7786
+ "approved",
7787
+ "rejected"
7788
+ ];
7789
+ var RejectBody = z13.object({
7790
+ reason: z13.string().min(1).max(280)
7791
+ });
7792
+ function sendJSON8(res, status, body) {
7793
+ res.writeHead(status, { "Content-Type": "application/json" });
7794
+ res.end(JSON.stringify(body));
7795
+ }
7796
+ function getDecidedBy(req, deps) {
7797
+ if (deps.decidedByResolver) return deps.decidedByResolver(req);
7798
+ const token = req._authToken;
7799
+ return token?.id ?? "unknown";
7800
+ }
7801
+ function parseStatusFromQuery(url) {
7802
+ const queryIdx = url.indexOf("?");
7803
+ if (queryIdx === -1) return void 0;
7804
+ const params = new URLSearchParams(url.slice(queryIdx + 1));
7805
+ const raw = params.get("status");
7806
+ if (!raw) return void 0;
7807
+ if (raw === "all") return "all";
7808
+ if (ProposalStatusValues.includes(raw)) return raw;
7809
+ return void 0;
7810
+ }
7811
+ async function handleList(req, res, deps) {
7812
+ const url = req.url ?? "";
7813
+ const status = parseStatusFromQuery(url);
7814
+ const proposals = await listProposals(deps.projectPath, status ? { status } : {});
7815
+ sendJSON8(res, 200, proposals);
7816
+ }
7817
+ async function handleGet(res, deps, id) {
7818
+ const proposal = await getProposal3(deps.projectPath, id);
7819
+ if (!proposal) {
7820
+ sendJSON8(res, 404, { error: "Proposal not found" });
7821
+ return;
7822
+ }
7823
+ sendJSON8(res, 200, proposal);
7824
+ }
7825
+ async function handleRunGate(res, deps, id) {
7826
+ try {
7827
+ const result = await runGate(deps.projectPath, id);
7828
+ sendJSON8(res, 200, result);
7829
+ } catch (err) {
7830
+ if (err instanceof ProposalNotFoundError3) {
7831
+ sendJSON8(res, 404, { error: err.message });
7832
+ return;
7833
+ }
7834
+ if (err instanceof GateRunError) {
7835
+ sendJSON8(res, 409, { error: err.message });
7836
+ return;
7837
+ }
7838
+ sendJSON8(res, 500, {
7839
+ error: "gate run failed",
7840
+ detail: err instanceof Error ? err.message : "unknown"
7841
+ });
7842
+ }
7843
+ }
7844
+ async function handleApprove(req, res, deps, id) {
7845
+ const decidedBy = getDecidedBy(req, deps);
7846
+ try {
7847
+ const result = await promote(deps.projectPath, id, decidedBy);
7848
+ const proposal = await getProposal3(deps.projectPath, id);
7849
+ if (proposal) emitProposalApproved(deps.bus, proposal);
7850
+ sendJSON8(res, 200, { promotion: result, proposal });
7851
+ } catch (err) {
7852
+ if (err instanceof ProposalNotFoundError3) {
7853
+ sendJSON8(res, 404, { error: err.message });
7854
+ return;
7855
+ }
7856
+ if (err instanceof GateNotReadyError) {
7857
+ sendJSON8(res, 409, { error: err.message });
7858
+ return;
7859
+ }
7860
+ if (err instanceof PromotionError) {
7861
+ sendJSON8(res, 422, { error: err.message });
7862
+ return;
7863
+ }
7864
+ sendJSON8(res, 500, {
7865
+ error: "approve failed",
7866
+ detail: err instanceof Error ? err.message : "unknown"
7867
+ });
7868
+ }
7869
+ }
7870
+ async function handleReject(req, res, deps, id) {
7871
+ let raw;
7872
+ try {
7873
+ raw = await readBody(req);
7874
+ } catch (err) {
7875
+ sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
7876
+ return;
7877
+ }
7878
+ let json;
7879
+ try {
7880
+ json = raw.length > 0 ? JSON.parse(raw) : {};
7881
+ } catch {
7882
+ sendJSON8(res, 400, { error: "Invalid JSON body" });
7883
+ return;
7884
+ }
7885
+ const parsed = RejectBody.safeParse(json);
7886
+ if (!parsed.success) {
7887
+ sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
7888
+ return;
7889
+ }
7890
+ const proposal = await getProposal3(deps.projectPath, id);
7891
+ if (!proposal) {
7892
+ sendJSON8(res, 404, { error: "Proposal not found" });
7893
+ return;
7894
+ }
7895
+ if (proposal.status === "approved" || proposal.status === "rejected") {
7896
+ sendJSON8(res, 409, {
7897
+ error: `proposal already ${proposal.status}; cannot reject`
7898
+ });
7899
+ return;
7900
+ }
7901
+ const decidedBy = getDecidedBy(req, deps);
7902
+ const updated = await updateProposal3(deps.projectPath, id, {
7903
+ status: "rejected",
7904
+ decision: {
7905
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
7906
+ decidedBy,
7907
+ action: "rejected",
7908
+ reason: parsed.data.reason
7909
+ }
7910
+ });
7911
+ emitProposalRejected(deps.bus, updated);
7912
+ sendJSON8(res, 200, updated);
7913
+ }
7914
+ async function handleEdit(req, res, deps, id) {
7915
+ let raw;
7916
+ try {
7917
+ raw = await readBody(req);
7918
+ } catch (err) {
7919
+ sendJSON8(res, 413, { error: err instanceof Error ? err.message : "Body too large" });
7920
+ return;
7921
+ }
7922
+ let json;
7923
+ try {
7924
+ json = JSON.parse(raw);
7925
+ } catch {
7926
+ sendJSON8(res, 400, { error: "Invalid JSON body" });
7927
+ return;
7928
+ }
7929
+ const parsed = EditProposalInputSchema.safeParse(json);
7930
+ if (!parsed.success) {
7931
+ sendJSON8(res, 400, { error: "Invalid body", issues: parsed.error.issues });
7932
+ return;
7933
+ }
7934
+ const existing = await getProposal3(deps.projectPath, id);
7935
+ if (!existing) {
7936
+ sendJSON8(res, 404, { error: "Proposal not found" });
7937
+ return;
7938
+ }
7939
+ if (existing.status === "approved" || existing.status === "rejected") {
7940
+ sendJSON8(res, 409, {
7941
+ error: `proposal already ${existing.status}; cannot edit`
7942
+ });
7943
+ return;
7944
+ }
7945
+ const mergedContent = {
7946
+ ...existing.content,
7947
+ ...parsed.data.content,
7948
+ name: parsed.data.content.name ?? existing.content.name,
7949
+ description: parsed.data.content.description ?? existing.content.description
7950
+ };
7951
+ try {
7952
+ const updated = await updateProposal3(deps.projectPath, id, {
7953
+ content: mergedContent,
7954
+ status: "open",
7955
+ gate: void 0
7956
+ });
7957
+ sendJSON8(res, 200, updated);
7958
+ } catch (err) {
7959
+ sendJSON8(res, 422, {
7960
+ error: "edit failed",
7961
+ detail: err instanceof Error ? err.message : "unknown"
7962
+ });
7963
+ }
7964
+ }
7965
+ function handleV1ProposalsRoute(req, res, deps) {
7966
+ const url = req.url ?? "";
7967
+ const method = req.method ?? "GET";
7968
+ if (method === "GET" && LIST_RE.test(url)) {
7969
+ void handleList(req, res, deps);
7970
+ return true;
7971
+ }
7972
+ const runGateMatch = method === "POST" ? RUN_GATE_RE.exec(url) : null;
7973
+ if (runGateMatch) {
7974
+ void handleRunGate(res, deps, runGateMatch[1]);
7975
+ return true;
7976
+ }
7977
+ const approveMatch = method === "POST" ? APPROVE_RE.exec(url) : null;
7978
+ if (approveMatch) {
7979
+ void handleApprove(req, res, deps, approveMatch[1]);
7980
+ return true;
7981
+ }
7982
+ const rejectMatch = method === "POST" ? REJECT_RE.exec(url) : null;
7983
+ if (rejectMatch) {
7984
+ void handleReject(req, res, deps, rejectMatch[1]);
7985
+ return true;
7986
+ }
7987
+ if (method === "PATCH") {
7988
+ const m = SINGLE_RE.exec(url);
7989
+ if (m) {
7990
+ void handleEdit(req, res, deps, m[1]);
7991
+ return true;
7992
+ }
7993
+ }
7994
+ if (method === "GET") {
7995
+ const m = SINGLE_RE.exec(url);
7996
+ if (m) {
7997
+ void handleGet(res, deps, m[1]);
7998
+ return true;
7999
+ }
8000
+ }
8001
+ return false;
8002
+ }
8003
+
8004
+ // src/server/routes/sessions.ts
8005
+ import * as fs12 from "fs/promises";
8006
+ import * as path12 from "path";
8007
+ import { z as z14 } from "zod";
8008
+ var SessionCreateSchema = z14.object({
8009
+ sessionId: z14.string().min(1)
7469
8010
  }).passthrough();
7470
8011
  var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7471
8012
  function isSafeId(id) {
7472
- return UUID_RE2.test(id) || path11.basename(id) === id && !id.includes("..");
8013
+ return UUID_RE2.test(id) || path12.basename(id) === id && !id.includes("..");
7473
8014
  }
7474
8015
  function jsonResponse(res, status, data) {
7475
8016
  res.writeHead(status, { "Content-Type": "application/json" });
7476
8017
  res.end(JSON.stringify(data));
7477
8018
  }
7478
8019
  function extractSessionId(url) {
7479
- const segments = new URL(url, "http://localhost").pathname.split(path11.posix.sep);
8020
+ const segments = new URL(url, "http://localhost").pathname.split(path12.posix.sep);
7480
8021
  const id = segments.pop();
7481
8022
  return id && id !== "sessions" ? id : null;
7482
8023
  }
7483
- async function handleList(res, sessionsDir) {
8024
+ async function handleList2(res, sessionsDir) {
7484
8025
  try {
7485
- const entries = await fs11.readdir(sessionsDir, { withFileTypes: true });
8026
+ const entries = await fs12.readdir(sessionsDir, { withFileTypes: true });
7486
8027
  const sessions = [];
7487
8028
  for (const entry of entries) {
7488
8029
  if (!entry.isDirectory()) continue;
7489
8030
  try {
7490
- const content = await fs11.readFile(
7491
- path11.join(sessionsDir, entry.name, "session.json"),
8031
+ const content = await fs12.readFile(
8032
+ path12.join(sessionsDir, entry.name, "session.json"),
7492
8033
  "utf-8"
7493
8034
  );
7494
8035
  sessions.push(JSON.parse(content));
@@ -7507,13 +8048,13 @@ async function handleList(res, sessionsDir) {
7507
8048
  jsonResponse(res, 500, { error: "Failed to list sessions" });
7508
8049
  }
7509
8050
  }
7510
- async function handleGet(res, id, sessionsDir) {
8051
+ async function handleGet2(res, id, sessionsDir) {
7511
8052
  if (!isSafeId(id)) {
7512
8053
  jsonResponse(res, 400, { error: "Invalid sessionId" });
7513
8054
  return;
7514
8055
  }
7515
8056
  try {
7516
- const content = await fs11.readFile(path11.join(sessionsDir, id, "session.json"), "utf-8");
8057
+ const content = await fs12.readFile(path12.join(sessionsDir, id, "session.json"), "utf-8");
7517
8058
  jsonResponse(res, 200, JSON.parse(content));
7518
8059
  } catch (err) {
7519
8060
  if (err.code === "ENOENT") {
@@ -7536,9 +8077,9 @@ async function handleCreate(req, res, sessionsDir) {
7536
8077
  jsonResponse(res, 400, { error: "Invalid sessionId" });
7537
8078
  return;
7538
8079
  }
7539
- const sessionDir = path11.join(sessionsDir, session.sessionId);
7540
- await fs11.mkdir(sessionDir, { recursive: true });
7541
- await fs11.writeFile(path11.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
8080
+ const sessionDir = path12.join(sessionsDir, session.sessionId);
8081
+ await fs12.mkdir(sessionDir, { recursive: true });
8082
+ await fs12.writeFile(path12.join(sessionDir, "session.json"), JSON.stringify(session, null, 2));
7542
8083
  jsonResponse(res, 200, { ok: true });
7543
8084
  } catch {
7544
8085
  jsonResponse(res, 500, { error: "Failed to save session" });
@@ -7552,10 +8093,10 @@ async function handleUpdate(req, res, url, sessionsDir) {
7552
8093
  return;
7553
8094
  }
7554
8095
  const body = await readBody(req);
7555
- const updates = z13.record(z13.unknown()).parse(JSON.parse(body));
7556
- const sessionFilePath = path11.join(sessionsDir, id, "session.json");
7557
- const current = JSON.parse(await fs11.readFile(sessionFilePath, "utf-8"));
7558
- await fs11.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
8096
+ const updates = z14.record(z14.unknown()).parse(JSON.parse(body));
8097
+ const sessionFilePath = path12.join(sessionsDir, id, "session.json");
8098
+ const current = JSON.parse(await fs12.readFile(sessionFilePath, "utf-8"));
8099
+ await fs12.writeFile(sessionFilePath, JSON.stringify({ ...current, ...updates }, null, 2));
7559
8100
  jsonResponse(res, 200, { ok: true });
7560
8101
  } catch {
7561
8102
  jsonResponse(res, 500, { error: "Failed to update session" });
@@ -7568,7 +8109,7 @@ async function handleDelete(res, url, sessionsDir) {
7568
8109
  jsonResponse(res, 400, { error: "Missing or invalid sessionId" });
7569
8110
  return;
7570
8111
  }
7571
- await fs11.rm(path11.join(sessionsDir, id), { recursive: true, force: true });
8112
+ await fs12.rm(path12.join(sessionsDir, id), { recursive: true, force: true });
7572
8113
  jsonResponse(res, 200, { ok: true });
7573
8114
  } catch {
7574
8115
  jsonResponse(res, 500, { error: "Failed to delete session" });
@@ -7581,8 +8122,8 @@ function handleSessionsRoute(req, res, sessionsDir) {
7581
8122
  switch (method) {
7582
8123
  case "GET": {
7583
8124
  const id = extractSessionId(url);
7584
- if (id) void handleGet(res, id, sessionsDir);
7585
- else void handleList(res, sessionsDir);
8125
+ if (id) void handleGet2(res, id, sessionsDir);
8126
+ else void handleList2(res, sessionsDir);
7586
8127
  return true;
7587
8128
  }
7588
8129
  case "POST":
@@ -7672,20 +8213,20 @@ function handleStreamsRoute(req, res, recorder) {
7672
8213
  }
7673
8214
 
7674
8215
  // src/server/routes/auth.ts
7675
- import { z as z14 } from "zod";
8216
+ import { z as z15 } from "zod";
7676
8217
  import {
7677
8218
  TokenScopeSchema,
7678
8219
  BridgeKindSchema,
7679
8220
  AuthTokenPublicSchema
7680
8221
  } from "@harness-engineering/types";
7681
- var CreateBodySchema = z14.object({
7682
- name: z14.string().min(1).max(100),
7683
- scopes: z14.array(TokenScopeSchema).min(1),
8222
+ var CreateBodySchema = z15.object({
8223
+ name: z15.string().min(1).max(100),
8224
+ scopes: z15.array(TokenScopeSchema).min(1),
7684
8225
  bridgeKind: BridgeKindSchema.optional(),
7685
- tenantId: z14.string().optional(),
7686
- expiresAt: z14.string().datetime().optional()
8226
+ tenantId: z15.string().optional(),
8227
+ expiresAt: z15.string().datetime().optional()
7687
8228
  });
7688
- function sendJSON8(res, status, body) {
8229
+ function sendJSON9(res, status, body) {
7689
8230
  res.writeHead(status, { "Content-Type": "application/json" });
7690
8231
  res.end(JSON.stringify(body));
7691
8232
  }
@@ -7695,19 +8236,19 @@ async function handlePost(req, res, store) {
7695
8236
  raw = await readBody(req);
7696
8237
  } catch (err) {
7697
8238
  const msg = err instanceof Error ? err.message : "Failed to read body";
7698
- sendJSON8(res, 413, { error: msg });
8239
+ sendJSON9(res, 413, { error: msg });
7699
8240
  return;
7700
8241
  }
7701
8242
  let json;
7702
8243
  try {
7703
8244
  json = JSON.parse(raw);
7704
8245
  } catch {
7705
- sendJSON8(res, 400, { error: "Invalid JSON body" });
8246
+ sendJSON9(res, 400, { error: "Invalid JSON body" });
7706
8247
  return;
7707
8248
  }
7708
8249
  const parsed = CreateBodySchema.safeParse(json);
7709
8250
  if (!parsed.success) {
7710
- sendJSON8(res, 422, { error: "Invalid body", issues: parsed.error.issues });
8251
+ sendJSON9(res, 422, { error: "Invalid body", issues: parsed.error.issues });
7711
8252
  return;
7712
8253
  }
7713
8254
  try {
@@ -7720,37 +8261,37 @@ async function handlePost(req, res, store) {
7720
8261
  if (parsed.data.expiresAt !== void 0) input.expiresAt = parsed.data.expiresAt;
7721
8262
  const result = await store.create(input);
7722
8263
  const publicRecord = AuthTokenPublicSchema.parse(result.record);
7723
- sendJSON8(res, 200, {
8264
+ sendJSON9(res, 200, {
7724
8265
  ...publicRecord,
7725
8266
  token: result.token
7726
8267
  });
7727
8268
  } catch (err) {
7728
8269
  const msg = err instanceof Error ? err.message : "Failed to create token";
7729
8270
  if (msg.includes("already exists")) {
7730
- sendJSON8(res, 409, { error: msg });
8271
+ sendJSON9(res, 409, { error: msg });
7731
8272
  return;
7732
8273
  }
7733
- sendJSON8(res, 500, { error: "Internal error creating token" });
8274
+ sendJSON9(res, 500, { error: "Internal error creating token" });
7734
8275
  }
7735
8276
  }
7736
- async function handleList2(res, store) {
8277
+ async function handleList3(res, store) {
7737
8278
  try {
7738
8279
  const list = await store.list();
7739
- sendJSON8(res, 200, list);
8280
+ sendJSON9(res, 200, list);
7740
8281
  } catch {
7741
- sendJSON8(res, 500, { error: "Internal error listing tokens" });
8282
+ sendJSON9(res, 500, { error: "Internal error listing tokens" });
7742
8283
  }
7743
8284
  }
7744
8285
  async function handleDelete2(res, store, id) {
7745
8286
  try {
7746
8287
  const ok = await store.revoke(id);
7747
8288
  if (!ok) {
7748
- sendJSON8(res, 404, { error: "Token not found" });
8289
+ sendJSON9(res, 404, { error: "Token not found" });
7749
8290
  return;
7750
8291
  }
7751
- sendJSON8(res, 200, { deleted: true });
8292
+ sendJSON9(res, 200, { deleted: true });
7752
8293
  } catch {
7753
- sendJSON8(res, 500, { error: "Internal error revoking token" });
8294
+ sendJSON9(res, 500, { error: "Internal error revoking token" });
7754
8295
  }
7755
8296
  }
7756
8297
  var DELETE_PATH_RE2 = /^\/api\/v1\/auth\/tokens\/([^/?]+)(?:\?.*)?$/;
@@ -7764,7 +8305,7 @@ function handleAuthRoute(req, res, store) {
7764
8305
  return true;
7765
8306
  }
7766
8307
  if (method === "GET" && pathname === "/api/v1/auth/tokens") {
7767
- void handleList2(res, store);
8308
+ void handleList3(res, store);
7768
8309
  return true;
7769
8310
  }
7770
8311
  if (method === "DELETE") {
@@ -7775,12 +8316,12 @@ function handleAuthRoute(req, res, store) {
7775
8316
  return true;
7776
8317
  }
7777
8318
  }
7778
- sendJSON8(res, 405, { error: "Method not allowed" });
8319
+ sendJSON9(res, 405, { error: "Method not allowed" });
7779
8320
  return true;
7780
8321
  }
7781
8322
 
7782
8323
  // src/server/routes/local-model.ts
7783
- function sendJSON9(res, status, body) {
8324
+ function sendJSON10(res, status, body) {
7784
8325
  res.writeHead(status, { "Content-Type": "application/json" });
7785
8326
  res.end(JSON.stringify(body));
7786
8327
  }
@@ -7788,36 +8329,36 @@ function handleLocalModelRoute(req, res, getStatus) {
7788
8329
  const { method, url } = req;
7789
8330
  if (url !== "/api/v1/local-model/status") return false;
7790
8331
  if (method !== "GET") {
7791
- sendJSON9(res, 405, { error: "Method not allowed" });
8332
+ sendJSON10(res, 405, { error: "Method not allowed" });
7792
8333
  return true;
7793
8334
  }
7794
8335
  if (!getStatus) {
7795
- sendJSON9(res, 503, { error: "Local backend not configured" });
8336
+ sendJSON10(res, 503, { error: "Local backend not configured" });
7796
8337
  return true;
7797
8338
  }
7798
8339
  const status = getStatus();
7799
8340
  if (!status) {
7800
- sendJSON9(res, 503, { error: "Local backend not configured" });
8341
+ sendJSON10(res, 503, { error: "Local backend not configured" });
7801
8342
  return true;
7802
8343
  }
7803
- sendJSON9(res, 200, status);
8344
+ sendJSON10(res, 200, status);
7804
8345
  return true;
7805
8346
  }
7806
8347
  function handleLocalModelsRoute(req, res, getStatuses) {
7807
8348
  const { method, url } = req;
7808
8349
  if (url !== "/api/v1/local-models/status") return false;
7809
8350
  if (method !== "GET") {
7810
- sendJSON9(res, 405, { error: "Method not allowed" });
8351
+ sendJSON10(res, 405, { error: "Method not allowed" });
7811
8352
  return true;
7812
8353
  }
7813
8354
  const statuses = getStatuses ? getStatuses() : [];
7814
- sendJSON9(res, 200, statuses);
8355
+ sendJSON10(res, 200, statuses);
7815
8356
  return true;
7816
8357
  }
7817
8358
 
7818
8359
  // src/server/static.ts
7819
- import * as fs12 from "fs";
7820
- import * as path12 from "path";
8360
+ import * as fs13 from "fs";
8361
+ import * as path13 from "path";
7821
8362
  var MIME_TYPES = {
7822
8363
  ".html": "text/html; charset=utf-8",
7823
8364
  ".js": "application/javascript; charset=utf-8",
@@ -7837,29 +8378,29 @@ var MIME_TYPES = {
7837
8378
  function handleStaticFile(req, res, dashboardDir) {
7838
8379
  const { method, url } = req;
7839
8380
  if (method !== "GET") return false;
7840
- const apiPrefix = path12.posix.join(path12.posix.sep, "api", path12.posix.sep);
7841
- const wsPath = path12.posix.join(path12.posix.sep, "ws");
8381
+ const apiPrefix = path13.posix.join(path13.posix.sep, "api", path13.posix.sep);
8382
+ const wsPath = path13.posix.join(path13.posix.sep, "ws");
7842
8383
  if (url?.startsWith(apiPrefix) || url === wsPath) return false;
7843
8384
  const urlPath = new URL(url ?? "/", "http://localhost").pathname;
7844
- const requestedPath = path12.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
7845
- const resolved = path12.resolve(requestedPath);
7846
- if (!resolved.startsWith(path12.resolve(dashboardDir))) {
7847
- return serveFile(path12.join(dashboardDir, "index.html"), res);
8385
+ const requestedPath = path13.join(dashboardDir, urlPath === "/" ? "index.html" : urlPath);
8386
+ const resolved = path13.resolve(requestedPath);
8387
+ if (!resolved.startsWith(path13.resolve(dashboardDir))) {
8388
+ return serveFile(path13.join(dashboardDir, "index.html"), res);
7848
8389
  }
7849
- if (fs12.existsSync(resolved) && fs12.statSync(resolved).isFile()) {
8390
+ if (fs13.existsSync(resolved) && fs13.statSync(resolved).isFile()) {
7850
8391
  return serveFile(resolved, res);
7851
8392
  }
7852
- const indexPath = path12.join(dashboardDir, "index.html");
7853
- if (fs12.existsSync(indexPath)) {
8393
+ const indexPath = path13.join(dashboardDir, "index.html");
8394
+ if (fs13.existsSync(indexPath)) {
7854
8395
  return serveFile(indexPath, res);
7855
8396
  }
7856
8397
  return false;
7857
8398
  }
7858
8399
  function serveFile(filePath, res) {
7859
- const ext = path12.extname(filePath).toLowerCase();
8400
+ const ext = path13.extname(filePath).toLowerCase();
7860
8401
  const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
7861
8402
  try {
7862
- const content = fs12.readFileSync(filePath);
8403
+ const content = fs13.readFileSync(filePath);
7863
8404
  res.writeHead(200, { "Content-Type": contentType });
7864
8405
  res.end(content);
7865
8406
  return true;
@@ -7869,8 +8410,8 @@ function serveFile(filePath, res) {
7869
8410
  }
7870
8411
 
7871
8412
  // src/server/plan-watcher.ts
7872
- import * as fs13 from "fs";
7873
- import * as path13 from "path";
8413
+ import * as fs14 from "fs";
8414
+ import * as path14 from "path";
7874
8415
  var PlanWatcher = class {
7875
8416
  plansDir;
7876
8417
  queue;
@@ -7884,11 +8425,11 @@ var PlanWatcher = class {
7884
8425
  * Creates the directory if it does not exist.
7885
8426
  */
7886
8427
  start() {
7887
- fs13.mkdirSync(this.plansDir, { recursive: true });
7888
- this.watcher = fs13.watch(this.plansDir, (eventType, filename) => {
8428
+ fs14.mkdirSync(this.plansDir, { recursive: true });
8429
+ this.watcher = fs14.watch(this.plansDir, (eventType, filename) => {
7889
8430
  if (eventType === "rename" && filename && filename.endsWith(".md")) {
7890
- const filePath = path13.join(this.plansDir, filename);
7891
- if (fs13.existsSync(filePath)) {
8431
+ const filePath = path14.join(this.plansDir, filename);
8432
+ if (fs14.existsSync(filePath)) {
7892
8433
  void this.handleNewPlan(filename);
7893
8434
  }
7894
8435
  }
@@ -7941,8 +8482,8 @@ function parseToken(raw) {
7941
8482
  return { id: raw.slice(0, dot), secret: raw.slice(dot + 1) };
7942
8483
  }
7943
8484
  var TokenStore = class {
7944
- constructor(path19) {
7945
- this.path = path19;
8485
+ constructor(path22) {
8486
+ this.path = path22;
7946
8487
  }
7947
8488
  path;
7948
8489
  cache = null;
@@ -8049,8 +8590,8 @@ import { appendFile, mkdir as mkdir8 } from "fs/promises";
8049
8590
  import { dirname as dirname5 } from "path";
8050
8591
  import { AuthAuditEntrySchema } from "@harness-engineering/types";
8051
8592
  var AuditLogger = class {
8052
- constructor(path19, opts = {}) {
8053
- this.path = path19;
8593
+ constructor(path22, opts = {}) {
8594
+ this.path = path22;
8054
8595
  this.opts = opts;
8055
8596
  }
8056
8597
  path;
@@ -8134,6 +8675,43 @@ var V1_BRIDGE_ROUTES = [
8134
8675
  scope: "subscribe-webhook",
8135
8676
  description: "Webhook delivery queue depth + DLQ stats."
8136
8677
  },
8678
+ // Hermes Phase 4 — skill proposal review queue.
8679
+ {
8680
+ method: "GET",
8681
+ pattern: /^\/api\/v1\/proposals(?:\?.*)?$/,
8682
+ scope: "read-status",
8683
+ description: "List skill proposals (open + decided)."
8684
+ },
8685
+ {
8686
+ method: "GET",
8687
+ pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
8688
+ scope: "read-status",
8689
+ description: "Get a single skill proposal."
8690
+ },
8691
+ {
8692
+ method: "POST",
8693
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/run-gate(?:\?.*)?$/,
8694
+ scope: "manage-proposals",
8695
+ description: "Run the soundness-review gate against a proposal."
8696
+ },
8697
+ {
8698
+ method: "POST",
8699
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/approve(?:\?.*)?$/,
8700
+ scope: "manage-proposals",
8701
+ description: "Approve a proposal \u2014 promotes the skill into the catalog."
8702
+ },
8703
+ {
8704
+ method: "POST",
8705
+ pattern: /^\/api\/v1\/proposals\/[^/]+\/reject(?:\?.*)?$/,
8706
+ scope: "manage-proposals",
8707
+ description: "Reject a proposal with a one-line reason."
8708
+ },
8709
+ {
8710
+ method: "PATCH",
8711
+ pattern: /^\/api\/v1\/proposals\/[^/]+(?:\?.*)?$/,
8712
+ scope: "manage-proposals",
8713
+ description: "Edit proposal content (resets gate to not-run)."
8714
+ },
8137
8715
  // ── Phase 5 bridge primitives ──
8138
8716
  {
8139
8717
  method: "GET",
@@ -8145,9 +8723,9 @@ var V1_BRIDGE_ROUTES = [
8145
8723
  function isV1Bridge(method, url) {
8146
8724
  return V1_BRIDGE_ROUTES.some((r) => r.method === method && r.pattern.test(url));
8147
8725
  }
8148
- function requiredBridgeScope(method, path19) {
8726
+ function requiredBridgeScope(method, path22) {
8149
8727
  for (const r of V1_BRIDGE_ROUTES) {
8150
- if (r.method === method && r.pattern.test(path19)) return r.scope;
8728
+ if (r.method === method && r.pattern.test(path22)) return r.scope;
8151
8729
  }
8152
8730
  return null;
8153
8731
  }
@@ -8157,24 +8735,24 @@ function hasScope(held, required) {
8157
8735
  if (held.includes("admin")) return true;
8158
8736
  return held.includes(required);
8159
8737
  }
8160
- function requiredScopeForRoute(method, path19) {
8161
- const bridgeScope = requiredBridgeScope(method, path19);
8738
+ function requiredScopeForRoute(method, path22) {
8739
+ const bridgeScope = requiredBridgeScope(method, path22);
8162
8740
  if (bridgeScope) return bridgeScope;
8163
- if (path19 === "/api/v1/auth/token" && method === "POST") return "admin";
8164
- if (path19 === "/api/v1/auth/tokens" && method === "GET") return "admin";
8165
- if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path19) && method === "DELETE") return "admin";
8166
- if ((path19 === "/api/state" || path19 === "/api/v1/state") && method === "GET") return "read-status";
8167
- if (path19.startsWith("/api/interactions")) return "resolve-interaction";
8168
- if (path19.startsWith("/api/plans")) return "read-status";
8169
- if (path19.startsWith("/api/analyze") || path19.startsWith("/api/analyses")) return "read-status";
8170
- if (path19.startsWith("/api/roadmap-actions")) return "modify-roadmap";
8171
- if (path19.startsWith("/api/dispatch-actions")) return "trigger-job";
8172
- if (path19.startsWith("/api/local-model") || path19.startsWith("/api/local-models"))
8741
+ if (path22 === "/api/v1/auth/token" && method === "POST") return "admin";
8742
+ if (path22 === "/api/v1/auth/tokens" && method === "GET") return "admin";
8743
+ if (/^\/api\/v1\/auth\/tokens\/[^/]+$/.test(path22) && method === "DELETE") return "admin";
8744
+ if ((path22 === "/api/state" || path22 === "/api/v1/state") && method === "GET") return "read-status";
8745
+ if (path22.startsWith("/api/interactions")) return "resolve-interaction";
8746
+ if (path22.startsWith("/api/plans")) return "read-status";
8747
+ if (path22.startsWith("/api/analyze") || path22.startsWith("/api/analyses")) return "read-status";
8748
+ if (path22.startsWith("/api/roadmap-actions")) return "modify-roadmap";
8749
+ if (path22.startsWith("/api/dispatch-actions")) return "trigger-job";
8750
+ if (path22.startsWith("/api/local-model") || path22.startsWith("/api/local-models"))
8173
8751
  return "read-status";
8174
- if (path19.startsWith("/api/maintenance")) return "trigger-job";
8175
- if (path19.startsWith("/api/streams")) return "read-status";
8176
- if (path19.startsWith("/api/sessions")) return "read-status";
8177
- if (path19.startsWith("/api/chat-proxy")) return "trigger-job";
8752
+ if (path22.startsWith("/api/maintenance")) return "trigger-job";
8753
+ if (path22.startsWith("/api/streams")) return "read-status";
8754
+ if (path22.startsWith("/api/sessions")) return "read-status";
8755
+ if (path22.startsWith("/api/chat-proxy")) return "trigger-job";
8178
8756
  return null;
8179
8757
  }
8180
8758
 
@@ -8228,6 +8806,11 @@ var OrchestratorServer = class {
8228
8806
  roadmapPath;
8229
8807
  dispatchAdHoc;
8230
8808
  sessionsDir;
8809
+ /**
8810
+ * Project root used by file-backed routes (Phase 4 proposals at
8811
+ * `.harness/proposals/`). Defaults to process.cwd().
8812
+ */
8813
+ projectPath;
8231
8814
  maintenanceDeps = null;
8232
8815
  getLocalModelStatus = null;
8233
8816
  getLocalModelStatuses = null;
@@ -8245,8 +8828,8 @@ var OrchestratorServer = class {
8245
8828
  this.orchestrator = orchestrator;
8246
8829
  this.port = port;
8247
8830
  this.initDependencies(deps);
8248
- const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path14.resolve(".harness", "tokens.json");
8249
- const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path14.resolve(".harness", "audit.log");
8831
+ const tokensPath = process.env["HARNESS_TOKENS_PATH"] ?? path15.resolve(".harness", "tokens.json");
8832
+ const auditPath = process.env["HARNESS_AUDIT_PATH"] ?? path15.resolve(".harness", "audit.log");
8250
8833
  this.tokenStore = new TokenStore(tokensPath);
8251
8834
  this.auditLogger = new AuditLogger(auditPath);
8252
8835
  this.httpServer = http.createServer(this.handleRequest.bind(this));
@@ -8259,14 +8842,15 @@ var OrchestratorServer = class {
8259
8842
  }
8260
8843
  initDependencies(deps) {
8261
8844
  this.interactionQueue = deps?.interactionQueue;
8262
- this.plansDir = deps?.plansDir ?? path14.resolve("docs", "plans");
8263
- this.dashboardDir = deps?.dashboardDir ?? path14.resolve("packages", "dashboard", "dist", "client");
8845
+ this.plansDir = deps?.plansDir ?? path15.resolve("docs", "plans");
8846
+ this.dashboardDir = deps?.dashboardDir ?? path15.resolve("packages", "dashboard", "dist", "client");
8264
8847
  this.claudeCommand = deps?.claudeCommand ?? "claude";
8265
8848
  this.pipeline = deps?.pipeline ?? null;
8266
8849
  this.analysisArchive = deps?.analysisArchive;
8267
8850
  this.roadmapPath = deps?.roadmapPath ?? null;
8268
8851
  this.dispatchAdHoc = deps?.dispatchAdHoc ?? null;
8269
- this.sessionsDir = deps?.sessionsDir ?? path14.resolve(".harness", "sessions");
8852
+ this.sessionsDir = deps?.sessionsDir ?? path15.resolve(".harness", "sessions");
8853
+ this.projectPath = deps?.projectPath ?? process.cwd();
8270
8854
  this.maintenanceDeps = deps?.maintenanceDeps ?? null;
8271
8855
  this.getLocalModelStatus = deps?.getLocalModelStatus ?? null;
8272
8856
  this.getLocalModelStatuses = deps?.getLocalModelStatuses ?? null;
@@ -8437,6 +9021,15 @@ var OrchestratorServer = class {
8437
9021
  (req, res) => handleV1TelemetryRoute(req, res, {
8438
9022
  ...this.cacheMetrics ? { cacheMetrics: this.cacheMetrics } : {}
8439
9023
  }),
9024
+ // Hermes Phase 4 — skill proposal review queue. Read scopes
9025
+ // (`read-status`) and write scopes (`manage-proposals`) are enforced
9026
+ // upstream by V1_BRIDGE_ROUTES; this dispatcher only handles
9027
+ // business logic. `projectPath` defaults to process.cwd() — that is
9028
+ // where `.harness/proposals/` lives in every deployment we ship.
9029
+ (req, res) => handleV1ProposalsRoute(req, res, {
9030
+ projectPath: this.projectPath,
9031
+ bus: this.orchestrator
9032
+ }),
8440
9033
  // Chat proxy route (spawns Claude Code CLI — no API key required)
8441
9034
  (req, res) => handleChatProxyRoute(req, res, this.claudeCommand)
8442
9035
  ];
@@ -8524,11 +9117,11 @@ var OrchestratorServer = class {
8524
9117
  this.planWatcher = new PlanWatcher(this.plansDir, this.interactionQueue);
8525
9118
  this.planWatcher.start();
8526
9119
  }
8527
- return new Promise((resolve6) => {
9120
+ return new Promise((resolve7) => {
8528
9121
  const host = getBindHost();
8529
9122
  this.httpServer.listen(this.port, host, () => {
8530
9123
  console.log(`Orchestrator API listening on ${host}:${this.port}`);
8531
- resolve6();
9124
+ resolve7();
8532
9125
  });
8533
9126
  });
8534
9127
  }
@@ -8578,8 +9171,8 @@ function genSecret2() {
8578
9171
  return randomBytes4(32).toString("base64url");
8579
9172
  }
8580
9173
  var WebhookStore = class {
8581
- constructor(path19) {
8582
- this.path = path19;
9174
+ constructor(path22) {
9175
+ this.path = path22;
8583
9176
  }
8584
9177
  path;
8585
9178
  cache = null;
@@ -8970,7 +9563,12 @@ var WEBHOOK_TOPICS = [
8970
9563
  "maintenance:completed",
8971
9564
  "maintenance:error",
8972
9565
  "webhook.subscription.created",
8973
- "webhook.subscription.deleted"
9566
+ "webhook.subscription.deleted",
9567
+ // Hermes Phase 4 — skill proposal lifecycle. Subscriptions can use the
9568
+ // `proposal.*` glob pattern to receive all three.
9569
+ "proposal.created",
9570
+ "proposal.approved",
9571
+ "proposal.rejected"
8974
9572
  ];
8975
9573
  function newEventId2() {
8976
9574
  return `evt_${randomBytes6(8).toString("hex")}`;
@@ -9395,6 +9993,33 @@ var ENVELOPE_DERIVERS = {
9395
9993
  summary: data.message ?? "If you see this, your notification sink is working.",
9396
9994
  severity: "info"
9397
9995
  };
9996
+ },
9997
+ // Hermes Phase 4 — skill proposal lifecycle events.
9998
+ "proposal.created": (event) => {
9999
+ const data = asObj(event.data);
10000
+ const label = data.kind === "refinement" ? `refinement of ${data.targetSkill ?? "(unknown skill)"}` : data.name ?? "(new skill)";
10001
+ return {
10002
+ title: `New skill proposal: ${label}`,
10003
+ summary: truncate(data.justification ?? "No justification provided.", 240),
10004
+ severity: "info"
10005
+ };
10006
+ },
10007
+ "proposal.approved": (event) => {
10008
+ const data = asObj(event.data);
10009
+ const label = data.name ?? data.targetSkill ?? "(unknown skill)";
10010
+ return {
10011
+ title: `Skill proposal approved: ${label}`,
10012
+ summary: `Approved by ${data.decidedBy ?? "(unknown reviewer)"}.`,
10013
+ severity: "success"
10014
+ };
10015
+ },
10016
+ "proposal.rejected": (event) => {
10017
+ const data = asObj(event.data);
10018
+ return {
10019
+ title: "Skill proposal rejected",
10020
+ summary: truncate(data.reason ?? "No reason provided.", 240),
10021
+ severity: "warning"
10022
+ };
9398
10023
  }
9399
10024
  };
9400
10025
  function truncate(s, max) {
@@ -9439,7 +10064,11 @@ var NOTIFICATION_TOPICS = [
9439
10064
  "interaction.resolved",
9440
10065
  "maintenance:started",
9441
10066
  "maintenance:completed",
9442
- "maintenance:error"
10067
+ "maintenance:error",
10068
+ // Hermes Phase 4 — skill proposal lifecycle.
10069
+ "proposal.created",
10070
+ "proposal.approved",
10071
+ "proposal.rejected"
9443
10072
  ];
9444
10073
  function newEventId4() {
9445
10074
  return `evt_${randomBytes8(8).toString("hex")}`;
@@ -9535,8 +10164,8 @@ var StructuredLogger = class {
9535
10164
  };
9536
10165
 
9537
10166
  // src/workspace/config-scanner.ts
9538
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
9539
- import { join as join13, relative } from "path";
10167
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
10168
+ import { join as join14, relative } from "path";
9540
10169
  import {
9541
10170
  scanForInjection,
9542
10171
  SecurityScanner,
@@ -9560,10 +10189,10 @@ function adjustFindingSeverity(findings) {
9560
10189
  });
9561
10190
  }
9562
10191
  async function scanSingleFile(filePath, targetDir, scanner) {
9563
- if (!existsSync4(filePath)) return null;
10192
+ if (!existsSync5(filePath)) return null;
9564
10193
  let content;
9565
10194
  try {
9566
- content = readFileSync4(filePath, "utf8");
10195
+ content = readFileSync5(filePath, "utf8");
9567
10196
  } catch {
9568
10197
  return null;
9569
10198
  }
@@ -9582,7 +10211,7 @@ async function scanWorkspaceConfig(workspacePath) {
9582
10211
  const scanner = new SecurityScanner(parseSecurityConfig({}));
9583
10212
  const results = [];
9584
10213
  for (const configFile of CONFIG_FILES) {
9585
- const result = await scanSingleFile(join13(workspacePath, configFile), workspacePath, scanner);
10214
+ const result = await scanSingleFile(join14(workspacePath, configFile), workspacePath, scanner);
9586
10215
  if (result) results.push(result);
9587
10216
  }
9588
10217
  return { exitCode: computeScanExitCode(results), results };
@@ -9768,6 +10397,19 @@ var BUILT_IN_TASKS = [
9768
10397
  schedule: "*/15 * * * *",
9769
10398
  branch: null,
9770
10399
  checkCommand: ["harness", "sync-main", "--json"]
10400
+ },
10401
+ // Hermes Phase 4 — one-shot backfill that stamps `provenance: user-authored`
10402
+ // on every existing catalog skill. Schedule is Feb 31 (a date that never
10403
+ // exists) so the cron loop never fires it automatically; operators trigger
10404
+ // it once via the dashboard "Run now" button or `harness backfill-skill-
10405
+ // provenance` after upgrading to Phase 4.
10406
+ {
10407
+ id: "proposal-provenance-backfill",
10408
+ type: "housekeeping",
10409
+ description: "Backfill provenance: user-authored on every existing skill (one-shot, idempotent)",
10410
+ schedule: "0 0 31 2 *",
10411
+ branch: null,
10412
+ checkCommand: ["backfill-skill-provenance"]
9771
10413
  }
9772
10414
  ];
9773
10415
 
@@ -9860,26 +10502,51 @@ var MaintenanceScheduler = class {
9860
10502
  this.resolvedTasks = this.resolveTasks();
9861
10503
  }
9862
10504
  /**
9863
- * Merge built-in task definitions with config overrides.
9864
- * Tasks with `enabled: false` are filtered out.
9865
- * Schedule overrides replace the default cron expression.
10505
+ * Merge built-in task definitions with config overrides, then append
10506
+ * Hermes Phase 2 `customTasks` (also respecting `tasks.<id>.enabled`
10507
+ * overrides). Tasks with `enabled: false` are filtered out. Schedule
10508
+ * overrides replace the default cron expression.
9866
10509
  */
9867
10510
  resolveTasks() {
9868
10511
  const overrides = this.config.tasks ?? {};
9869
- return BUILT_IN_TASKS.filter((task) => {
9870
- const override = overrides[task.id];
9871
- if (override?.enabled === false) return false;
9872
- return true;
9873
- }).map((task) => {
10512
+ const customs = this.config.customTasks ?? {};
10513
+ const merged = [];
10514
+ for (const task of BUILT_IN_TASKS) {
9874
10515
  const override = overrides[task.id];
9875
- if (!override) return { ...task };
9876
- return {
10516
+ if (override?.enabled === false) continue;
10517
+ merged.push({
9877
10518
  ...task,
9878
- ...override.schedule !== void 0 && { schedule: override.schedule }
9879
- };
9880
- });
9881
- }
9882
- /** Returns the resolved (merged) task list. Useful for testing and dashboard. */
10519
+ ...override?.schedule !== void 0 && { schedule: override.schedule }
10520
+ });
10521
+ }
10522
+ for (const [id, def] of Object.entries(customs)) {
10523
+ const override = overrides[id];
10524
+ if (override?.enabled === false) continue;
10525
+ merged.push({
10526
+ id,
10527
+ type: def.type,
10528
+ description: def.description,
10529
+ schedule: override?.schedule ?? def.schedule,
10530
+ branch: def.branch,
10531
+ ...def.checkCommand !== void 0 && { checkCommand: def.checkCommand },
10532
+ ...def.checkScript !== void 0 && { checkScript: def.checkScript },
10533
+ ...def.fixSkill !== void 0 && { fixSkill: def.fixSkill },
10534
+ ...def.inlineSkills !== void 0 && { inlineSkills: def.inlineSkills },
10535
+ ...def.inlineSkillsBudgetTokens !== void 0 && {
10536
+ inlineSkillsBudgetTokens: def.inlineSkillsBudgetTokens
10537
+ },
10538
+ ...def.contextFrom !== void 0 && { contextFrom: def.contextFrom },
10539
+ ...def.contextFromMaxAgeMinutes !== void 0 && {
10540
+ contextFromMaxAgeMinutes: def.contextFromMaxAgeMinutes
10541
+ },
10542
+ ...def.outputRetention !== void 0 && { outputRetention: def.outputRetention },
10543
+ ...def.costCeiling !== void 0 && { costCeiling: def.costCeiling },
10544
+ isCustom: true
10545
+ });
10546
+ }
10547
+ return merged;
10548
+ }
10549
+ /** Returns the resolved (merged) task list. Useful for testing and dashboard. */
9883
10550
  getResolvedTasks() {
9884
10551
  return this.resolvedTasks;
9885
10552
  }
@@ -10058,19 +10725,19 @@ var SingleProcessLeaderElector = class {
10058
10725
  };
10059
10726
 
10060
10727
  // src/maintenance/reporter.ts
10061
- import * as fs14 from "fs";
10062
- import * as path15 from "path";
10063
- import { z as z15 } from "zod";
10064
- var RunResultSchema = z15.object({
10065
- taskId: z15.string(),
10066
- startedAt: z15.string(),
10067
- completedAt: z15.string(),
10068
- status: z15.enum(["success", "failure", "skipped", "no-issues"]),
10069
- findings: z15.number(),
10070
- fixed: z15.number(),
10071
- prUrl: z15.string().nullable(),
10072
- prUpdated: z15.boolean(),
10073
- error: z15.string().optional()
10728
+ import * as fs15 from "fs";
10729
+ import * as path16 from "path";
10730
+ import { z as z16 } from "zod";
10731
+ var RunResultSchema = z16.object({
10732
+ taskId: z16.string(),
10733
+ startedAt: z16.string(),
10734
+ completedAt: z16.string(),
10735
+ status: z16.enum(["success", "failure", "skipped", "no-issues"]),
10736
+ findings: z16.number(),
10737
+ fixed: z16.number(),
10738
+ prUrl: z16.string().nullable(),
10739
+ prUpdated: z16.boolean(),
10740
+ error: z16.string().optional()
10074
10741
  });
10075
10742
  var MAX_HISTORY = 500;
10076
10743
  var fallbackLogger = {
@@ -10094,10 +10761,10 @@ var MaintenanceReporter = class {
10094
10761
  */
10095
10762
  async load() {
10096
10763
  try {
10097
- await fs14.promises.mkdir(this.persistDir, { recursive: true });
10098
- const filePath = path15.join(this.persistDir, "history.json");
10099
- const data = await fs14.promises.readFile(filePath, "utf-8");
10100
- const parsed = z15.array(RunResultSchema).safeParse(JSON.parse(data));
10764
+ await fs15.promises.mkdir(this.persistDir, { recursive: true });
10765
+ const filePath = path16.join(this.persistDir, "history.json");
10766
+ const data = await fs15.promises.readFile(filePath, "utf-8");
10767
+ const parsed = z16.array(RunResultSchema).safeParse(JSON.parse(data));
10101
10768
  if (parsed.success) {
10102
10769
  this.history = parsed.data.slice(0, MAX_HISTORY);
10103
10770
  }
@@ -10130,9 +10797,9 @@ var MaintenanceReporter = class {
10130
10797
  */
10131
10798
  async persist() {
10132
10799
  try {
10133
- await fs14.promises.mkdir(this.persistDir, { recursive: true });
10134
- const filePath = path15.join(this.persistDir, "history.json");
10135
- await fs14.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10800
+ await fs15.promises.mkdir(this.persistDir, { recursive: true });
10801
+ const filePath = path16.join(this.persistDir, "history.json");
10802
+ await fs15.promises.writeFile(filePath, JSON.stringify(this.history, null, 2), "utf-8");
10136
10803
  } catch (err) {
10137
10804
  this.logger.error("MaintenanceReporter: failed to persist history", { error: String(err) });
10138
10805
  }
@@ -10148,6 +10815,9 @@ var TaskRunner = class {
10148
10815
  cwd;
10149
10816
  prManager;
10150
10817
  baseBranch;
10818
+ checkScriptRunner;
10819
+ contextResolver;
10820
+ outputStore;
10151
10821
  constructor(options) {
10152
10822
  this.config = options.config;
10153
10823
  this.checkRunner = options.checkRunner;
@@ -10156,27 +10826,49 @@ var TaskRunner = class {
10156
10826
  this.cwd = options.cwd;
10157
10827
  this.prManager = options.prManager ?? null;
10158
10828
  this.baseBranch = options.baseBranch ?? "main";
10829
+ this.checkScriptRunner = options.checkScriptRunner ?? null;
10830
+ this.contextResolver = options.contextResolver ?? null;
10831
+ this.outputStore = options.outputStore ?? null;
10159
10832
  }
10160
10833
  /**
10161
10834
  * Run a maintenance task and return the result.
10162
10835
  * Dispatches to the appropriate execution path based on task type.
10163
10836
  * Never throws -- errors are captured in the RunResult.
10837
+ *
10838
+ * @param task - Resolved task definition.
10839
+ * @param origin - Hermes Phase 2 trigger-source tag; defaults to `'cron'`
10840
+ * when called from the scheduler path.
10164
10841
  */
10165
- async run(task) {
10842
+ async run(task, origin = "cron") {
10166
10843
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10844
+ let result;
10845
+ let captured;
10167
10846
  try {
10168
10847
  switch (task.type) {
10169
- case "mechanical-ai":
10170
- return await this.runMechanicalAI(task, startedAt);
10848
+ case "mechanical-ai": {
10849
+ const out = await this.runMechanicalAI(task, startedAt);
10850
+ result = out.result;
10851
+ captured = out.captured;
10852
+ break;
10853
+ }
10171
10854
  case "pure-ai":
10172
- return await this.runPureAI(task, startedAt);
10173
- case "report-only":
10174
- return await this.runReportOnly(task, startedAt);
10175
- case "housekeeping":
10176
- return await this.runHousekeeping(task, startedAt);
10855
+ result = await this.runPureAI(task, startedAt);
10856
+ break;
10857
+ case "report-only": {
10858
+ const out = await this.runReportOnly(task, startedAt);
10859
+ result = out.result;
10860
+ captured = out.captured;
10861
+ break;
10862
+ }
10863
+ case "housekeeping": {
10864
+ const out = await this.runHousekeeping(task, startedAt);
10865
+ result = out.result;
10866
+ captured = out.captured;
10867
+ break;
10868
+ }
10177
10869
  default: {
10178
10870
  const _exhaustive = task.type;
10179
- return this.failureResult(
10871
+ result = this.failureResult(
10180
10872
  task.id,
10181
10873
  startedAt,
10182
10874
  `Unknown task type: ${String(_exhaustive)}`
@@ -10184,69 +10876,174 @@ var TaskRunner = class {
10184
10876
  }
10185
10877
  }
10186
10878
  } catch (err) {
10187
- return this.failureResult(task.id, startedAt, String(err));
10879
+ result = this.failureResult(task.id, startedAt, String(err));
10880
+ }
10881
+ result.origin = origin;
10882
+ await this.persistOutput(task, result, captured, origin);
10883
+ return result;
10884
+ }
10885
+ async persistOutput(task, result, captured, origin) {
10886
+ if (!this.outputStore) return;
10887
+ const entry = {
10888
+ taskId: result.taskId,
10889
+ startedAt: result.startedAt,
10890
+ completedAt: result.completedAt,
10891
+ status: result.status,
10892
+ findings: result.findings,
10893
+ fixed: result.fixed,
10894
+ prUrl: result.prUrl,
10895
+ prUpdated: result.prUpdated,
10896
+ origin,
10897
+ ...result.error !== void 0 && { error: result.error },
10898
+ ...result.costUsd !== void 0 && { costUsd: result.costUsd },
10899
+ ...captured?.stdout !== void 0 && { stdout: captured.stdout },
10900
+ ...captured?.stderr !== void 0 && { stderr: captured.stderr },
10901
+ ...captured?.structured !== void 0 && { structured: captured.structured },
10902
+ ...captured?.context !== void 0 && { context: captured.context }
10903
+ };
10904
+ try {
10905
+ await this.outputStore.write(task.id, entry, task.outputRetention);
10906
+ } catch {
10188
10907
  }
10189
10908
  }
10190
10909
  /**
10191
- * Mechanical-AI: run check command, dispatch AI agent only if fixable findings exist.
10910
+ * Run the check step using whichever runner the task asks for. Custom
10911
+ * tasks that declare `checkScript` go through the Hermes Phase 2
10912
+ * `CheckScriptRunner`; built-ins (and customs that use the legacy
10913
+ * `checkCommand` shape) go through the original heuristic runner.
10192
10914
  */
10193
- async runMechanicalAI(task, startedAt) {
10915
+ async runCheckStep(task) {
10916
+ if (task.checkScript) {
10917
+ if (!this.checkScriptRunner) {
10918
+ throw new Error(
10919
+ `task '${task.id}' declares checkScript but no CheckScriptRunner is configured`
10920
+ );
10921
+ }
10922
+ const r2 = await this.checkScriptRunner.run(task.checkScript, this.cwd);
10923
+ return {
10924
+ passed: r2.passed,
10925
+ findings: r2.findings,
10926
+ stdout: r2.output,
10927
+ stderr: r2.stderr,
10928
+ structured: r2.structured ? r2.structured : null
10929
+ };
10930
+ }
10194
10931
  if (!task.checkCommand || task.checkCommand.length === 0) {
10195
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing checkCommand");
10932
+ throw new Error(`task '${task.id}' is missing checkCommand`);
10196
10933
  }
10934
+ const r = await this.checkRunner.run(task.checkCommand, this.cwd);
10935
+ return {
10936
+ passed: r.passed,
10937
+ findings: r.findings,
10938
+ stdout: r.output,
10939
+ stderr: "",
10940
+ structured: null
10941
+ };
10942
+ }
10943
+ /**
10944
+ * Hermes Phase 2 — Compose the agent prompt-context block from inlined
10945
+ * skills + upstream task outputs. Returns an empty string when nothing
10946
+ * is configured (or when the resolver is absent), which is the safe
10947
+ * no-op default.
10948
+ */
10949
+ async composePromptContext(task) {
10950
+ if (!this.contextResolver) return "";
10951
+ const skills = await this.contextResolver.resolveInlineSkills(
10952
+ task.inlineSkills,
10953
+ task.inlineSkillsBudgetTokens ?? 8e3
10954
+ );
10955
+ const upstream = await this.contextResolver.resolveContextFrom(task.contextFrom, {
10956
+ maxAgeMinutes: task.contextFromMaxAgeMinutes ?? 1440
10957
+ });
10958
+ return [skills, upstream].filter(Boolean).join("\n");
10959
+ }
10960
+ /**
10961
+ * Mechanical-AI: run check (legacy or Phase 2 script), dispatch AI agent
10962
+ * only if fixable findings exist; persist captured stdout/stderr/context
10963
+ * via the output store on the way out.
10964
+ */
10965
+ async runMechanicalAI(task, startedAt) {
10197
10966
  if (!task.fixSkill) {
10198
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill");
10967
+ return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing fixSkill"));
10199
10968
  }
10200
10969
  if (!task.branch) {
10201
- return this.failureResult(task.id, startedAt, "mechanical-ai task missing branch");
10970
+ return wrap(this.failureResult(task.id, startedAt, "mechanical-ai task missing branch"));
10202
10971
  }
10203
- const checkResult = await this.checkRunner.run(task.checkCommand, this.cwd);
10204
- if (checkResult.findings === 0) {
10972
+ if (!task.checkCommand && !task.checkScript) {
10973
+ return wrap(
10974
+ this.failureResult(
10975
+ task.id,
10976
+ startedAt,
10977
+ "mechanical-ai task missing checkCommand or checkScript"
10978
+ )
10979
+ );
10980
+ }
10981
+ let check;
10982
+ try {
10983
+ check = await this.runCheckStep(task);
10984
+ } catch (err) {
10985
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
10986
+ }
10987
+ const promptContext = await this.composePromptContext(task);
10988
+ const baseCaptured = {
10989
+ stdout: check.stdout,
10990
+ stderr: check.stderr,
10991
+ structured: check.structured,
10992
+ ...promptContext ? { context: promptContext } : {}
10993
+ };
10994
+ const wakeAgentExplicitlyFalse = check.structured !== null && typeof check.structured === "object" && check.structured.wakeAgent === false;
10995
+ if (check.findings === 0 || wakeAgentExplicitlyFalse) {
10205
10996
  return {
10206
- taskId: task.id,
10207
- startedAt,
10208
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10209
- status: "no-issues",
10210
- findings: 0,
10211
- fixed: 0,
10212
- prUrl: null,
10213
- prUpdated: false
10997
+ result: {
10998
+ taskId: task.id,
10999
+ startedAt,
11000
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11001
+ status: "no-issues",
11002
+ findings: check.findings,
11003
+ fixed: 0,
11004
+ prUrl: null,
11005
+ prUpdated: false
11006
+ },
11007
+ captured: baseCaptured
10214
11008
  };
10215
11009
  }
10216
11010
  if (this.prManager) {
10217
11011
  try {
10218
11012
  await this.prManager.ensureBranch(task.branch, this.baseBranch);
10219
11013
  } catch (err) {
10220
- return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
11014
+ return wrap(
11015
+ this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`),
11016
+ baseCaptured
11017
+ );
10221
11018
  }
10222
11019
  }
10223
11020
  const backendName = this.resolveBackend(task.id);
10224
11021
  let agentResult;
10225
11022
  try {
10226
- agentResult = await this.agentDispatcher.dispatch(
10227
- task.fixSkill,
10228
- task.branch,
10229
- backendName,
10230
- this.cwd
10231
- );
11023
+ agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
11024
+ promptContext
11025
+ }) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
10232
11026
  } catch (err) {
10233
11027
  return {
10234
- taskId: task.id,
10235
- startedAt,
10236
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10237
- status: "failure",
10238
- findings: checkResult.findings,
10239
- fixed: 0,
10240
- prUrl: null,
10241
- prUpdated: false,
10242
- error: `Agent dispatch failed: ${String(err)}`
11028
+ result: {
11029
+ taskId: task.id,
11030
+ startedAt,
11031
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11032
+ status: "failure",
11033
+ findings: check.findings,
11034
+ fixed: 0,
11035
+ prUrl: null,
11036
+ prUpdated: false,
11037
+ error: `Agent dispatch failed: ${String(err)}`
11038
+ },
11039
+ captured: baseCaptured
10243
11040
  };
10244
11041
  }
10245
11042
  let prUrl = null;
10246
11043
  let prUpdated = false;
10247
11044
  if (this.prManager && agentResult.producedCommits) {
10248
11045
  try {
10249
- const summary = `Findings: ${checkResult.findings}, Fixed: ${agentResult.fixed}`;
11046
+ const summary = `Findings: ${check.findings}, Fixed: ${agentResult.fixed}`;
10250
11047
  const prResult = await this.prManager.ensurePR(task, summary);
10251
11048
  prUrl = prResult.prUrl;
10252
11049
  prUpdated = prResult.prUpdated;
@@ -10255,14 +11052,17 @@ var TaskRunner = class {
10255
11052
  }
10256
11053
  }
10257
11054
  return {
10258
- taskId: task.id,
10259
- startedAt,
10260
- completedAt: (/* @__PURE__ */ new Date()).toISOString(),
10261
- status: "success",
10262
- findings: checkResult.findings,
10263
- fixed: agentResult.fixed,
10264
- prUrl,
10265
- prUpdated
11055
+ result: {
11056
+ taskId: task.id,
11057
+ startedAt,
11058
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
11059
+ status: "success",
11060
+ findings: check.findings,
11061
+ fixed: agentResult.fixed,
11062
+ prUrl,
11063
+ prUpdated
11064
+ },
11065
+ captured: baseCaptured
10266
11066
  };
10267
11067
  }
10268
11068
  /**
@@ -10282,15 +11082,13 @@ var TaskRunner = class {
10282
11082
  return this.failureResult(task.id, startedAt, `ensureBranch failed: ${String(err)}`);
10283
11083
  }
10284
11084
  }
11085
+ const promptContext = await this.composePromptContext(task);
10285
11086
  const backendName = this.resolveBackend(task.id);
10286
11087
  let agentResult;
10287
11088
  try {
10288
- agentResult = await this.agentDispatcher.dispatch(
10289
- task.fixSkill,
10290
- task.branch,
10291
- backendName,
10292
- this.cwd
10293
- );
11089
+ agentResult = promptContext ? await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd, {
11090
+ promptContext
11091
+ }) : await this.agentDispatcher.dispatch(task.fixSkill, task.branch, backendName, this.cwd);
10294
11092
  } catch (err) {
10295
11093
  return this.failureResult(task.id, startedAt, `Agent dispatch failed: ${String(err)}`);
10296
11094
  }
@@ -10318,7 +11116,7 @@ var TaskRunner = class {
10318
11116
  };
10319
11117
  }
10320
11118
  /**
10321
- * Report-only: run check command, record metrics, no AI dispatch.
11119
+ * Report-only: run check (legacy or Phase 2 script), record metrics, no AI dispatch.
10322
11120
  *
10323
11121
  * Honors the JSON status contract emitted by Phase 4/5 CLIs (`harness pulse run`
10324
11122
  * and `harness compound scan-candidates` in `--non-interactive` mode):
@@ -10328,13 +11126,24 @@ var TaskRunner = class {
10328
11126
  * Legacy report-only tasks emit free-form output and fall through to 'success'.
10329
11127
  */
10330
11128
  async runReportOnly(task, startedAt) {
10331
- if (!task.checkCommand || task.checkCommand.length === 0) {
10332
- return this.failureResult(task.id, startedAt, "report-only task missing checkCommand");
11129
+ if (!task.checkCommand && !task.checkScript) {
11130
+ return wrap(
11131
+ this.failureResult(
11132
+ task.id,
11133
+ startedAt,
11134
+ "report-only task missing checkCommand or checkScript"
11135
+ )
11136
+ );
10333
11137
  }
10334
- const checkResult = await this.checkRunner.run(task.checkCommand, this.cwd);
10335
- const parsed = parseStatusLine(checkResult.output);
11138
+ let check;
11139
+ try {
11140
+ check = await this.runCheckStep(task);
11141
+ } catch (err) {
11142
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11143
+ }
11144
+ const parsed = parseStatusLine(check.stdout);
10336
11145
  const status = parsed?.status ?? "success";
10337
- const findings = parsed === null ? checkResult.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
11146
+ const findings = parsed === null ? check.findings : typeof parsed.candidatesFound === "number" ? parsed.candidatesFound : 0;
10338
11147
  const result = {
10339
11148
  taskId: task.id,
10340
11149
  startedAt,
@@ -10348,7 +11157,10 @@ var TaskRunner = class {
10348
11157
  if (parsed?.error) {
10349
11158
  result.error = parsed.error;
10350
11159
  }
10351
- return result;
11160
+ return {
11161
+ result,
11162
+ captured: { stdout: check.stdout, stderr: check.stderr, structured: check.structured }
11163
+ };
10352
11164
  }
10353
11165
  /**
10354
11166
  * Housekeeping: run command directly, no AI, no PR.
@@ -10359,17 +11171,39 @@ var TaskRunner = class {
10359
11171
  * - sync-main contract: updated/no-op/skipped/error → mapped onto the run-result status
10360
11172
  * Legacy housekeeping commands that emit no JSON keep the prior behavior:
10361
11173
  * status: 'success', findings: 0.
11174
+ *
11175
+ * Hermes Phase 2: a `checkScript` may replace `checkCommand` for housekeeping
11176
+ * tasks; the runner falls through to the same JSON-status parsing path.
10362
11177
  */
10363
11178
  async runHousekeeping(task, startedAt) {
10364
- if (!task.checkCommand || task.checkCommand.length === 0) {
10365
- return this.failureResult(task.id, startedAt, "housekeeping task missing checkCommand");
11179
+ if (!task.checkCommand && !task.checkScript) {
11180
+ return wrap(
11181
+ this.failureResult(
11182
+ task.id,
11183
+ startedAt,
11184
+ "housekeeping task missing checkCommand or checkScript"
11185
+ )
11186
+ );
10366
11187
  }
10367
11188
  let stdout;
10368
- try {
10369
- const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
10370
- stdout = out.stdout ?? "";
10371
- } catch (err) {
10372
- return this.failureResult(task.id, startedAt, String(err));
11189
+ let stderr = "";
11190
+ let structured = null;
11191
+ if (task.checkScript) {
11192
+ try {
11193
+ const r = await this.runCheckStep(task);
11194
+ stdout = r.stdout;
11195
+ stderr = r.stderr;
11196
+ structured = r.structured;
11197
+ } catch (err) {
11198
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11199
+ }
11200
+ } else {
11201
+ try {
11202
+ const out = await this.commandExecutor.exec(task.checkCommand, this.cwd);
11203
+ stdout = out.stdout ?? "";
11204
+ } catch (err) {
11205
+ return wrap(this.failureResult(task.id, startedAt, String(err)));
11206
+ }
10373
11207
  }
10374
11208
  const parsed = parseStatusLine(stdout);
10375
11209
  const status = parsed?.status ?? "success";
@@ -10384,7 +11218,7 @@ var TaskRunner = class {
10384
11218
  prUpdated: false
10385
11219
  };
10386
11220
  if (parsed?.error) result.error = parsed.error;
10387
- return result;
11221
+ return { result, captured: { stdout, stderr, structured } };
10388
11222
  }
10389
11223
  /**
10390
11224
  * Resolve which AI backend name to use for a given task.
@@ -10409,6 +11243,9 @@ var TaskRunner = class {
10409
11243
  };
10410
11244
  }
10411
11245
  };
11246
+ function wrap(result, captured) {
11247
+ return captured ? { result, captured } : { result };
11248
+ }
10412
11249
  function parseStatusLine(output) {
10413
11250
  const lines = output.split("\n").map((l) => l.trim()).filter(Boolean);
10414
11251
  for (let i = lines.length - 1; i >= 0; i--) {
@@ -10446,6 +11283,560 @@ function parseStatusLine(output) {
10446
11283
  return null;
10447
11284
  }
10448
11285
 
11286
+ // src/maintenance/check-script-runner.ts
11287
+ import { execFile as execFile6 } from "child_process";
11288
+ import { promisify as promisify3 } from "util";
11289
+ import * as path17 from "path";
11290
+ var execFileAsync = promisify3(execFile6);
11291
+ var CheckScriptRunner = class {
11292
+ constructor(cwd) {
11293
+ this.cwd = cwd;
11294
+ }
11295
+ cwd;
11296
+ async run(spec, cwd) {
11297
+ const projectRoot = cwd ?? this.cwd;
11298
+ const captured = await captureScript(spec, projectRoot);
11299
+ const parseJson = spec.parseStdoutJson !== false;
11300
+ const structured = parseJson ? parseStatusEnvelope(captured.stdout) : null;
11301
+ if (structured) {
11302
+ return mapStructured(structured, captured.stdout, captured.stderr);
11303
+ }
11304
+ return heuristicResult(captured.stdout, captured.stderr, captured.exitedAbnormally);
11305
+ }
11306
+ };
11307
+ async function captureScript(spec, projectRoot) {
11308
+ const resolved = path17.isAbsolute(spec.path) ? spec.path : path17.resolve(projectRoot, spec.path);
11309
+ const args = spec.args ?? [];
11310
+ const timeoutMs = spec.timeoutMs ?? 12e4;
11311
+ try {
11312
+ const result = await execFileAsync(resolved, args, { cwd: projectRoot, timeout: timeoutMs });
11313
+ return {
11314
+ stdout: String(result.stdout ?? ""),
11315
+ stderr: String(result.stderr ?? ""),
11316
+ exitedAbnormally: false
11317
+ };
11318
+ } catch (err) {
11319
+ const e = err;
11320
+ return {
11321
+ stdout: String(e.stdout ?? ""),
11322
+ stderr: String(e.stderr ?? ""),
11323
+ exitedAbnormally: true
11324
+ };
11325
+ }
11326
+ }
11327
+ function parseStatusEnvelope(stdout) {
11328
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
11329
+ for (let i = lines.length - 1; i >= 0; i--) {
11330
+ const env = classifyLine2(lines[i]);
11331
+ if (env) return env;
11332
+ }
11333
+ return null;
11334
+ }
11335
+ var ENVELOPE_STATUSES = /* @__PURE__ */ new Set(["ok", "findings", "skip", "error"]);
11336
+ function classifyLine2(line) {
11337
+ const obj = tryParseJsonObject(line);
11338
+ if (!obj) return null;
11339
+ const s = obj.status;
11340
+ if (typeof s !== "string" || !ENVELOPE_STATUSES.has(s)) return null;
11341
+ return buildEnvelope(s, obj);
11342
+ }
11343
+ function tryParseJsonObject(line) {
11344
+ if (!line || !line.startsWith("{") || !line.endsWith("}")) return null;
11345
+ try {
11346
+ return JSON.parse(line);
11347
+ } catch {
11348
+ return null;
11349
+ }
11350
+ }
11351
+ function buildEnvelope(status, obj) {
11352
+ const env = { status };
11353
+ if (typeof obj.findings === "number") env.findings = obj.findings;
11354
+ if (typeof obj.wakeAgent === "boolean") env.wakeAgent = obj.wakeAgent;
11355
+ if (typeof obj.message === "string") env.message = obj.message;
11356
+ if (obj.outputs && typeof obj.outputs === "object") {
11357
+ env.outputs = obj.outputs;
11358
+ }
11359
+ return env;
11360
+ }
11361
+ function mapStructured(env, stdout, stderr) {
11362
+ const findings = env.findings ?? (env.status === "findings" ? 1 : 0);
11363
+ switch (env.status) {
11364
+ case "ok":
11365
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11366
+ case "findings": {
11367
+ const wake = env.wakeAgent ?? findings > 0;
11368
+ return { passed: !wake, findings, output: stdout, stderr, structured: env };
11369
+ }
11370
+ case "skip":
11371
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11372
+ case "error":
11373
+ return {
11374
+ passed: false,
11375
+ findings: Math.max(findings, 1),
11376
+ output: stdout,
11377
+ stderr,
11378
+ structured: env
11379
+ };
11380
+ default:
11381
+ return { passed: true, findings: 0, output: stdout, stderr, structured: env };
11382
+ }
11383
+ }
11384
+ function heuristicResult(stdout, stderr, exitedAbnormally) {
11385
+ const combined = [stdout, stderr].filter(Boolean).join("\n");
11386
+ const findingsMatch = combined.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
11387
+ const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : exitedAbnormally ? 1 : 0;
11388
+ return {
11389
+ passed: findings === 0 && !exitedAbnormally,
11390
+ findings,
11391
+ output: stdout,
11392
+ stderr,
11393
+ structured: null
11394
+ };
11395
+ }
11396
+
11397
+ // src/maintenance/output-store.ts
11398
+ import * as fs16 from "fs";
11399
+ import * as path18 from "path";
11400
+ var DEFAULT_RETENTION = {
11401
+ runs: 50,
11402
+ maxAgeDays: 30
11403
+ };
11404
+ var fallbackLogger2 = {
11405
+ info: () => {
11406
+ },
11407
+ warn: (m, c) => console.warn(m, c),
11408
+ error: (m, c) => console.error(m, c)
11409
+ };
11410
+ var TaskOutputStore = class {
11411
+ rootDir;
11412
+ retentionDefaults;
11413
+ logger;
11414
+ constructor(options) {
11415
+ this.rootDir = options.rootDir;
11416
+ this.retentionDefaults = options.retentionDefaults ?? DEFAULT_RETENTION;
11417
+ this.logger = options.logger ?? fallbackLogger2;
11418
+ }
11419
+ /**
11420
+ * Reject task IDs that don't match the validator's kebab-case pattern —
11421
+ * defends `dirFor()` against caller-supplied path-traversal segments
11422
+ * (`'../foo'`) when the store is invoked from CLI surfaces that don't
11423
+ * round-trip through `validateCustomTasks`.
11424
+ */
11425
+ ensureSafeTaskId(taskId) {
11426
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(taskId)) {
11427
+ throw new Error(
11428
+ `TaskOutputStore: invalid task id '${taskId}' (must match ^[a-z0-9][a-z0-9-]*$)`
11429
+ );
11430
+ }
11431
+ }
11432
+ /**
11433
+ * Persist a single run entry. Retention is applied after the write so
11434
+ * the latest record is durable even if pruning fails.
11435
+ */
11436
+ async write(taskId, entry, retention) {
11437
+ this.ensureSafeTaskId(taskId);
11438
+ const dir = this.dirFor(taskId);
11439
+ await fs16.promises.mkdir(dir, { recursive: true });
11440
+ const fileName = `${sanitizeIso(entry.completedAt || (/* @__PURE__ */ new Date()).toISOString())}.json`;
11441
+ const filePath = path18.join(dir, fileName);
11442
+ const tmpPath = `${filePath}.tmp`;
11443
+ const payload = JSON.stringify(entry, null, 2);
11444
+ await fs16.promises.writeFile(tmpPath, payload, "utf-8");
11445
+ await fs16.promises.rename(tmpPath, filePath);
11446
+ try {
11447
+ await this.applyRetention(taskId, retention);
11448
+ } catch (err) {
11449
+ this.logger.warn("TaskOutputStore retention failed", { taskId, error: String(err) });
11450
+ }
11451
+ }
11452
+ /**
11453
+ * Return the most recent persisted entry for the task, or null if none.
11454
+ */
11455
+ async latest(taskId) {
11456
+ const entries = await this.list(taskId, 1, 0);
11457
+ return entries[0] ?? null;
11458
+ }
11459
+ /**
11460
+ * List entries newest-first with offset+limit pagination.
11461
+ */
11462
+ async list(taskId, limit, offset) {
11463
+ this.ensureSafeTaskId(taskId);
11464
+ const dir = this.dirFor(taskId);
11465
+ const fileNames = await listJsonFilesDescending(dir);
11466
+ const slice = fileNames.slice(offset, offset + limit);
11467
+ const out = [];
11468
+ for (const name of slice) {
11469
+ const entry = await this.readEntry(path18.join(dir, name));
11470
+ if (entry) out.push(entry);
11471
+ }
11472
+ return out;
11473
+ }
11474
+ /**
11475
+ * Lookup a specific run by its file name (without the `.json` suffix) or
11476
+ * by its raw completion timestamp.
11477
+ */
11478
+ async get(taskId, runId) {
11479
+ this.ensureSafeTaskId(taskId);
11480
+ if (/[\\/]|\.\./.test(runId)) {
11481
+ throw new Error(`TaskOutputStore: runId '${runId}' must not contain path separators or '..'`);
11482
+ }
11483
+ const dir = this.dirFor(taskId);
11484
+ const fileName = runId.endsWith(".json") ? runId : `${sanitizeIso(runId)}.json`;
11485
+ return this.readEntry(path18.join(dir, fileName));
11486
+ }
11487
+ /**
11488
+ * The on-disk root for a given task. Exposed for tooling that needs to walk
11489
+ * outputs from outside the store API.
11490
+ */
11491
+ dirFor(taskId) {
11492
+ return path18.join(this.rootDir, taskId, "outputs");
11493
+ }
11494
+ async readEntry(filePath) {
11495
+ try {
11496
+ const buf = await fs16.promises.readFile(filePath, "utf-8");
11497
+ const parsed = JSON.parse(buf);
11498
+ return parsed;
11499
+ } catch {
11500
+ return null;
11501
+ }
11502
+ }
11503
+ async applyRetention(taskId, retention) {
11504
+ const runs = retention?.runs ?? this.retentionDefaults.runs;
11505
+ const maxAgeDays = retention?.maxAgeDays ?? this.retentionDefaults.maxAgeDays;
11506
+ const dir = this.dirFor(taskId);
11507
+ const fileNames = await listJsonFilesDescending(dir);
11508
+ const overflow = fileNames.slice(runs);
11509
+ const cutoffMs = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
11510
+ const aged = [];
11511
+ for (const name of fileNames) {
11512
+ const ts = parseIsoFromFileName(name);
11513
+ if (ts !== null && ts < cutoffMs) aged.push(name);
11514
+ }
11515
+ const toRemove = /* @__PURE__ */ new Set([...overflow, ...aged]);
11516
+ for (const name of toRemove) {
11517
+ try {
11518
+ await fs16.promises.unlink(path18.join(dir, name));
11519
+ } catch {
11520
+ }
11521
+ }
11522
+ }
11523
+ };
11524
+ async function listJsonFilesDescending(dir) {
11525
+ let names;
11526
+ try {
11527
+ names = await fs16.promises.readdir(dir);
11528
+ } catch {
11529
+ return [];
11530
+ }
11531
+ return names.filter((n) => n.endsWith(".json")).sort().reverse();
11532
+ }
11533
+ function sanitizeIso(iso) {
11534
+ return iso.replace(/:/g, "-");
11535
+ }
11536
+ function parseIsoFromFileName(fileName) {
11537
+ const stem = fileName.replace(/\.json$/, "");
11538
+ const restored = stem.replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3");
11539
+ const ms = Date.parse(restored);
11540
+ return Number.isFinite(ms) ? ms : null;
11541
+ }
11542
+
11543
+ // src/maintenance/context-resolver.ts
11544
+ var ContextResolver = class {
11545
+ outputStore;
11546
+ skillReader;
11547
+ logger;
11548
+ perUpstreamMaxChars;
11549
+ constructor(options) {
11550
+ this.outputStore = options.outputStore;
11551
+ this.skillReader = options.skillReader ?? null;
11552
+ this.logger = options.logger ?? fallbackLogger3;
11553
+ this.perUpstreamMaxChars = options.perUpstreamMaxChars ?? 2e3;
11554
+ }
11555
+ async resolveContextFrom(upstreamTaskIds, options = {}) {
11556
+ if (!upstreamTaskIds || upstreamTaskIds.length === 0) return "";
11557
+ const maxAgeMs = (options.maxAgeMinutes ?? 1440) * 60 * 1e3;
11558
+ const now = Date.now();
11559
+ const sections = [];
11560
+ for (const id of upstreamTaskIds) {
11561
+ const entry = await this.outputStore.latest(id);
11562
+ sections.push(this.formatUpstream(id, entry, now, maxAgeMs));
11563
+ }
11564
+ return `## Upstream context
11565
+
11566
+ ${sections.join("\n\n")}
11567
+ `;
11568
+ }
11569
+ async resolveInlineSkills(skillNames, budgetTokens = 8e3) {
11570
+ if (!skillNames || skillNames.length === 0) return "";
11571
+ if (!this.skillReader) return "";
11572
+ const charBudget = budgetTokens * 4;
11573
+ let used = 0;
11574
+ const sections = [];
11575
+ let truncatedAt = -1;
11576
+ for (let i = 0; i < skillNames.length; i++) {
11577
+ const name = skillNames[i];
11578
+ const body = await this.skillReader.read(name);
11579
+ if (body === null) {
11580
+ this.logger.warn("inlineSkills: skill not found in registry", { name });
11581
+ continue;
11582
+ }
11583
+ const block = `### ${name}
11584
+
11585
+ ${body}`;
11586
+ if (used + block.length > charBudget) {
11587
+ truncatedAt = i;
11588
+ break;
11589
+ }
11590
+ used += block.length;
11591
+ sections.push(block);
11592
+ }
11593
+ if (truncatedAt >= 0) {
11594
+ this.logger.warn(
11595
+ `inlineSkillsBudgetTokens (${budgetTokens}) exhausted after ${sections.length} of ${skillNames.length} skills; truncated.`
11596
+ );
11597
+ }
11598
+ if (sections.length === 0) return "";
11599
+ return `## Reference skills
11600
+
11601
+ ${sections.join("\n\n")}
11602
+ `;
11603
+ }
11604
+ formatUpstream(id, entry, now, maxAgeMs) {
11605
+ if (!entry) {
11606
+ return `### ${id}
11607
+
11608
+ _[no prior run]_`;
11609
+ }
11610
+ const completedMs = Date.parse(entry.completedAt);
11611
+ if (Number.isFinite(completedMs) && now - completedMs > maxAgeMs) {
11612
+ return `### ${id} (last run ${entry.completedAt}, stale)
11613
+
11614
+ _[stale: omitted]_`;
11615
+ }
11616
+ const head = `### ${id} (last run ${entry.completedAt}, status=${entry.status}, findings=${entry.findings})`;
11617
+ const body = (entry.stdout ?? "").trim();
11618
+ const truncated = body.length > this.perUpstreamMaxChars ? `${body.slice(0, this.perUpstreamMaxChars)}
11619
+
11620
+ _[truncated]_` : body;
11621
+ return `${head}
11622
+
11623
+ ${truncated || "_[no stdout captured]_"}`;
11624
+ }
11625
+ };
11626
+ var fallbackLogger3 = {
11627
+ info: () => {
11628
+ },
11629
+ warn: () => {
11630
+ },
11631
+ error: () => {
11632
+ }
11633
+ };
11634
+
11635
+ // src/maintenance/custom-task-validator.ts
11636
+ import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
11637
+ var ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
11638
+ var REQUIRED_FIELDS_BY_TYPE = {
11639
+ "mechanical-ai": ["branch", "fixSkill"],
11640
+ "pure-ai": ["branch", "fixSkill"],
11641
+ "report-only": [],
11642
+ housekeeping: []
11643
+ };
11644
+ function validateCustomTasks(customTasks, builtIns, deps = {}) {
11645
+ const errors = [];
11646
+ if (!customTasks) return Ok23(void 0);
11647
+ const builtInIds = new Set(builtIns.map((t) => t.id));
11648
+ const customIds = Object.keys(customTasks);
11649
+ const allIds = /* @__PURE__ */ new Set([...builtInIds, ...customIds]);
11650
+ for (const id of customIds) {
11651
+ const task = customTasks[id];
11652
+ if (!task) continue;
11653
+ validateOne(id, task, builtInIds, allIds, deps, errors);
11654
+ }
11655
+ detectCycles(customTasks, builtIns, errors);
11656
+ return errors.length === 0 ? Ok23(void 0) : Err20(errors);
11657
+ }
11658
+ function validateOne(id, task, builtInIds, allIds, deps, errors) {
11659
+ const prefix = `customTasks.${id}`;
11660
+ if (!ID_PATTERN.test(id)) {
11661
+ errors.push({
11662
+ path: prefix,
11663
+ message: `task ID '${id}' must match ^[a-z0-9][a-z0-9-]*$`
11664
+ });
11665
+ }
11666
+ if (builtInIds.has(id)) {
11667
+ errors.push({
11668
+ path: prefix,
11669
+ message: `task ID '${id}' collides with a built-in task; choose a different name`
11670
+ });
11671
+ }
11672
+ if (!task.description || task.description.trim().length === 0) {
11673
+ errors.push({ path: `${prefix}.description`, message: "description is required" });
11674
+ }
11675
+ if (!task.schedule || task.schedule.trim().length === 0) {
11676
+ errors.push({ path: `${prefix}.schedule`, message: "schedule (cron expression) is required" });
11677
+ }
11678
+ validateCheckShape(prefix, task, errors);
11679
+ validateRequiredByType(prefix, task, errors);
11680
+ validateContextFrom(prefix, id, task, allIds, errors);
11681
+ validateInlineSkills(prefix, task, deps, errors);
11682
+ validateScriptPath(prefix, task, deps, errors);
11683
+ }
11684
+ function validateCheckShape(prefix, task, errors) {
11685
+ const hasCommand = Array.isArray(task.checkCommand) && task.checkCommand.length > 0;
11686
+ const hasScript = task.checkScript !== void 0;
11687
+ if (hasCommand && hasScript) {
11688
+ errors.push({
11689
+ path: prefix,
11690
+ message: "a task may declare checkCommand OR checkScript, not both"
11691
+ });
11692
+ }
11693
+ const needsCheck = task.type === "mechanical-ai" || task.type === "report-only" || task.type === "housekeeping";
11694
+ if (needsCheck && !hasCommand && !hasScript) {
11695
+ errors.push({
11696
+ path: prefix,
11697
+ message: `${task.type} task must declare either checkCommand or checkScript`
11698
+ });
11699
+ }
11700
+ if (hasScript) {
11701
+ const path22 = task.checkScript?.path;
11702
+ if (!path22 || path22.trim().length === 0) {
11703
+ errors.push({ path: `${prefix}.checkScript.path`, message: "checkScript.path is required" });
11704
+ }
11705
+ if (task.checkScript?.timeoutMs !== void 0 && task.checkScript.timeoutMs <= 0) {
11706
+ errors.push({
11707
+ path: `${prefix}.checkScript.timeoutMs`,
11708
+ message: "timeoutMs must be a positive integer"
11709
+ });
11710
+ }
11711
+ }
11712
+ }
11713
+ function validateRequiredByType(prefix, task, errors) {
11714
+ const required = REQUIRED_FIELDS_BY_TYPE[task.type];
11715
+ if (!required) {
11716
+ errors.push({ path: `${prefix}.type`, message: `unknown task type '${String(task.type)}'` });
11717
+ return;
11718
+ }
11719
+ for (const field of required) {
11720
+ const value = task[field];
11721
+ if (value === void 0 || value === null || typeof value === "string" && value.length === 0) {
11722
+ errors.push({
11723
+ path: `${prefix}.${String(field)}`,
11724
+ message: `${task.type} task requires ${String(field)}`
11725
+ });
11726
+ }
11727
+ }
11728
+ if ((task.type === "mechanical-ai" || task.type === "pure-ai") && task.branch === null) {
11729
+ errors.push({
11730
+ path: `${prefix}.branch`,
11731
+ message: `${task.type} task requires a non-null branch`
11732
+ });
11733
+ }
11734
+ }
11735
+ function validateContextFrom(prefix, selfId, task, allIds, errors) {
11736
+ if (task.contextFromMaxAgeMinutes !== void 0 && task.contextFromMaxAgeMinutes <= 0) {
11737
+ errors.push({
11738
+ path: `${prefix}.contextFromMaxAgeMinutes`,
11739
+ message: "contextFromMaxAgeMinutes must be a positive integer"
11740
+ });
11741
+ }
11742
+ if (!task.contextFrom) return;
11743
+ for (let i = 0; i < task.contextFrom.length; i++) {
11744
+ const upstreamId = task.contextFrom[i];
11745
+ if (!upstreamId) continue;
11746
+ if (upstreamId === selfId) {
11747
+ errors.push({
11748
+ path: `${prefix}.contextFrom[${i}]`,
11749
+ message: `task '${selfId}' cannot reference itself in contextFrom`
11750
+ });
11751
+ }
11752
+ if (!allIds.has(upstreamId)) {
11753
+ errors.push({
11754
+ path: `${prefix}.contextFrom[${i}]`,
11755
+ message: `references unknown task '${upstreamId}'`
11756
+ });
11757
+ }
11758
+ }
11759
+ }
11760
+ function validateInlineSkills(prefix, task, deps, errors) {
11761
+ if (!task.inlineSkills) return;
11762
+ if (!deps.skillExists) return;
11763
+ for (let i = 0; i < task.inlineSkills.length; i++) {
11764
+ const name = task.inlineSkills[i];
11765
+ if (!name) continue;
11766
+ if (!deps.skillExists(name)) {
11767
+ errors.push({
11768
+ path: `${prefix}.inlineSkills[${i}]`,
11769
+ message: `skill '${name}' not found in the registry`
11770
+ });
11771
+ }
11772
+ }
11773
+ if (task.inlineSkillsBudgetTokens !== void 0 && task.inlineSkillsBudgetTokens <= 0) {
11774
+ errors.push({
11775
+ path: `${prefix}.inlineSkillsBudgetTokens`,
11776
+ message: "inlineSkillsBudgetTokens must be a positive integer"
11777
+ });
11778
+ }
11779
+ }
11780
+ function validateScriptPath(prefix, task, deps, errors) {
11781
+ if (!task.checkScript?.path) return;
11782
+ if (!deps.scriptExists) return;
11783
+ if (!deps.scriptExists(task.checkScript.path)) {
11784
+ errors.push({
11785
+ path: `${prefix}.checkScript.path`,
11786
+ message: `executable not found: ${task.checkScript.path}`
11787
+ });
11788
+ }
11789
+ }
11790
+ function detectCycles(customTasks, builtIns, errors) {
11791
+ const adjacency = /* @__PURE__ */ new Map();
11792
+ for (const t of builtIns) adjacency.set(t.id, []);
11793
+ for (const [id, task] of Object.entries(customTasks)) {
11794
+ adjacency.set(id, (task.contextFrom ?? []).slice());
11795
+ }
11796
+ const color = /* @__PURE__ */ new Map();
11797
+ for (const id of adjacency.keys()) color.set(id, "white");
11798
+ const reported = /* @__PURE__ */ new Set();
11799
+ for (const id of Object.keys(customTasks)) {
11800
+ if (color.get(id) === "white") visitFromRoot(id, adjacency, color, errors, reported);
11801
+ }
11802
+ }
11803
+ function visitFromRoot(start, adjacency, color, errors, reported) {
11804
+ const stack = [{ id: start, nextIdx: 0, path: [start] }];
11805
+ color.set(start, "grey");
11806
+ while (stack.length) {
11807
+ const top = stack[stack.length - 1];
11808
+ const neighbors = adjacency.get(top.id) ?? [];
11809
+ if (top.nextIdx >= neighbors.length) {
11810
+ color.set(top.id, "black");
11811
+ stack.pop();
11812
+ continue;
11813
+ }
11814
+ const next = neighbors[top.nextIdx++];
11815
+ if (!next || !adjacency.has(next)) continue;
11816
+ handleEdge(top, next, color, stack, errors, reported);
11817
+ }
11818
+ }
11819
+ function handleEdge(top, next, color, stack, errors, reported) {
11820
+ const nextColor = color.get(next);
11821
+ if (nextColor === "grey") {
11822
+ reportCycle(top.path, next, errors, reported);
11823
+ } else if (nextColor === "white") {
11824
+ color.set(next, "grey");
11825
+ stack.push({ id: next, nextIdx: 0, path: [...top.path, next] });
11826
+ }
11827
+ }
11828
+ function reportCycle(path22, next, errors, reported) {
11829
+ const cycleStart = path22.indexOf(next);
11830
+ const cyclePath = cycleStart >= 0 ? [...path22.slice(cycleStart), next] : [...path22, next];
11831
+ const key = cyclePath.join("\u2192");
11832
+ if (reported.has(key)) return;
11833
+ reported.add(key);
11834
+ errors.push({
11835
+ path: `customTasks.${cyclePath[0]}.contextFrom`,
11836
+ message: `contextFrom cycle detected: ${cyclePath.join(" \u2192 ")}`
11837
+ });
11838
+ }
11839
+
10449
11840
  // src/orchestrator.ts
10450
11841
  function useCaseForBackendParam(issue, backendParam) {
10451
11842
  if (backendParam === "local") return { kind: "tier", tier: "quick-fix" };
@@ -10546,7 +11937,7 @@ var Orchestrator = class extends EventEmitter {
10546
11937
  completionHandler;
10547
11938
  /** Project root directory, derived from workspace root. */
10548
11939
  get projectRoot() {
10549
- return path16.resolve(this.config.workspace.root, "..", "..");
11940
+ return path19.resolve(this.config.workspace.root, "..", "..");
10550
11941
  }
10551
11942
  enrichedSpecsByIssue = /* @__PURE__ */ new Map();
10552
11943
  /** Tracks recently-failed intelligence analysis to avoid re-requesting every tick */
@@ -10601,10 +11992,10 @@ var Orchestrator = class extends EventEmitter {
10601
11992
  this.renderer = new PromptRenderer();
10602
11993
  this.overrideBackend = overrides?.backend ?? null;
10603
11994
  this.interactionQueue = new InteractionQueue(
10604
- path16.join(config.workspace.root, "..", "interactions"),
11995
+ path19.join(config.workspace.root, "..", "interactions"),
10605
11996
  this
10606
11997
  );
10607
- this.analysisArchive = new AnalysisArchive(path16.join(config.workspace.root, "..", "analyses"));
11998
+ this.analysisArchive = new AnalysisArchive(path19.join(config.workspace.root, "..", "analyses"));
10608
11999
  const backendsMap = this.config.agent.backends ?? {};
10609
12000
  for (const [name, def] of Object.entries(backendsMap)) {
10610
12001
  if (def.type === "local" || def.type === "pi") {
@@ -10648,7 +12039,7 @@ var Orchestrator = class extends EventEmitter {
10648
12039
  ...overrides?.execFileFn ? { execFileFn: overrides.execFileFn } : {}
10649
12040
  });
10650
12041
  this.recorder = new StreamRecorder(
10651
- path16.resolve(config.workspace.root, "..", "streams"),
12042
+ path19.resolve(config.workspace.root, "..", "streams"),
10652
12043
  this.logger
10653
12044
  );
10654
12045
  const self = this;
@@ -10679,10 +12070,10 @@ var Orchestrator = class extends EventEmitter {
10679
12070
  this.completionHandler = new CompletionHandler(ctx, this.postLifecycleComment.bind(this));
10680
12071
  if (config.server?.port) {
10681
12072
  const webhookStore = new WebhookStore(
10682
- path16.join(this.projectRoot, ".harness", "webhooks.json")
12073
+ path19.join(this.projectRoot, ".harness", "webhooks.json")
10683
12074
  );
10684
12075
  this.webhookQueue = new WebhookQueue(
10685
- path16.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
12076
+ path19.join(this.projectRoot, ".harness", "webhook-queue.sqlite")
10686
12077
  );
10687
12078
  const webhookDelivery = new WebhookDelivery({
10688
12079
  queue: this.webhookQueue,
@@ -10720,7 +12111,7 @@ var Orchestrator = class extends EventEmitter {
10720
12111
  queue: this.webhookQueue
10721
12112
  },
10722
12113
  cacheMetrics: this.cacheMetrics,
10723
- plansDir: path16.resolve(config.workspace.root, "..", "docs", "plans"),
12114
+ plansDir: path19.resolve(config.workspace.root, "..", "docs", "plans"),
10724
12115
  pipeline: this.pipeline,
10725
12116
  analysisArchive: this.analysisArchive,
10726
12117
  roadmapPath: config.tracker.filePath ?? null,
@@ -10776,13 +12167,13 @@ var Orchestrator = class extends EventEmitter {
10776
12167
  const logger = this.logger;
10777
12168
  const checkRunner = {
10778
12169
  run: async (command, cwd) => {
10779
- const { execFile: execFile6 } = await import("child_process");
10780
- const { promisify: promisify4 } = await import("util");
10781
- const execFileAsync = promisify4(execFile6);
12170
+ const { execFile: execFile7 } = await import("child_process");
12171
+ const { promisify: promisify5 } = await import("util");
12172
+ const execFileAsync2 = promisify5(execFile7);
10782
12173
  const [cmd, ...args] = command;
10783
12174
  if (!cmd) return { passed: true, findings: 0, output: "" };
10784
12175
  try {
10785
- const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
12176
+ const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
10786
12177
  const findingsMatch = stdout.match(/(\d+)\s+(?:finding|issue|violation|error)/i);
10787
12178
  const findings = findingsMatch ? parseInt(findingsMatch[1], 10) : 0;
10788
12179
  return { passed: findings === 0, findings, output: stdout };
@@ -10811,13 +12202,13 @@ var Orchestrator = class extends EventEmitter {
10811
12202
  };
10812
12203
  const commandExecutor = {
10813
12204
  exec: async (command, cwd) => {
10814
- const { execFile: execFile6 } = await import("child_process");
10815
- const { promisify: promisify4 } = await import("util");
10816
- const execFileAsync = promisify4(execFile6);
12205
+ const { execFile: execFile7 } = await import("child_process");
12206
+ const { promisify: promisify5 } = await import("util");
12207
+ const execFileAsync2 = promisify5(execFile7);
10817
12208
  const [cmd, ...args] = command;
10818
12209
  if (!cmd) return { stdout: "" };
10819
12210
  try {
10820
- const { stdout } = await execFileAsync(cmd, args, { cwd, timeout: 12e4 });
12211
+ const { stdout } = await execFileAsync2(cmd, args, { cwd, timeout: 12e4 });
10821
12212
  return { stdout: String(stdout) };
10822
12213
  } catch (err) {
10823
12214
  logger.warn("Maintenance command execution failed", {
@@ -10829,12 +12220,31 @@ var Orchestrator = class extends EventEmitter {
10829
12220
  }
10830
12221
  }
10831
12222
  };
12223
+ const outputStore = new TaskOutputStore({
12224
+ rootDir: path19.join(this.projectRoot, ".harness", "maintenance"),
12225
+ logger: this.logger
12226
+ });
12227
+ const checkScriptRunner = new CheckScriptRunner(this.projectRoot);
12228
+ const skillReader = {
12229
+ // The orchestrator does not own the skill registry; CLI-side skill
12230
+ // resolution wires this in via direct injection. Default: skill not
12231
+ // resolvable from the orchestrator boundary.
12232
+ read: async () => null
12233
+ };
12234
+ const contextResolver = new ContextResolver({
12235
+ outputStore,
12236
+ skillReader,
12237
+ logger: this.logger
12238
+ });
10832
12239
  return new TaskRunner({
10833
12240
  config: maintenanceConfig,
10834
12241
  checkRunner,
10835
12242
  agentDispatcher,
10836
12243
  commandExecutor,
10837
- cwd: this.projectRoot
12244
+ cwd: this.projectRoot,
12245
+ checkScriptRunner,
12246
+ contextResolver,
12247
+ outputStore
10838
12248
  });
10839
12249
  }
10840
12250
  /**
@@ -10842,8 +12252,17 @@ var Orchestrator = class extends EventEmitter {
10842
12252
  * Extracted from start() to keep function length under threshold.
10843
12253
  */
10844
12254
  async initMaintenance(maintenanceConfig) {
12255
+ const validation = validateCustomTasks(
12256
+ maintenanceConfig.customTasks,
12257
+ BUILT_IN_TASKS
12258
+ );
12259
+ if (!validation.ok) {
12260
+ const messages = validation.error.map((e) => ` - ${e.path}: ${e.message}`).join("\n");
12261
+ throw new Error(`Invalid maintenance.customTasks configuration:
12262
+ ${messages}`);
12263
+ }
10845
12264
  this.maintenanceReporter = new MaintenanceReporter({
10846
- persistDir: path16.join(this.projectRoot, ".harness", "maintenance"),
12265
+ persistDir: path19.join(this.projectRoot, ".harness", "maintenance"),
10847
12266
  logger: this.logger
10848
12267
  });
10849
12268
  await this.maintenanceReporter.load();
@@ -12020,10 +13439,10 @@ function launchTUI(orchestrator) {
12020
13439
 
12021
13440
  // src/maintenance/sync-main.ts
12022
13441
  import { execFile as nodeExecFile } from "child_process";
12023
- import { promisify as promisify3 } from "util";
13442
+ import { promisify as promisify4 } from "util";
12024
13443
  var DEFAULT_TIMEOUT_MS3 = 6e4;
12025
13444
  async function git(execFileFn, args, cwd, timeoutMs) {
12026
- const exec = promisify3(execFileFn);
13445
+ const exec = promisify4(execFileFn);
12027
13446
  const { stdout, stderr } = await exec("git", args, { cwd, timeout: timeoutMs });
12028
13447
  return { stdout: String(stdout), stderr: String(stderr) };
12029
13448
  }
@@ -12163,8 +13582,8 @@ async function syncMain(repoRoot, opts = {}) {
12163
13582
  }
12164
13583
 
12165
13584
  // src/sessions/search-index.ts
12166
- import * as fs15 from "fs";
12167
- import * as path17 from "path";
13585
+ import * as fs17 from "fs";
13586
+ import * as path20 from "path";
12168
13587
  import Database2 from "better-sqlite3";
12169
13588
  import { INDEXED_FILE_KINDS } from "@harness-engineering/types";
12170
13589
  var SEARCH_INDEX_FILE = "search-index.sqlite";
@@ -12209,7 +13628,7 @@ function normalizeFts5Query(query) {
12209
13628
  return query.split(/\s+/).filter((tok) => tok.length > 0).map((tok) => `"${tok.replace(/"/g, '""')}"`).join(" ");
12210
13629
  }
12211
13630
  function searchIndexPath(projectPath) {
12212
- return path17.join(projectPath, ".harness", SEARCH_INDEX_FILE);
13631
+ return path20.join(projectPath, ".harness", SEARCH_INDEX_FILE);
12213
13632
  }
12214
13633
  var FILE_KIND_TO_FILENAME = {
12215
13634
  summary: "summary.md",
@@ -12224,7 +13643,7 @@ var SqliteSearchIndex = class {
12224
13643
  removeSessionStmt;
12225
13644
  totalStmt;
12226
13645
  constructor(dbPath) {
12227
- fs15.mkdirSync(path17.dirname(dbPath), { recursive: true });
13646
+ fs17.mkdirSync(path20.dirname(dbPath), { recursive: true });
12228
13647
  this.db = new Database2(dbPath);
12229
13648
  this.db.pragma("journal_mode = WAL");
12230
13649
  this.db.pragma("synchronous = NORMAL");
@@ -12329,14 +13748,14 @@ function indexSessionDirectory(idx, args) {
12329
13748
  let docsWritten = 0;
12330
13749
  for (const kind of kinds) {
12331
13750
  const fileName = FILE_KIND_TO_FILENAME[kind];
12332
- const filePath = path17.join(args.sessionDir, fileName);
12333
- if (!fs15.existsSync(filePath)) continue;
12334
- let body = fs15.readFileSync(filePath, "utf8");
13751
+ const filePath = path20.join(args.sessionDir, fileName);
13752
+ if (!fs17.existsSync(filePath)) continue;
13753
+ let body = fs17.readFileSync(filePath, "utf8");
12335
13754
  if (Buffer.byteLength(body, "utf8") > cap) {
12336
13755
  body = body.slice(0, cap) + "\n\n[TRUNCATED]";
12337
13756
  }
12338
- const stat = fs15.statSync(filePath);
12339
- const relPath = path17.relative(args.projectPath, filePath).replaceAll("\\", "/");
13757
+ const stat = fs17.statSync(filePath);
13758
+ const relPath = path20.relative(args.projectPath, filePath).replaceAll("\\", "/");
12340
13759
  idx.upsertSessionDoc({
12341
13760
  sessionId: args.sessionId,
12342
13761
  archived: args.archived,
@@ -12351,17 +13770,17 @@ function indexSessionDirectory(idx, args) {
12351
13770
  }
12352
13771
  function reindexFromArchive(projectPath, opts = {}) {
12353
13772
  const start = Date.now();
12354
- const archiveBase = path17.join(projectPath, ".harness", "archive", "sessions");
13773
+ const archiveBase = path20.join(projectPath, ".harness", "archive", "sessions");
12355
13774
  const idx = openSearchIndex(projectPath);
12356
13775
  try {
12357
13776
  idx.resetArchived();
12358
13777
  let sessionsIndexed = 0;
12359
13778
  let docsWritten = 0;
12360
- if (fs15.existsSync(archiveBase)) {
12361
- const entries = fs15.readdirSync(archiveBase, { withFileTypes: true });
13779
+ if (fs17.existsSync(archiveBase)) {
13780
+ const entries = fs17.readdirSync(archiveBase, { withFileTypes: true });
12362
13781
  for (const entry of entries) {
12363
13782
  if (!entry.isDirectory()) continue;
12364
- const sessionDir = path17.join(archiveBase, entry.name);
13783
+ const sessionDir = path20.join(archiveBase, entry.name);
12365
13784
  const result = indexSessionDirectory(idx, {
12366
13785
  sessionId: entry.name,
12367
13786
  sessionDir,
@@ -12381,12 +13800,12 @@ function reindexFromArchive(projectPath, opts = {}) {
12381
13800
  }
12382
13801
 
12383
13802
  // src/sessions/summarize.ts
12384
- import * as fs16 from "fs";
12385
- import * as path18 from "path";
13803
+ import * as fs18 from "fs";
13804
+ import * as path21 from "path";
12386
13805
  import {
12387
13806
  SessionSummarySchema
12388
13807
  } from "@harness-engineering/types";
12389
- import { Ok as Ok23, Err as Err20 } from "@harness-engineering/types";
13808
+ import { Ok as Ok24, Err as Err21 } from "@harness-engineering/types";
12390
13809
  var LLM_SUMMARY_FILE = "llm-summary.md";
12391
13810
  var SUMMARY_INPUT_FILES = [
12392
13811
  { filename: "summary.md", kind: "summary" },
@@ -12412,10 +13831,10 @@ var USER_PROMPT_PREAMBLE = `Below are the archived files for a single harness-en
12412
13831
  function readInputCorpus(archiveDir) {
12413
13832
  const parts = [];
12414
13833
  for (const { filename, kind } of SUMMARY_INPUT_FILES) {
12415
- const p = path18.join(archiveDir, filename);
12416
- if (!fs16.existsSync(p)) continue;
13834
+ const p = path21.join(archiveDir, filename);
13835
+ if (!fs18.existsSync(p)) continue;
12417
13836
  try {
12418
- const content = fs16.readFileSync(p, "utf8");
13837
+ const content = fs18.readFileSync(p, "utf8");
12419
13838
  if (content.trim().length === 0) continue;
12420
13839
  parts.push(`## FILE: ${kind}
12421
13840
 
@@ -12466,7 +13885,7 @@ function renderLlmSummaryMarkdown(summary, meta) {
12466
13885
  return lines.join("\n");
12467
13886
  }
12468
13887
  function writeStubMarkdown(archiveDir, reason) {
12469
- const filePath = path18.join(archiveDir, LLM_SUMMARY_FILE);
13888
+ const filePath = path21.join(archiveDir, LLM_SUMMARY_FILE);
12470
13889
  const body = `---
12471
13890
  generatedAt: ${(/* @__PURE__ */ new Date()).toISOString()}
12472
13891
  schemaVersion: 1
@@ -12477,17 +13896,17 @@ status: failed
12477
13896
 
12478
13897
  - reason: ${reason}
12479
13898
  `;
12480
- fs16.writeFileSync(filePath, body, "utf8");
13899
+ fs18.writeFileSync(filePath, body, "utf8");
12481
13900
  return filePath;
12482
13901
  }
12483
13902
  async function summarizeArchivedSession(ctx) {
12484
13903
  const writeStubOnError = ctx.writeStubOnError ?? true;
12485
- if (!fs16.existsSync(ctx.archiveDir)) {
12486
- return Err20(new Error(`archive directory not found: ${ctx.archiveDir}`));
13904
+ if (!fs18.existsSync(ctx.archiveDir)) {
13905
+ return Err21(new Error(`archive directory not found: ${ctx.archiveDir}`));
12487
13906
  }
12488
13907
  const corpus = readInputCorpus(ctx.archiveDir);
12489
13908
  if (corpus.trim().length === 0) {
12490
- return Err20(new Error(`no summary input files found in ${ctx.archiveDir}`));
13909
+ return Err21(new Error(`no summary input files found in ${ctx.archiveDir}`));
12491
13910
  }
12492
13911
  const inputBudgetTokens = ctx.config?.inputBudgetTokens ?? DEFAULT_INPUT_BUDGET_TOKENS;
12493
13912
  const truncated = truncateForBudget(corpus, inputBudgetTokens);
@@ -12520,7 +13939,7 @@ async function summarizeArchivedSession(ctx) {
12520
13939
  } catch {
12521
13940
  }
12522
13941
  }
12523
- return Err20(
13942
+ return Err21(
12524
13943
  new Error(`session summary failed: ${reason}` + (stubPath ? ` (stub: ${stubPath})` : ""))
12525
13944
  );
12526
13945
  }
@@ -12534,7 +13953,7 @@ async function summarizeArchivedSession(ctx) {
12534
13953
  } catch {
12535
13954
  }
12536
13955
  }
12537
- return Err20(new Error(reason));
13956
+ return Err21(new Error(reason));
12538
13957
  }
12539
13958
  const meta = {
12540
13959
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12543,10 +13962,10 @@ async function summarizeArchivedSession(ctx) {
12543
13962
  outputTokens: response.tokenUsage.outputTokens,
12544
13963
  schemaVersion: 1
12545
13964
  };
12546
- const filePath = path18.join(ctx.archiveDir, LLM_SUMMARY_FILE);
13965
+ const filePath = path21.join(ctx.archiveDir, LLM_SUMMARY_FILE);
12547
13966
  const body = renderLlmSummaryMarkdown(parsed.data, meta);
12548
- fs16.writeFileSync(filePath, body, "utf8");
12549
- return Ok23({ summary: parsed.data, meta, filePath });
13967
+ fs18.writeFileSync(filePath, body, "utf8");
13968
+ return Ok24({ summary: parsed.data, meta, filePath });
12550
13969
  }
12551
13970
  function isSummaryEnabled(config) {
12552
13971
  if (!config) return false;
@@ -12622,8 +14041,11 @@ function buildArchiveHooks(opts) {
12622
14041
  }
12623
14042
  export {
12624
14043
  AnalysisArchive,
14044
+ BUILT_IN_TASKS,
12625
14045
  BackendRouter,
12626
14046
  ClaimManager,
14047
+ GateNotReadyError,
14048
+ GateRunError,
12627
14049
  InteractionQueue,
12628
14050
  LinearGraphQLStub,
12629
14051
  MAX_ATTEMPTS,
@@ -12632,6 +14054,7 @@ export {
12632
14054
  Orchestrator,
12633
14055
  OrchestratorBackendFactory,
12634
14056
  PRDetector,
14057
+ PromotionError,
12635
14058
  PromptRenderer,
12636
14059
  RETRY_DELAYS_MS,
12637
14060
  RoadmapTrackerAdapter,
@@ -12640,6 +14063,7 @@ export {
12640
14063
  SlackSink,
12641
14064
  SqliteSearchIndex,
12642
14065
  StreamRecorder,
14066
+ TaskOutputStore,
12643
14067
  TokenStore,
12644
14068
  WebhookQueue,
12645
14069
  WorkflowLoader,
@@ -12654,6 +14078,9 @@ export {
12654
14078
  createBackend,
12655
14079
  createEmptyState,
12656
14080
  detectScopeTier,
14081
+ emitProposalApproved,
14082
+ emitProposalCreated,
14083
+ emitProposalRejected,
12657
14084
  extractHighlights,
12658
14085
  extractTitlePrefix,
12659
14086
  getAvailableSlots,
@@ -12667,6 +14094,7 @@ export {
12667
14094
  migrateAgentConfig,
12668
14095
  normalizeFts5Query,
12669
14096
  openSearchIndex,
14097
+ promote,
12670
14098
  reconcile,
12671
14099
  reindexFromArchive,
12672
14100
  renderAnalysisComment,
@@ -12675,6 +14103,7 @@ export {
12675
14103
  resolveEscalationConfig,
12676
14104
  resolveOrchestratorId,
12677
14105
  routeIssue,
14106
+ runGate,
12678
14107
  savePublishedIndex,
12679
14108
  searchIndexPath,
12680
14109
  selectCandidates,
@@ -12683,6 +14112,7 @@ export {
12683
14112
  syncMain,
12684
14113
  triageIssue,
12685
14114
  truncateForBudget,
14115
+ validateCustomTasks,
12686
14116
  validateWorkflowConfig,
12687
14117
  wireNotificationSinks,
12688
14118
  wrapAsEnvelope