@gethmy/agent 1.24.0 → 1.25.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.
Files changed (3) hide show
  1. package/dist/cli.js +176 -31
  2. package/dist/index.js +176 -31
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -654,6 +654,14 @@ var init_classification = __esm(() => {
654
654
  MODEL_TIERS = ["simple", "advanced", "research"];
655
655
  });
656
656
 
657
+ // ../harmony-shared/dist/columnSort.js
658
+ var init_columnSort = () => {};
659
+
660
+ // ../harmony-shared/dist/columnCardSplit.js
661
+ var init_columnCardSplit = __esm(() => {
662
+ init_columnSort();
663
+ });
664
+
657
665
  // ../harmony-shared/dist/commentSerializer.js
658
666
  function sanitizeHeaderField(value) {
659
667
  return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
@@ -1440,6 +1448,8 @@ var init_dist = __esm(() => {
1440
1448
  init_branchRef();
1441
1449
  init_cardLinks();
1442
1450
  init_classification();
1451
+ init_columnCardSplit();
1452
+ init_columnSort();
1443
1453
  init_commentSerializer();
1444
1454
  init_constants();
1445
1455
  init_gateEvaluate();
@@ -5684,8 +5694,27 @@ var init_artifact_judge = __esm(() => {
5684
5694
  init_sdk_agent_runner();
5685
5695
  });
5686
5696
 
5697
+ // src/gate-config-error.ts
5698
+ function gateConfigErrorReason(evaluation) {
5699
+ if (!evaluation || evaluation.passed)
5700
+ return null;
5701
+ const structured = evaluation.structured;
5702
+ if (!structured || typeof structured !== "object")
5703
+ return null;
5704
+ if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
5705
+ return null;
5706
+ if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
5707
+ return null;
5708
+ }
5709
+ const reason = structured.reason;
5710
+ return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
5711
+ }
5712
+ var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
5713
+ var init_gate_config_error = __esm(() => {
5714
+ GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
5715
+ });
5716
+
5687
5717
  // src/command-metric.ts
5688
- import { execFileSync as execFileSync10 } from "node:child_process";
5689
5718
  function parseParseMode(parse) {
5690
5719
  if (typeof parse !== "string" || parse.length === 0) {
5691
5720
  return { kind: "invalid", reason: "`parse` must be a non-empty string" };
@@ -5746,15 +5775,91 @@ function parseMetricValue(mode, stdout) {
5746
5775
  }
5747
5776
  return { ok: true, value: resolved };
5748
5777
  }
5749
- function defaultRunCommand(args) {
5750
- const out = execFileSync10(args.command, args.args, {
5751
- cwd: args.cwd,
5752
- timeout: args.timeoutMs,
5753
- stdio: "pipe",
5754
- maxBuffer: MAX_OUTPUT_BUFFER2,
5755
- encoding: "utf8"
5778
+ function runMetricCommand(args) {
5779
+ return new Promise((resolve3, reject) => {
5780
+ let child;
5781
+ try {
5782
+ child = spawnInGroup(args.command, args.args, {
5783
+ cwd: args.cwd,
5784
+ stdio: ["ignore", "pipe", "pipe"]
5785
+ });
5786
+ } catch (err) {
5787
+ reject(err);
5788
+ return;
5789
+ }
5790
+ const pgid = child.pid;
5791
+ const chunks = [];
5792
+ let stdoutBytes = 0;
5793
+ let stderr = "";
5794
+ let settled = false;
5795
+ let killReason = null;
5796
+ let timer;
5797
+ let drainTimer;
5798
+ const settle = (failure) => {
5799
+ if (settled)
5800
+ return;
5801
+ settled = true;
5802
+ if (timer)
5803
+ clearTimeout(timer);
5804
+ if (drainTimer)
5805
+ clearTimeout(drainTimer);
5806
+ reapGroup(pgid);
5807
+ if (failure)
5808
+ reject(failure);
5809
+ else
5810
+ resolve3(Buffer.concat(chunks).toString("utf8"));
5811
+ };
5812
+ const killTree = (reason) => {
5813
+ if (settled || killReason)
5814
+ return;
5815
+ killReason = reason;
5816
+ terminateGroup(child, {
5817
+ sigintTimeoutMs: METRIC_SIGINT_GRACE_MS,
5818
+ sigtermTimeoutMs: METRIC_SIGTERM_GRACE_MS
5819
+ }).catch(() => {}).then(() => {
5820
+ settle(reason === "timeout" ? Object.assign(new Error(`command timed out after ${args.timeoutMs}ms`), { code: "ETIMEDOUT" }) : Object.assign(new Error("stdout maxBuffer exceeded"), {
5821
+ code: "ENOBUFS"
5822
+ }));
5823
+ });
5824
+ };
5825
+ child.stdout?.on("data", (chunk) => {
5826
+ stdoutBytes += chunk.length;
5827
+ if (stdoutBytes > MAX_OUTPUT_BUFFER2) {
5828
+ killTree("overflow");
5829
+ return;
5830
+ }
5831
+ chunks.push(chunk);
5832
+ });
5833
+ child.stderr?.on("data", (chunk) => {
5834
+ if (stderr.length >= MAX_STDERR_CHARS)
5835
+ return;
5836
+ stderr += chunk.toString("utf8");
5837
+ });
5838
+ child.once("error", (err) => settle(err));
5839
+ const settleFromExit = (code, signal) => {
5840
+ if (drainTimer)
5841
+ clearTimeout(drainTimer);
5842
+ if (code === 0) {
5843
+ settle(null);
5844
+ return;
5845
+ }
5846
+ const detail = signal ? `terminated by signal ${signal}` : `exited ${code}`;
5847
+ settle(Object.assign(new Error(`command ${detail}`), {
5848
+ status: typeof code === "number" ? code : null,
5849
+ stderr
5850
+ }));
5851
+ };
5852
+ child.once("exit", (code, signal) => {
5853
+ if (killReason)
5854
+ return;
5855
+ if (timer)
5856
+ clearTimeout(timer);
5857
+ reapGroup(pgid);
5858
+ drainTimer = setTimeout(() => settleFromExit(code, signal), STDIO_DRAIN_GRACE_MS);
5859
+ child.once("close", () => settleFromExit(code, signal));
5860
+ });
5861
+ timer = setTimeout(() => killTree("timeout"), args.timeoutMs);
5756
5862
  });
5757
- return typeof out === "string" ? out : String(out);
5758
5863
  }
5759
5864
 
5760
5865
  class CommandMetricCollector {
@@ -5766,28 +5871,28 @@ class CommandMetricCollector {
5766
5871
  async collect(context) {
5767
5872
  const name = context.gate.metric;
5768
5873
  if (typeof name !== "string" || name.length === 0) {
5769
- return blocked(null, 'Gate kind "custom" needs a `metric` name naming an allowlisted command (e.g. { "kind": "custom", "metric": "lighthouse_performance" }).');
5874
+ return configError(null, 'Gate kind "custom" needs a `metric` name naming an allowlisted command (e.g. { "kind": "custom", "metric": "lighthouse_performance" }).');
5770
5875
  }
5771
5876
  const def = Object.hasOwn(this.deps.metrics, name) ? this.deps.metrics[name] : undefined;
5772
5877
  if (!def) {
5773
- return blocked(name, `Metric "${name}" is not declared in this daemon's allowlist — add it under \`agent.playbooks.metrics\` to permit it.`);
5878
+ return configError(name, `Metric "${name}" is not declared in this daemon's allowlist — add it under \`agent.playbooks.metrics\` to permit it.`);
5774
5879
  }
5775
5880
  if (typeof def.command !== "string" || def.command.length === 0) {
5776
- return blocked(name, `Metric "${name}" declares no \`command\`.`);
5881
+ return configError(name, `Metric "${name}" declares no \`command\`.`);
5777
5882
  }
5778
5883
  const mode = parseParseMode(def.parse);
5779
5884
  if (mode.kind === "invalid") {
5780
- return blocked(name, `Metric "${name}": ${mode.reason}.`);
5885
+ return configError(name, `Metric "${name}": ${mode.reason}.`);
5781
5886
  }
5782
5887
  const requested = typeof def.timeoutMs === "number" && def.timeoutMs > 0 ? Math.floor(def.timeoutMs) : DEFAULT_METRIC_TIMEOUT_MS;
5783
5888
  const timeoutMs = Math.min(requested, MAX_METRIC_TIMEOUT_MS);
5784
5889
  if (timeoutMs < requested) {
5785
5890
  log.warn(TAG19, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
5786
5891
  }
5787
- const run = this.deps.runCommand ?? defaultRunCommand;
5892
+ const run = this.deps.runCommand ?? runMetricCommand;
5788
5893
  let stdout;
5789
5894
  try {
5790
- stdout = run({
5895
+ stdout = await run({
5791
5896
  command: def.command,
5792
5897
  args: def.args ?? [],
5793
5898
  cwd: this.deps.worktreePath,
@@ -5824,6 +5929,13 @@ function blocked(metric, reason, raw) {
5824
5929
  }
5825
5930
  };
5826
5931
  }
5932
+ function configError(metric, reason) {
5933
+ const evidence = blocked(metric, reason);
5934
+ return {
5935
+ ...evidence,
5936
+ structured: { ...evidence.structured, ...GATE_CONFIG_ERROR_MARK }
5937
+ };
5938
+ }
5827
5939
  function describeRunFailure(err, timeoutMs) {
5828
5940
  const e = err;
5829
5941
  if (e?.code === "ENOBUFS") {
@@ -5843,12 +5955,15 @@ function describeRunFailure(err, timeoutMs) {
5843
5955
  function truncate(value, max) {
5844
5956
  return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
5845
5957
  }
5846
- var TAG19 = "command-metric", MAX_RAW_CHARS = 2000, MAX_OUTPUT_BUFFER2, MAX_METRIC_TIMEOUT_MS = 900000;
5958
+ var TAG19 = "command-metric", MAX_RAW_CHARS = 2000, MAX_OUTPUT_BUFFER2, MAX_STDERR_CHARS, MAX_METRIC_TIMEOUT_MS = 900000, METRIC_SIGINT_GRACE_MS = 2000, METRIC_SIGTERM_GRACE_MS = 3000, STDIO_DRAIN_GRACE_MS = 500;
5847
5959
  var init_command_metric = __esm(() => {
5848
5960
  init_dist();
5961
+ init_gate_config_error();
5849
5962
  init_log();
5963
+ init_process_group();
5850
5964
  init_types2();
5851
5965
  MAX_OUTPUT_BUFFER2 = 10 * 1024 * 1024;
5966
+ MAX_STDERR_CHARS = 64 * 1024;
5852
5967
  });
5853
5968
 
5854
5969
  // src/gate-collectors.ts
@@ -7548,7 +7663,7 @@ var init_transitions = __esm(() => {
7548
7663
  });
7549
7664
 
7550
7665
  // src/review-worker.ts
7551
- import { execFileSync as execFileSync11 } from "node:child_process";
7666
+ import { execFileSync as execFileSync10 } from "node:child_process";
7552
7667
 
7553
7668
  class ReviewWorker {
7554
7669
  config;
@@ -7666,7 +7781,7 @@ class ReviewWorker {
7666
7781
  costCents: 0,
7667
7782
  numTurns: 0
7668
7783
  });
7669
- const repoRoot = execFileSync11("git", ["rev-parse", "--show-toplevel"], {
7784
+ const repoRoot = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
7670
7785
  encoding: "utf-8",
7671
7786
  timeout: 5000
7672
7787
  }).trim();
@@ -7735,7 +7850,7 @@ class ReviewWorker {
7735
7850
  return;
7736
7851
  let diff = "";
7737
7852
  try {
7738
- diff = execFileSync11("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
7853
+ diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
7739
7854
  } catch {
7740
7855
  diff = "(unable to retrieve diff)";
7741
7856
  }
@@ -8574,8 +8689,11 @@ var init_prompt = __esm(() => {
8574
8689
  });
8575
8690
 
8576
8691
  // src/stage-advance.ts
8692
+ function gateKindOf(stage) {
8693
+ return stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate";
8694
+ }
8577
8695
  function gateSummary(stage, evaluation) {
8578
- const kind = stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate";
8696
+ const kind = gateKindOf(stage);
8579
8697
  if (evaluation.passed)
8580
8698
  return `${kind} passed`;
8581
8699
  const firstError = evaluation.findings.find((f) => f.level === "error");
@@ -8598,6 +8716,10 @@ async function persistStagePointer(client, card, body) {
8598
8716
  await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
8599
8717
  }
8600
8718
  async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
8719
+ const configErrorDetail = gateConfigErrorReason(evaluation);
8720
+ if (configErrorDetail) {
8721
+ return holdGateMisconfigured(card, stage, configErrorDetail, deps);
8722
+ }
8601
8723
  const loop = getStageLoop(stage);
8602
8724
  if (!isConvergeLoop(loop)) {
8603
8725
  if (!evaluation)
@@ -8606,6 +8728,28 @@ async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
8606
8728
  }
8607
8729
  return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
8608
8730
  }
8731
+ async function holdGateMisconfigured(card, stage, detail, deps) {
8732
+ const reason = `Stage "${stage.name}" cannot be evaluated as configured — ${detail} Holding for a human: this is a configuration defect, so re-running the stage would reach the same result. No attempt was charged.`;
8733
+ deps.sink?.recordStageGateEvaluated({
8734
+ stageId: stage.id,
8735
+ gateId: gateKindOf(stage),
8736
+ verdict: "blocked",
8737
+ evidence: detail,
8738
+ summary: `${gateKindOf(stage)} misconfigured`
8739
+ });
8740
+ await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
8741
+ try {
8742
+ await deps.client.updateAgentProgress(card.id, {
8743
+ agentIdentifier: "claude-code-stage",
8744
+ agentName: "Harmony Agent",
8745
+ status: "waiting",
8746
+ currentTask: reason
8747
+ });
8748
+ } catch {}
8749
+ await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
8750
+ log.info(TAG31, `#${card.short_id} GateMisconfigured: ${reason}`);
8751
+ return { kind: "held_misconfigured", reason };
8752
+ }
8609
8753
  function firstErrorMessage(evaluation) {
8610
8754
  const e = evaluation?.findings.find((f) => f.level === "error");
8611
8755
  return e ? e.message : null;
@@ -8716,7 +8860,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
8716
8860
  const summary = gateSummary(stage, evaluation);
8717
8861
  deps.sink?.recordStageGateEvaluated({
8718
8862
  stageId: stage.id,
8719
- gateId: stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate",
8863
+ gateId: gateKindOf(stage),
8720
8864
  verdict: evaluation.passed ? "passed" : "failed",
8721
8865
  evidence: evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
8722
8866
  `),
@@ -8829,6 +8973,7 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
8829
8973
  var TAG31 = "stage-advance", AGENT_LABEL = "agent";
8830
8974
  var init_stage_advance = __esm(() => {
8831
8975
  init_dist();
8976
+ init_gate_config_error();
8832
8977
  init_log();
8833
8978
  init_transitions();
8834
8979
  });
@@ -11381,7 +11526,7 @@ __export(exports_worktree_gc, {
11381
11526
  isTransientGitNetworkError: () => isTransientGitNetworkError,
11382
11527
  WorktreeGc: () => WorktreeGc
11383
11528
  });
11384
- import { execFileSync as execFileSync12 } from "node:child_process";
11529
+ import { execFileSync as execFileSync11 } from "node:child_process";
11385
11530
  import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
11386
11531
  import { resolve as resolve3 } from "node:path";
11387
11532
  function isTransientGitNetworkError(message) {
@@ -11449,7 +11594,7 @@ function runWorktreeGc(basePath, store, opts = {}) {
11449
11594
  }
11450
11595
  }
11451
11596
  try {
11452
- execFileSync12("git", ["worktree", "prune", "--expire=now"], {
11597
+ execFileSync11("git", ["worktree", "prune", "--expire=now"], {
11453
11598
  cwd: repoRoot,
11454
11599
  stdio: "pipe"
11455
11600
  });
@@ -11480,7 +11625,7 @@ function pruneFailedRemoteBranches(opts) {
11480
11625
  return result;
11481
11626
  }
11482
11627
  try {
11483
- execFileSync12("git", ["fetch", "--prune", "origin"], {
11628
+ execFileSync11("git", ["fetch", "--prune", "origin"], {
11484
11629
  cwd: repoRoot,
11485
11630
  stdio: "pipe",
11486
11631
  ...GIT_NETWORK_EXEC
@@ -11496,7 +11641,7 @@ function pruneFailedRemoteBranches(opts) {
11496
11641
  const refPattern = `refs/remotes/origin/${opts.prefix}*`;
11497
11642
  let listing = "";
11498
11643
  try {
11499
- listing = execFileSync12("git", [
11644
+ listing = execFileSync11("git", [
11500
11645
  "for-each-ref",
11501
11646
  "--format=%(refname:strip=3) %(committerdate:unix)",
11502
11647
  refPattern
@@ -11531,7 +11676,7 @@ function pruneFailedRemoteBranches(opts) {
11531
11676
  break;
11532
11677
  }
11533
11678
  try {
11534
- execFileSync12("git", ["push", "origin", `:refs/heads/${ref}`], {
11679
+ execFileSync11("git", ["push", "origin", `:refs/heads/${ref}`], {
11535
11680
  cwd: repoRoot,
11536
11681
  stdio: "pipe",
11537
11682
  ...GIT_NETWORK_EXEC
@@ -11594,7 +11739,7 @@ class WorktreeGc {
11594
11739
  }
11595
11740
  function getRepoRoot2() {
11596
11741
  try {
11597
- return execFileSync12("git", ["rev-parse", "--show-toplevel"], {
11742
+ return execFileSync11("git", ["rev-parse", "--show-toplevel"], {
11598
11743
  encoding: "utf-8"
11599
11744
  }).trim();
11600
11745
  } catch {
@@ -11635,12 +11780,12 @@ __export(exports_src, {
11635
11780
  validatePrerequisites: () => validatePrerequisites,
11636
11781
  main: () => main
11637
11782
  });
11638
- import { execFileSync as execFileSync13 } from "node:child_process";
11783
+ import { execFileSync as execFileSync12 } from "node:child_process";
11639
11784
  import { randomUUID as randomUUID3 } from "node:crypto";
11640
11785
  import { createRequire as createRequire2 } from "node:module";
11641
11786
  async function validatePrerequisites(config, banner) {
11642
11787
  try {
11643
- const ver = execFileSync13("claude", ["--version"], {
11788
+ const ver = execFileSync12("claude", ["--version"], {
11644
11789
  encoding: "utf-8"
11645
11790
  }).trim();
11646
11791
  banner.check(`Claude CLI ${ver}`);
@@ -11655,14 +11800,14 @@ async function validatePrerequisites(config, banner) {
11655
11800
  validateGitProviderCli(provider);
11656
11801
  }
11657
11802
  try {
11658
- const status = execFileSync13("git", ["status", "--porcelain"], {
11803
+ const status = execFileSync12("git", ["status", "--porcelain"], {
11659
11804
  encoding: "utf-8"
11660
11805
  }).trim();
11661
11806
  if (status) {
11662
11807
  banner.warn(`Working directory has uncommitted changes:
11663
11808
  ${status}`);
11664
11809
  }
11665
- execFileSync13("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
11810
+ execFileSync12("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
11666
11811
  encoding: "utf-8",
11667
11812
  stdio: "pipe"
11668
11813
  });
package/dist/index.js CHANGED
@@ -653,6 +653,14 @@ var init_classification = __esm(() => {
653
653
  MODEL_TIERS = ["simple", "advanced", "research"];
654
654
  });
655
655
 
656
+ // ../harmony-shared/dist/columnSort.js
657
+ var init_columnSort = () => {};
658
+
659
+ // ../harmony-shared/dist/columnCardSplit.js
660
+ var init_columnCardSplit = __esm(() => {
661
+ init_columnSort();
662
+ });
663
+
656
664
  // ../harmony-shared/dist/commentSerializer.js
657
665
  function sanitizeHeaderField(value) {
658
666
  return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
@@ -1439,6 +1447,8 @@ var init_dist = __esm(() => {
1439
1447
  init_branchRef();
1440
1448
  init_cardLinks();
1441
1449
  init_classification();
1450
+ init_columnCardSplit();
1451
+ init_columnSort();
1442
1452
  init_commentSerializer();
1443
1453
  init_constants();
1444
1454
  init_gateEvaluate();
@@ -5683,8 +5693,27 @@ var init_artifact_judge = __esm(() => {
5683
5693
  init_sdk_agent_runner();
5684
5694
  });
5685
5695
 
5696
+ // src/gate-config-error.ts
5697
+ function gateConfigErrorReason(evaluation) {
5698
+ if (!evaluation || evaluation.passed)
5699
+ return null;
5700
+ const structured = evaluation.structured;
5701
+ if (!structured || typeof structured !== "object")
5702
+ return null;
5703
+ if (!Object.hasOwn(structured, GATE_CONFIG_ERROR_KEY))
5704
+ return null;
5705
+ if (structured[GATE_CONFIG_ERROR_KEY] !== true) {
5706
+ return null;
5707
+ }
5708
+ const reason = structured.reason;
5709
+ return typeof reason === "string" && reason.trim().length > 0 ? reason.trim() : "the gate cannot be measured as configured";
5710
+ }
5711
+ var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
5712
+ var init_gate_config_error = __esm(() => {
5713
+ GATE_CONFIG_ERROR_MARK = Object.freeze({ [GATE_CONFIG_ERROR_KEY]: true });
5714
+ });
5715
+
5686
5716
  // src/command-metric.ts
5687
- import { execFileSync as execFileSync10 } from "node:child_process";
5688
5717
  function parseParseMode(parse) {
5689
5718
  if (typeof parse !== "string" || parse.length === 0) {
5690
5719
  return { kind: "invalid", reason: "`parse` must be a non-empty string" };
@@ -5745,15 +5774,91 @@ function parseMetricValue(mode, stdout) {
5745
5774
  }
5746
5775
  return { ok: true, value: resolved };
5747
5776
  }
5748
- function defaultRunCommand(args) {
5749
- const out = execFileSync10(args.command, args.args, {
5750
- cwd: args.cwd,
5751
- timeout: args.timeoutMs,
5752
- stdio: "pipe",
5753
- maxBuffer: MAX_OUTPUT_BUFFER2,
5754
- encoding: "utf8"
5777
+ function runMetricCommand(args) {
5778
+ return new Promise((resolve3, reject) => {
5779
+ let child;
5780
+ try {
5781
+ child = spawnInGroup(args.command, args.args, {
5782
+ cwd: args.cwd,
5783
+ stdio: ["ignore", "pipe", "pipe"]
5784
+ });
5785
+ } catch (err) {
5786
+ reject(err);
5787
+ return;
5788
+ }
5789
+ const pgid = child.pid;
5790
+ const chunks = [];
5791
+ let stdoutBytes = 0;
5792
+ let stderr = "";
5793
+ let settled = false;
5794
+ let killReason = null;
5795
+ let timer;
5796
+ let drainTimer;
5797
+ const settle = (failure) => {
5798
+ if (settled)
5799
+ return;
5800
+ settled = true;
5801
+ if (timer)
5802
+ clearTimeout(timer);
5803
+ if (drainTimer)
5804
+ clearTimeout(drainTimer);
5805
+ reapGroup(pgid);
5806
+ if (failure)
5807
+ reject(failure);
5808
+ else
5809
+ resolve3(Buffer.concat(chunks).toString("utf8"));
5810
+ };
5811
+ const killTree = (reason) => {
5812
+ if (settled || killReason)
5813
+ return;
5814
+ killReason = reason;
5815
+ terminateGroup(child, {
5816
+ sigintTimeoutMs: METRIC_SIGINT_GRACE_MS,
5817
+ sigtermTimeoutMs: METRIC_SIGTERM_GRACE_MS
5818
+ }).catch(() => {}).then(() => {
5819
+ settle(reason === "timeout" ? Object.assign(new Error(`command timed out after ${args.timeoutMs}ms`), { code: "ETIMEDOUT" }) : Object.assign(new Error("stdout maxBuffer exceeded"), {
5820
+ code: "ENOBUFS"
5821
+ }));
5822
+ });
5823
+ };
5824
+ child.stdout?.on("data", (chunk) => {
5825
+ stdoutBytes += chunk.length;
5826
+ if (stdoutBytes > MAX_OUTPUT_BUFFER2) {
5827
+ killTree("overflow");
5828
+ return;
5829
+ }
5830
+ chunks.push(chunk);
5831
+ });
5832
+ child.stderr?.on("data", (chunk) => {
5833
+ if (stderr.length >= MAX_STDERR_CHARS)
5834
+ return;
5835
+ stderr += chunk.toString("utf8");
5836
+ });
5837
+ child.once("error", (err) => settle(err));
5838
+ const settleFromExit = (code, signal) => {
5839
+ if (drainTimer)
5840
+ clearTimeout(drainTimer);
5841
+ if (code === 0) {
5842
+ settle(null);
5843
+ return;
5844
+ }
5845
+ const detail = signal ? `terminated by signal ${signal}` : `exited ${code}`;
5846
+ settle(Object.assign(new Error(`command ${detail}`), {
5847
+ status: typeof code === "number" ? code : null,
5848
+ stderr
5849
+ }));
5850
+ };
5851
+ child.once("exit", (code, signal) => {
5852
+ if (killReason)
5853
+ return;
5854
+ if (timer)
5855
+ clearTimeout(timer);
5856
+ reapGroup(pgid);
5857
+ drainTimer = setTimeout(() => settleFromExit(code, signal), STDIO_DRAIN_GRACE_MS);
5858
+ child.once("close", () => settleFromExit(code, signal));
5859
+ });
5860
+ timer = setTimeout(() => killTree("timeout"), args.timeoutMs);
5755
5861
  });
5756
- return typeof out === "string" ? out : String(out);
5757
5862
  }
5758
5863
 
5759
5864
  class CommandMetricCollector {
@@ -5765,28 +5870,28 @@ class CommandMetricCollector {
5765
5870
  async collect(context) {
5766
5871
  const name = context.gate.metric;
5767
5872
  if (typeof name !== "string" || name.length === 0) {
5768
- return blocked(null, 'Gate kind "custom" needs a `metric` name naming an allowlisted command (e.g. { "kind": "custom", "metric": "lighthouse_performance" }).');
5873
+ return configError(null, 'Gate kind "custom" needs a `metric` name naming an allowlisted command (e.g. { "kind": "custom", "metric": "lighthouse_performance" }).');
5769
5874
  }
5770
5875
  const def = Object.hasOwn(this.deps.metrics, name) ? this.deps.metrics[name] : undefined;
5771
5876
  if (!def) {
5772
- return blocked(name, `Metric "${name}" is not declared in this daemon's allowlist — add it under \`agent.playbooks.metrics\` to permit it.`);
5877
+ return configError(name, `Metric "${name}" is not declared in this daemon's allowlist — add it under \`agent.playbooks.metrics\` to permit it.`);
5773
5878
  }
5774
5879
  if (typeof def.command !== "string" || def.command.length === 0) {
5775
- return blocked(name, `Metric "${name}" declares no \`command\`.`);
5880
+ return configError(name, `Metric "${name}" declares no \`command\`.`);
5776
5881
  }
5777
5882
  const mode = parseParseMode(def.parse);
5778
5883
  if (mode.kind === "invalid") {
5779
- return blocked(name, `Metric "${name}": ${mode.reason}.`);
5884
+ return configError(name, `Metric "${name}": ${mode.reason}.`);
5780
5885
  }
5781
5886
  const requested = typeof def.timeoutMs === "number" && def.timeoutMs > 0 ? Math.floor(def.timeoutMs) : DEFAULT_METRIC_TIMEOUT_MS;
5782
5887
  const timeoutMs = Math.min(requested, MAX_METRIC_TIMEOUT_MS);
5783
5888
  if (timeoutMs < requested) {
5784
5889
  log.warn(TAG19, `Metric "${name}" declares timeoutMs=${requested}, clamped to the ${MAX_METRIC_TIMEOUT_MS}ms ceiling`);
5785
5890
  }
5786
- const run = this.deps.runCommand ?? defaultRunCommand;
5891
+ const run = this.deps.runCommand ?? runMetricCommand;
5787
5892
  let stdout;
5788
5893
  try {
5789
- stdout = run({
5894
+ stdout = await run({
5790
5895
  command: def.command,
5791
5896
  args: def.args ?? [],
5792
5897
  cwd: this.deps.worktreePath,
@@ -5823,6 +5928,13 @@ function blocked(metric, reason, raw) {
5823
5928
  }
5824
5929
  };
5825
5930
  }
5931
+ function configError(metric, reason) {
5932
+ const evidence = blocked(metric, reason);
5933
+ return {
5934
+ ...evidence,
5935
+ structured: { ...evidence.structured, ...GATE_CONFIG_ERROR_MARK }
5936
+ };
5937
+ }
5826
5938
  function describeRunFailure(err, timeoutMs) {
5827
5939
  const e = err;
5828
5940
  if (e?.code === "ENOBUFS") {
@@ -5842,12 +5954,15 @@ function describeRunFailure(err, timeoutMs) {
5842
5954
  function truncate(value, max) {
5843
5955
  return value.length <= max ? value : `${value.slice(0, max)}…[truncated]`;
5844
5956
  }
5845
- var TAG19 = "command-metric", MAX_RAW_CHARS = 2000, MAX_OUTPUT_BUFFER2, MAX_METRIC_TIMEOUT_MS = 900000;
5957
+ var TAG19 = "command-metric", MAX_RAW_CHARS = 2000, MAX_OUTPUT_BUFFER2, MAX_STDERR_CHARS, MAX_METRIC_TIMEOUT_MS = 900000, METRIC_SIGINT_GRACE_MS = 2000, METRIC_SIGTERM_GRACE_MS = 3000, STDIO_DRAIN_GRACE_MS = 500;
5846
5958
  var init_command_metric = __esm(() => {
5847
5959
  init_dist();
5960
+ init_gate_config_error();
5848
5961
  init_log();
5962
+ init_process_group();
5849
5963
  init_types2();
5850
5964
  MAX_OUTPUT_BUFFER2 = 10 * 1024 * 1024;
5965
+ MAX_STDERR_CHARS = 64 * 1024;
5851
5966
  });
5852
5967
 
5853
5968
  // src/gate-collectors.ts
@@ -7547,7 +7662,7 @@ var init_transitions = __esm(() => {
7547
7662
  });
7548
7663
 
7549
7664
  // src/review-worker.ts
7550
- import { execFileSync as execFileSync11 } from "node:child_process";
7665
+ import { execFileSync as execFileSync10 } from "node:child_process";
7551
7666
 
7552
7667
  class ReviewWorker {
7553
7668
  config;
@@ -7665,7 +7780,7 @@ class ReviewWorker {
7665
7780
  costCents: 0,
7666
7781
  numTurns: 0
7667
7782
  });
7668
- const repoRoot = execFileSync11("git", ["rev-parse", "--show-toplevel"], {
7783
+ const repoRoot = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
7669
7784
  encoding: "utf-8",
7670
7785
  timeout: 5000
7671
7786
  }).trim();
@@ -7734,7 +7849,7 @@ class ReviewWorker {
7734
7849
  return;
7735
7850
  let diff = "";
7736
7851
  try {
7737
- diff = execFileSync11("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
7852
+ diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
7738
7853
  } catch {
7739
7854
  diff = "(unable to retrieve diff)";
7740
7855
  }
@@ -8573,8 +8688,11 @@ var init_prompt = __esm(() => {
8573
8688
  });
8574
8689
 
8575
8690
  // src/stage-advance.ts
8691
+ function gateKindOf(stage) {
8692
+ return stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate";
8693
+ }
8576
8694
  function gateSummary(stage, evaluation) {
8577
- const kind = stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate";
8695
+ const kind = gateKindOf(stage);
8578
8696
  if (evaluation.passed)
8579
8697
  return `${kind} passed`;
8580
8698
  const firstError = evaluation.findings.find((f) => f.level === "error");
@@ -8597,6 +8715,10 @@ async function persistStagePointer(client, card, body) {
8597
8715
  await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
8598
8716
  }
8599
8717
  async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
8718
+ const configErrorDetail = gateConfigErrorReason(evaluation);
8719
+ if (configErrorDetail) {
8720
+ return holdGateMisconfigured(card, stage, configErrorDetail, deps);
8721
+ }
8600
8722
  const loop = getStageLoop(stage);
8601
8723
  if (!isConvergeLoop(loop)) {
8602
8724
  if (!evaluation)
@@ -8605,6 +8727,28 @@ async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
8605
8727
  }
8606
8728
  return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
8607
8729
  }
8730
+ async function holdGateMisconfigured(card, stage, detail, deps) {
8731
+ const reason = `Stage "${stage.name}" cannot be evaluated as configured — ${detail} Holding for a human: this is a configuration defect, so re-running the stage would reach the same result. No attempt was charged.`;
8732
+ deps.sink?.recordStageGateEvaluated({
8733
+ stageId: stage.id,
8734
+ gateId: gateKindOf(stage),
8735
+ verdict: "blocked",
8736
+ evidence: detail,
8737
+ summary: `${gateKindOf(stage)} misconfigured`
8738
+ });
8739
+ await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
8740
+ try {
8741
+ await deps.client.updateAgentProgress(card.id, {
8742
+ agentIdentifier: "claude-code-stage",
8743
+ agentName: "Harmony Agent",
8744
+ status: "waiting",
8745
+ currentTask: reason
8746
+ });
8747
+ } catch {}
8748
+ await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
8749
+ log.info(TAG31, `#${card.short_id} GateMisconfigured: ${reason}`);
8750
+ return { kind: "held_misconfigured", reason };
8751
+ }
8608
8752
  function firstErrorMessage(evaluation) {
8609
8753
  const e = evaluation?.findings.find((f) => f.level === "error");
8610
8754
  return e ? e.message : null;
@@ -8715,7 +8859,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
8715
8859
  const summary = gateSummary(stage, evaluation);
8716
8860
  deps.sink?.recordStageGateEvaluated({
8717
8861
  stageId: stage.id,
8718
- gateId: stage.gate && typeof stage.gate === "object" ? String(stage.gate.kind ?? "gate") : "gate",
8862
+ gateId: gateKindOf(stage),
8719
8863
  verdict: evaluation.passed ? "passed" : "failed",
8720
8864
  evidence: evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
8721
8865
  `),
@@ -8828,6 +8972,7 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
8828
8972
  var TAG31 = "stage-advance", AGENT_LABEL = "agent";
8829
8973
  var init_stage_advance = __esm(() => {
8830
8974
  init_dist();
8975
+ init_gate_config_error();
8831
8976
  init_log();
8832
8977
  init_transitions();
8833
8978
  });
@@ -11380,7 +11525,7 @@ __export(exports_worktree_gc, {
11380
11525
  isTransientGitNetworkError: () => isTransientGitNetworkError,
11381
11526
  WorktreeGc: () => WorktreeGc
11382
11527
  });
11383
- import { execFileSync as execFileSync12 } from "node:child_process";
11528
+ import { execFileSync as execFileSync11 } from "node:child_process";
11384
11529
  import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
11385
11530
  import { resolve as resolve3 } from "node:path";
11386
11531
  function isTransientGitNetworkError(message) {
@@ -11448,7 +11593,7 @@ function runWorktreeGc(basePath, store, opts = {}) {
11448
11593
  }
11449
11594
  }
11450
11595
  try {
11451
- execFileSync12("git", ["worktree", "prune", "--expire=now"], {
11596
+ execFileSync11("git", ["worktree", "prune", "--expire=now"], {
11452
11597
  cwd: repoRoot,
11453
11598
  stdio: "pipe"
11454
11599
  });
@@ -11479,7 +11624,7 @@ function pruneFailedRemoteBranches(opts) {
11479
11624
  return result;
11480
11625
  }
11481
11626
  try {
11482
- execFileSync12("git", ["fetch", "--prune", "origin"], {
11627
+ execFileSync11("git", ["fetch", "--prune", "origin"], {
11483
11628
  cwd: repoRoot,
11484
11629
  stdio: "pipe",
11485
11630
  ...GIT_NETWORK_EXEC
@@ -11495,7 +11640,7 @@ function pruneFailedRemoteBranches(opts) {
11495
11640
  const refPattern = `refs/remotes/origin/${opts.prefix}*`;
11496
11641
  let listing = "";
11497
11642
  try {
11498
- listing = execFileSync12("git", [
11643
+ listing = execFileSync11("git", [
11499
11644
  "for-each-ref",
11500
11645
  "--format=%(refname:strip=3) %(committerdate:unix)",
11501
11646
  refPattern
@@ -11530,7 +11675,7 @@ function pruneFailedRemoteBranches(opts) {
11530
11675
  break;
11531
11676
  }
11532
11677
  try {
11533
- execFileSync12("git", ["push", "origin", `:refs/heads/${ref}`], {
11678
+ execFileSync11("git", ["push", "origin", `:refs/heads/${ref}`], {
11534
11679
  cwd: repoRoot,
11535
11680
  stdio: "pipe",
11536
11681
  ...GIT_NETWORK_EXEC
@@ -11593,7 +11738,7 @@ class WorktreeGc {
11593
11738
  }
11594
11739
  function getRepoRoot2() {
11595
11740
  try {
11596
- return execFileSync12("git", ["rev-parse", "--show-toplevel"], {
11741
+ return execFileSync11("git", ["rev-parse", "--show-toplevel"], {
11597
11742
  encoding: "utf-8"
11598
11743
  }).trim();
11599
11744
  } catch {
@@ -11634,12 +11779,12 @@ __export(exports_src, {
11634
11779
  validatePrerequisites: () => validatePrerequisites,
11635
11780
  main: () => main
11636
11781
  });
11637
- import { execFileSync as execFileSync13 } from "node:child_process";
11782
+ import { execFileSync as execFileSync12 } from "node:child_process";
11638
11783
  import { randomUUID as randomUUID3 } from "node:crypto";
11639
11784
  import { createRequire as createRequire2 } from "node:module";
11640
11785
  async function validatePrerequisites(config, banner) {
11641
11786
  try {
11642
- const ver = execFileSync13("claude", ["--version"], {
11787
+ const ver = execFileSync12("claude", ["--version"], {
11643
11788
  encoding: "utf-8"
11644
11789
  }).trim();
11645
11790
  banner.check(`Claude CLI ${ver}`);
@@ -11654,14 +11799,14 @@ async function validatePrerequisites(config, banner) {
11654
11799
  validateGitProviderCli(provider);
11655
11800
  }
11656
11801
  try {
11657
- const status = execFileSync13("git", ["status", "--porcelain"], {
11802
+ const status = execFileSync12("git", ["status", "--porcelain"], {
11658
11803
  encoding: "utf-8"
11659
11804
  }).trim();
11660
11805
  if (status) {
11661
11806
  banner.warn(`Working directory has uncommitted changes:
11662
11807
  ${status}`);
11663
11808
  }
11664
- execFileSync13("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
11809
+ execFileSync12("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
11665
11810
  encoding: "utf-8",
11666
11811
  stdio: "pipe"
11667
11812
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.24.0",
3
+ "version": "1.25.0",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",