@gethmy/agent 1.35.0 → 1.36.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 (4) hide show
  1. package/README.md +6 -1
  2. package/dist/cli.js +363 -207
  3. package/dist/index.js +357 -203
  4. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -820,16 +820,121 @@ function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
820
820
  `);
821
821
  }
822
822
 
823
+ // src/config-merge.ts
824
+ function mergeAgentConfig(defaults, overrides) {
825
+ return mergeObjects(defaults, overrides);
826
+ }
827
+ function isPlainObject(value) {
828
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
829
+ return false;
830
+ }
831
+ const proto = Object.getPrototypeOf(value);
832
+ return proto === Object.prototype || proto === null;
833
+ }
834
+ function mergeObjects(base, override) {
835
+ const result = {};
836
+ for (const key of Object.keys(base)) {
837
+ if (FORBIDDEN_KEYS.has(key))
838
+ continue;
839
+ result[key] = mergeValue(base[key], override[key]);
840
+ }
841
+ for (const key of Object.keys(override)) {
842
+ if (FORBIDDEN_KEYS.has(key))
843
+ continue;
844
+ if (Object.hasOwn(base, key))
845
+ continue;
846
+ if (override[key] === undefined)
847
+ continue;
848
+ result[key] = copy(override[key]);
849
+ }
850
+ return result;
851
+ }
852
+ function mergeValue(base, override) {
853
+ if (override === undefined)
854
+ return copy(base);
855
+ if (isPlainObject(base)) {
856
+ return isPlainObject(override) ? mergeObjects(base, override) : copy(base);
857
+ }
858
+ if (Array.isArray(base)) {
859
+ return Array.isArray(override) ? override.map(copy) : copy(base);
860
+ }
861
+ if (base !== undefined && (isPlainObject(override) || Array.isArray(override))) {
862
+ return copy(base);
863
+ }
864
+ if (Array.isArray(override))
865
+ return override.map(copy);
866
+ return override;
867
+ }
868
+ function findConfigTypeMismatches(defaults, overrides) {
869
+ const found = [];
870
+ walk(defaults, overrides, "agent", found);
871
+ return found;
872
+ }
873
+ function walk(base, override, path, found) {
874
+ for (const key of Object.keys(base)) {
875
+ if (FORBIDDEN_KEYS.has(key))
876
+ continue;
877
+ const baseValue = base[key];
878
+ const overrideValue = override[key];
879
+ if (overrideValue === undefined)
880
+ continue;
881
+ const here = `${path}.${key}`;
882
+ if (isPlainObject(baseValue)) {
883
+ if (isPlainObject(overrideValue)) {
884
+ walk(baseValue, overrideValue, here, found);
885
+ } else {
886
+ found.push(mismatch(here, overrideValue, "a block of settings"));
887
+ }
888
+ } else if (Array.isArray(baseValue) && !Array.isArray(overrideValue)) {
889
+ found.push(mismatch(here, overrideValue, "a list"));
890
+ } else if (baseValue !== undefined && !isPlainObject(baseValue) && !Array.isArray(baseValue) && (isPlainObject(overrideValue) || Array.isArray(overrideValue))) {
891
+ found.push(mismatch(here, overrideValue, "a single value"));
892
+ }
893
+ }
894
+ }
895
+ function mismatch(path, value, expected) {
896
+ return `${path} is ${describe(value)} in your config but ${expected}, so it was IGNORED and the default used. Fix the type or remove the key.`;
897
+ }
898
+ function describe(value) {
899
+ if (value === null)
900
+ return "null";
901
+ if (Array.isArray(value))
902
+ return "a list";
903
+ if (isPlainObject(value))
904
+ return "a block of settings";
905
+ return `a ${typeof value}`;
906
+ }
907
+ function copy(value) {
908
+ if (Array.isArray(value))
909
+ return value.map(copy);
910
+ if (isPlainObject(value)) {
911
+ const out = {};
912
+ for (const key of Object.keys(value)) {
913
+ if (FORBIDDEN_KEYS.has(key))
914
+ continue;
915
+ out[key] = copy(value[key]);
916
+ }
917
+ return out;
918
+ }
919
+ return value;
920
+ }
921
+ var FORBIDDEN_KEYS;
922
+ var init_config_merge = __esm(() => {
923
+ FORBIDDEN_KEYS = new Set(["__proto__", "constructor", "prototype"]);
924
+ });
925
+
823
926
  // src/config-validation.ts
824
927
  var exports_config_validation = {};
825
928
  __export(exports_config_validation, {
826
929
  validateVerificationConfig: () => validateVerificationConfig,
827
930
  validateSweepConfig: () => validateSweepConfig,
828
931
  validateRankingConfig: () => validateRankingConfig,
932
+ validateHttpConfig: () => validateHttpConfig,
829
933
  validateColumnReferences: () => validateColumnReferences,
830
934
  validateBudgetConfig: () => validateBudgetConfig,
831
935
  validateAutoMergeConfig: () => validateAutoMergeConfig,
832
936
  validateAndListColumns: () => validateAndListColumns,
937
+ isLoopbackBindAddr: () => isLoopbackBindAddr,
833
938
  findRemovedConfigKeys: () => findRemovedConfigKeys,
834
939
  REMOVED_CONFIG_KEYS: () => REMOVED_CONFIG_KEYS,
835
940
  ConfigValidationError: () => ConfigValidationError
@@ -945,6 +1050,31 @@ function validateAutoMergeConfig(config) {
945
1050
  throw new ConfigValidationError(`Invalid agent config — ${issues.join("; ")}`, issues);
946
1051
  }
947
1052
  }
1053
+ function isLoopbackBindAddr(addr) {
1054
+ const value = addr.toLowerCase();
1055
+ if (value === "localhost" || value === "::1")
1056
+ return true;
1057
+ const v4 = value.startsWith("::ffff:") ? value.slice("::ffff:".length) : value;
1058
+ const match = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(v4);
1059
+ if (!match)
1060
+ return false;
1061
+ const octets = match.slice(1).map(Number);
1062
+ if (octets.some((octet) => octet > 255))
1063
+ return false;
1064
+ return octets[0] === 127;
1065
+ }
1066
+ function validateHttpConfig(config) {
1067
+ const bindAddr = config.http.bindAddr;
1068
+ const issues = [];
1069
+ if (typeof bindAddr !== "string" || !isLoopbackBindAddr(bindAddr)) {
1070
+ issues.push(`http.bindAddr: ${JSON.stringify(bindAddr)} is not a loopback address — the control server is UNAUTHENTICATED (no token, no Origin or Host check), so binding it anywhere else publishes the daemon's kill switch (\`POST /sweep/stop\`), its per-card commands (\`POST /stop/:cardId\`) and a \`GET /status\` carrying card ids, branch names and spend to every host that can reach that interface. Use 127.0.0.1 — any 127.0.0.0/8 address, ::1 or localhost also work. To reach the daemon from another machine, tunnel to loopback (\`ssh -L ${config.http.port}:127.0.0.1:${config.http.port} <host>\`); there is no shared-secret mode, by design (#1061).`);
1071
+ }
1072
+ if (issues.length > 0) {
1073
+ throw new ConfigValidationError(`Invalid agent config — HTTP control server:
1074
+ - ${issues.join(`
1075
+ - `)}`, issues);
1076
+ }
1077
+ }
948
1078
  function validateSweepConfig(config) {
949
1079
  const sweep = config.sweep;
950
1080
  const issues = [];
@@ -958,7 +1088,7 @@ function validateSweepConfig(config) {
958
1088
  issues.push("sweep.enabled: true with no ceiling at all — sweep.maxCardsPerSweep and budget.dailyBudgetCents are both opted out (-1). A self-claiming daemon needs at least one: set a card cap (e.g. 10) or a daily spend cap (e.g. 40000 for $400.00/day).");
959
1089
  }
960
1090
  if (sweep.enabled && !config.http.enabled) {
961
- issues.push("sweep.enabled: true but http.enabled is false — the sweep kill switch IS the local HTTP server (`POST /sweep/stop`, `harmony-agent sweep stop`), so this configuration ships a self-claiming daemon with no way to stop it, and no way to clear a latched card cap either. Set http.enabled: true.");
1091
+ issues.push("sweep.enabled: true but http.enabled is false — the local sweep kill switch IS the local HTTP server (`POST /sweep/stop`, `harmony-agent sweep stop`), so this configuration ships a self-claiming daemon without it. The board can stop and resume the sweep too (#1060), but only while this daemon still reaches the API and realtime — and that is exactly what is broken when you most need a stop. Set http.enabled: true.");
962
1092
  }
963
1093
  if (sweep.enabled && config.pickupColumns.length === 0) {
964
1094
  issues.push("sweep.enabled: true but pickupColumns is empty — the sweep has no column to claim from");
@@ -1461,9 +1591,9 @@ function isGateOperator(value) {
1461
1591
  return typeof value === "string" && GATE_OPERATORS.includes(value);
1462
1592
  }
1463
1593
  function gateEvaluate(gateSpec, evidence) {
1464
- const structured = isPlainObject(evidence) ? evidence.structured ?? {} : {};
1465
- const safeStructured = isPlainObject(structured) ? structured : {};
1466
- if (!isPlainObject(gateSpec)) {
1594
+ const structured = isPlainObject2(evidence) ? evidence.structured ?? {} : {};
1595
+ const safeStructured = isPlainObject2(structured) ? structured : {};
1596
+ if (!isPlainObject2(gateSpec)) {
1467
1597
  return {
1468
1598
  passed: false,
1469
1599
  findings: [{ level: "error", message: "Malformed gate: not an object." }],
@@ -1495,7 +1625,7 @@ function gateEvaluate(gateSpec, evidence) {
1495
1625
  structured: safeStructured
1496
1626
  };
1497
1627
  }
1498
- if (!isPlainObject(evidence)) {
1628
+ if (!isPlainObject2(evidence)) {
1499
1629
  return {
1500
1630
  passed: false,
1501
1631
  findings: [
@@ -1568,7 +1698,7 @@ function gateEvaluate(gateSpec, evidence) {
1568
1698
  return { passed: predicatePassed, findings, structured: safeStructured };
1569
1699
  }
1570
1700
  function evaluateCondition(raw, structured) {
1571
- if (!isPlainObject(raw)) {
1701
+ if (!isPlainObject2(raw)) {
1572
1702
  return {
1573
1703
  ok: false,
1574
1704
  finding: {
@@ -1646,7 +1776,7 @@ function evaluateCondition(raw, structured) {
1646
1776
  }
1647
1777
  };
1648
1778
  }
1649
- function isPlainObject(value) {
1779
+ function isPlainObject2(value) {
1650
1780
  return typeof value === "object" && value !== null && !Array.isArray(value);
1651
1781
  }
1652
1782
  function blockedDetail(structured) {
@@ -3018,97 +3148,8 @@ function loadDaemonConfig() {
3018
3148
  if (typeof parsed.agentColor === "string" && parsed.agentColor.trim())
3019
3149
  agentColor = parsed.agentColor.trim();
3020
3150
  } catch {}
3021
- const agent = {
3022
- ...DEFAULT_AGENT_CONFIG,
3023
- ...agentOverrides,
3024
- completion: {
3025
- ...DEFAULT_AGENT_CONFIG.completion,
3026
- ...agentOverrides.completion ?? {}
3027
- },
3028
- ranking: {
3029
- ...DEFAULT_AGENT_CONFIG.ranking,
3030
- ...agentOverrides.ranking ?? {}
3031
- },
3032
- claude: {
3033
- ...DEFAULT_AGENT_CONFIG.claude,
3034
- ...agentOverrides.claude ?? {}
3035
- },
3036
- worktree: {
3037
- ...DEFAULT_AGENT_CONFIG.worktree,
3038
- ...agentOverrides.worktree ?? {}
3039
- },
3040
- verification: {
3041
- ...DEFAULT_AGENT_CONFIG.verification,
3042
- ...agentOverrides.verification ?? {}
3043
- },
3044
- review: {
3045
- ...DEFAULT_AGENT_CONFIG.review,
3046
- ...agentOverrides.review ?? {},
3047
- autoMerge: {
3048
- ...DEFAULT_AGENT_CONFIG.review.autoMerge,
3049
- ...agentOverrides.review?.autoMerge ?? {},
3050
- ciRepair: {
3051
- ...DEFAULT_AGENT_CONFIG.review.autoMerge.ciRepair,
3052
- ...agentOverrides.review?.autoMerge?.ciRepair ?? {},
3053
- patch: {
3054
- ...DEFAULT_AGENT_CONFIG.review.autoMerge.ciRepair.patch,
3055
- ...agentOverrides.review?.autoMerge?.ciRepair?.patch ?? {}
3056
- }
3057
- },
3058
- independentReview: {
3059
- ...DEFAULT_AGENT_CONFIG.review.autoMerge.independentReview,
3060
- ...agentOverrides.review?.autoMerge?.independentReview ?? {},
3061
- verdict: {
3062
- ...DEFAULT_AGENT_CONFIG.review.autoMerge.independentReview.verdict,
3063
- ...agentOverrides.review?.autoMerge?.independentReview?.verdict ?? {}
3064
- }
3065
- }
3066
- }
3067
- },
3068
- budget: {
3069
- ...DEFAULT_AGENT_CONFIG.budget,
3070
- ...agentOverrides.budget ?? {},
3071
- pause: {
3072
- ...DEFAULT_AGENT_CONFIG.budget.pause,
3073
- ...agentOverrides.budget?.pause ?? {}
3074
- }
3075
- },
3076
- sdk: {
3077
- ...DEFAULT_AGENT_CONFIG.sdk,
3078
- ...agentOverrides.sdk ?? {}
3079
- },
3080
- http: {
3081
- ...DEFAULT_AGENT_CONFIG.http,
3082
- ...agentOverrides.http ?? {}
3083
- },
3084
- timing: {
3085
- ...DEFAULT_AGENT_CONFIG.timing,
3086
- ...agentOverrides.timing ?? {}
3087
- },
3088
- planning: {
3089
- ...DEFAULT_AGENT_CONFIG.planning,
3090
- ...agentOverrides.planning ?? {}
3091
- },
3092
- playbooks: {
3093
- ...DEFAULT_AGENT_CONFIG.playbooks,
3094
- ...agentOverrides.playbooks ?? {}
3095
- },
3096
- contractFirst: {
3097
- ...DEFAULT_AGENT_CONFIG.contractFirst,
3098
- ...agentOverrides.contractFirst ?? {}
3099
- },
3100
- boardReview: {
3101
- ...DEFAULT_AGENT_CONFIG.boardReview,
3102
- ...agentOverrides.boardReview ?? {}
3103
- },
3104
- sweep: {
3105
- ...DEFAULT_AGENT_CONFIG.sweep,
3106
- ...agentOverrides.sweep ?? {},
3107
- trustedAuthors: [
3108
- ...agentOverrides.sweep?.trustedAuthors ?? DEFAULT_AGENT_CONFIG.sweep.trustedAuthors
3109
- ]
3110
- }
3111
- };
3151
+ const agent = mergeAgentConfig(DEFAULT_AGENT_CONFIG, agentOverrides);
3152
+ configWarnings.push(...findConfigTypeMismatches(DEFAULT_AGENT_CONFIG, agentOverrides));
3112
3153
  if (agent.runner !== "cli" && agent.runner !== "sdk") {
3113
3154
  agent.runner = DEFAULT_AGENT_CONFIG.runner;
3114
3155
  }
@@ -3180,6 +3221,7 @@ function createApiClient(config) {
3180
3221
  }
3181
3222
  var NOT_A_REPOSITORY_EXIT = 128;
3182
3223
  var init_config = __esm(() => {
3224
+ init_config_merge();
3183
3225
  init_config_validation();
3184
3226
  init_types2();
3185
3227
  });
@@ -3210,6 +3252,9 @@ class HttpServer {
3210
3252
  return this.boundPort;
3211
3253
  }
3212
3254
  async start() {
3255
+ if (!isLoopbackBindAddr(this.opts.bindAddr)) {
3256
+ throw new Error(`refusing to bind the unauthenticated control server to ${JSON.stringify(this.opts.bindAddr)} — loopback only (127.0.0.0/8, ::1, localhost), see validateHttpConfig (#1061)`);
3257
+ }
3213
3258
  this.server = createServer((req, res) => {
3214
3259
  this.route(req, res).catch((err) => {
3215
3260
  log5.error(TAG5, `unhandled: ${err instanceof Error ? err.message : err}`);
@@ -3422,7 +3467,9 @@ function busy(w) {
3422
3467
  return w.cardId !== null;
3423
3468
  }
3424
3469
  var TAG5 = "http";
3425
- var init_http_server = () => {};
3470
+ var init_http_server = __esm(() => {
3471
+ init_config_validation();
3472
+ });
3426
3473
 
3427
3474
  // src/identity.ts
3428
3475
  function resolveDaemonUserId(userEmail, members, apiKeyUserId) {
@@ -3486,13 +3533,13 @@ function extractScratchTrees(cleanWorktree, commitish) {
3486
3533
  function changedPaths(changes) {
3487
3534
  return [...changes.modified, ...changes.added, ...changes.deleted];
3488
3535
  }
3489
- function walk(root, dir = root, out = []) {
3536
+ function walk2(root, dir = root, out = []) {
3490
3537
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
3491
3538
  const full = join2(dir, entry.name);
3492
3539
  if (entry.isSymbolicLink())
3493
3540
  continue;
3494
3541
  if (entry.isDirectory()) {
3495
- walk(root, full, out);
3542
+ walk2(root, full, out);
3496
3543
  } else if (entry.isFile()) {
3497
3544
  out.push(relative(root, full).split(sep).join("/"));
3498
3545
  }
@@ -3500,8 +3547,8 @@ function walk(root, dir = root, out = []) {
3500
3547
  return out;
3501
3548
  }
3502
3549
  function diffTrees(pristine, scratch) {
3503
- const before = new Set(walk(pristine));
3504
- const after = walk(scratch);
3550
+ const before = new Set(walk2(pristine));
3551
+ const after = walk2(scratch);
3505
3552
  const afterSet = new Set(after);
3506
3553
  const modified = [];
3507
3554
  const added = [];
@@ -4915,7 +4962,7 @@ async function settlePlanCriteria(client, tag, card) {
4915
4962
  if (divergent.length > 0) {
4916
4963
  log13.warn(tag, describeDivergence(card.short_id, card.plan_id, divergent));
4917
4964
  }
4918
- const settleGroup = async (criteria, planIdOf, describe) => {
4965
+ const settleGroup = async (criteria, planIdOf, describe2) => {
4919
4966
  if (criteria.length === 0)
4920
4967
  return;
4921
4968
  try {
@@ -4924,7 +4971,7 @@ async function settlePlanCriteria(client, tag, card) {
4924
4971
  status: CRITERION_COMPLETED
4925
4972
  });
4926
4973
  }
4927
- log13.info(tag, describe());
4974
+ log13.info(tag, describe2());
4928
4975
  } catch (err) {
4929
4976
  log13.warn(tag, `plan criteria write-back failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4930
4977
  }
@@ -13114,6 +13161,7 @@ __export(exports_port_registry, {
13114
13161
  recordDaemonPort: () => recordDaemonPort,
13115
13162
  lookupDaemonPort: () => lookupDaemonPort,
13116
13163
  defaultRegistryPath: () => defaultRegistryPath,
13164
+ daemonBaseUrl: () => daemonBaseUrl,
13117
13165
  clearDaemonPort: () => clearDaemonPort
13118
13166
  });
13119
13167
  import {
@@ -13160,6 +13208,10 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
13160
13208
  log33.warn(TAG30, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
13161
13209
  }
13162
13210
  }
13211
+ function daemonBaseUrl(bindAddr, port) {
13212
+ const host = bindAddr.includes(":") ? `[${bindAddr}]` : bindAddr;
13213
+ return `http://${host}:${port}`;
13214
+ }
13163
13215
  function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
13164
13216
  const registry = load(path);
13165
13217
  return registry[projectId] ?? null;
@@ -13844,8 +13896,63 @@ var init_sweep = __esm(() => {
13844
13896
  init_unblock();
13845
13897
  });
13846
13898
 
13847
- // src/sweep-guard.ts
13899
+ // src/sweep-commands.ts
13848
13900
  import { log as log38 } from "@gethmy/harness";
13901
+ function parseSweepCommands(payload) {
13902
+ if (!payload || typeof payload !== "object")
13903
+ return [];
13904
+ const rows = payload.commands;
13905
+ if (!Array.isArray(rows))
13906
+ return [];
13907
+ const parsed = [];
13908
+ for (const row of rows) {
13909
+ if (!row || typeof row !== "object")
13910
+ continue;
13911
+ const { id, verb, createdAt } = row;
13912
+ if (typeof id !== "string" || !id)
13913
+ continue;
13914
+ if (verb !== "stop" && verb !== "resume")
13915
+ continue;
13916
+ parsed.push({
13917
+ id,
13918
+ verb,
13919
+ createdAt: typeof createdAt === "string" ? createdAt : ""
13920
+ });
13921
+ }
13922
+ return parsed.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
13923
+ }
13924
+ async function claimSweepCommands(client, agentId, projectId, daemonId) {
13925
+ const request = client.request?.bind(client);
13926
+ if (typeof request !== "function") {
13927
+ log38.warn(TAG35, "installed @gethmy/mcp has no generic request method — board sweep commands cannot be claimed; use `harmony-agent sweep stop|resume` until it is upgraded");
13928
+ return [];
13929
+ }
13930
+ try {
13931
+ const res = await request("POST", `/agents/${encodeURIComponent(agentId)}/sweep-commands/claim`, { projectId, daemonId });
13932
+ return parseSweepCommands(res);
13933
+ } catch (err) {
13934
+ log38.warn(TAG35, `could not claim sweep commands: ${err instanceof Error ? err.message : err}`);
13935
+ return [];
13936
+ }
13937
+ }
13938
+ async function applySweepCommands(commands, actions) {
13939
+ let applied = null;
13940
+ for (const command of commands) {
13941
+ if (command.verb === "stop") {
13942
+ await actions.halt();
13943
+ } else {
13944
+ await actions.resume();
13945
+ }
13946
+ applied = command.verb;
13947
+ log38.info(TAG35, `applied "${command.verb}" from the board (${command.id})`);
13948
+ }
13949
+ return applied;
13950
+ }
13951
+ var TAG35 = "sweep-cmd";
13952
+ var init_sweep_commands = () => {};
13953
+
13954
+ // src/sweep-guard.ts
13955
+ import { log as log39 } from "@gethmy/harness";
13849
13956
 
13850
13957
  class SweepGuard {
13851
13958
  config;
@@ -13901,7 +14008,7 @@ class SweepGuard {
13901
14008
  await this.store.haltSweep(reason);
13902
14009
  if (!already) {
13903
14010
  const claimed = this.store.getSweep().claimed;
13904
- log38.info(TAG35, `claiming stopped (${reason}) after ${claimed} claimed`);
14011
+ log39.info(TAG36, `claiming stopped (${reason}) after ${claimed} claimed`);
13905
14012
  }
13906
14013
  return this.snapshot();
13907
14014
  }
@@ -13910,7 +14017,7 @@ class SweepGuard {
13910
14017
  const was = before.haltReason ? this.describeStop(before.haltReason) : null;
13911
14018
  await this.store.resumeSweep();
13912
14019
  if (was)
13913
- log38.info(TAG35, `claiming resumed by the operator — was stopped: ${was}`);
14020
+ log39.info(TAG36, `claiming resumed by the operator — was stopped: ${was}`);
13914
14021
  return this.snapshot();
13915
14022
  }
13916
14023
  snapshot() {
@@ -13972,11 +14079,11 @@ function sweepBannerLine(config) {
13972
14079
  const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
13973
14080
  return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
13974
14081
  }
13975
- var TAG35 = "sweep";
14082
+ var TAG36 = "sweep";
13976
14083
  var init_sweep_guard = () => {};
13977
14084
 
13978
14085
  // src/reconcile.ts
13979
- import { detectGitProvider as detectGitProvider5, log as log39 } from "@gethmy/harness";
14086
+ import { detectGitProvider as detectGitProvider5, log as log40 } from "@gethmy/harness";
13980
14087
  function daemonProcessLabels(approvedLabel) {
13981
14088
  const names = ["agent", "agent-recovered", NEED_REVIEW_LABEL, approvedLabel];
13982
14089
  return new Set(names.filter(Boolean).map((n) => n.toLowerCase()));
@@ -14007,6 +14114,8 @@ class Reconciler {
14007
14114
  repoDirectories = null;
14008
14115
  lastSweepStopReported = null;
14009
14116
  publishSweep = () => {};
14117
+ drainingSweepCommands = false;
14118
+ lastSweepNudgeAt = 0;
14010
14119
  get lastTick() {
14011
14120
  return this.lastTickAt;
14012
14121
  }
@@ -14036,7 +14145,7 @@ class Reconciler {
14036
14145
  clearInterval(this.timer);
14037
14146
  this.timer = null;
14038
14147
  }
14039
- log39.info(TAG36, "Heartbeat stopped");
14148
+ log40.info(TAG37, "Heartbeat stopped");
14040
14149
  }
14041
14150
  async recoverStaleRuns() {
14042
14151
  if (!this.stateStore || !this.agentConfig)
@@ -14047,7 +14156,7 @@ class Reconciler {
14047
14156
  const pool = this.pool;
14048
14157
  for (const run of active) {
14049
14158
  if (isBudgetHeldRun(run)) {
14050
- log39.info(TAG36, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
14159
+ log40.info(TAG37, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
14051
14160
  continue;
14052
14161
  }
14053
14162
  const foreignDaemon = run.daemonPid !== process.pid;
@@ -14057,7 +14166,7 @@ class Reconciler {
14057
14166
  if (!daemonDead && !(heartbeatStale && ourZombie))
14058
14167
  continue;
14059
14168
  const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
14060
- log39.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
14169
+ log40.warn(TAG37, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
14061
14170
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
14062
14171
  runId: run.runId,
14063
14172
  cardId: run.cardId,
@@ -14084,11 +14193,11 @@ class Reconciler {
14084
14193
  const stalledAt = Date.parse(card.updated_at ?? "");
14085
14194
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
14086
14195
  continue;
14087
- log39.warn(TAG36, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
14196
+ log40.warn(TAG37, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
14088
14197
  try {
14089
14198
  await this.client.moveCard(card.id, pickupCol.id);
14090
14199
  } catch (err) {
14091
- log39.error(TAG36, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
14200
+ log40.error(TAG37, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
14092
14201
  }
14093
14202
  }
14094
14203
  }
@@ -14120,7 +14229,7 @@ class Reconciler {
14120
14229
  return;
14121
14230
  const cardLabels = resolveCardLabels(card, labelMap);
14122
14231
  const subtasks = card.subtasks ?? [];
14123
- log39.info(TAG36, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
14232
+ log40.info(TAG37, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
14124
14233
  await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
14125
14234
  }
14126
14235
  });
@@ -14144,11 +14253,11 @@ class Reconciler {
14144
14253
  const parkedAt = Date.parse(card.updated_at ?? "");
14145
14254
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
14146
14255
  continue;
14147
- log39.warn(TAG36, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
14256
+ log40.warn(TAG37, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
14148
14257
  try {
14149
14258
  await this.client.moveCard(card.id, pickupCol.id);
14150
14259
  } catch (err) {
14151
- log39.error(TAG36, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
14260
+ log40.error(TAG37, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
14152
14261
  }
14153
14262
  }
14154
14263
  }
@@ -14183,7 +14292,7 @@ class Reconciler {
14183
14292
  }
14184
14293
  this.lastSweepStopReported = null;
14185
14294
  if (!this.pool.hasFreeImplementSlot()) {
14186
- log39.debug(TAG36, "sweep skipped — no free implement slot");
14295
+ log40.debug(TAG37, "sweep skipped — no free implement slot");
14187
14296
  return;
14188
14297
  }
14189
14298
  const { claimed } = await sweepForCard({
@@ -14202,9 +14311,9 @@ class Reconciler {
14202
14311
  reportSweepStopped(reason, detail) {
14203
14312
  const line = `sweep stopped claiming — ${detail}`;
14204
14313
  if (this.lastSweepStopReported === reason)
14205
- log39.debug(TAG36, line);
14314
+ log40.debug(TAG37, line);
14206
14315
  else
14207
- log39.warn(TAG36, line);
14316
+ log40.warn(TAG37, line);
14208
14317
  this.lastSweepStopReported = reason;
14209
14318
  }
14210
14319
  setSweepPresenceSink(sink) {
@@ -14213,15 +14322,47 @@ class Reconciler {
14213
14322
  publishSweepState() {
14214
14323
  this.publishSweep(toSweepPresence(this.sweepGuard?.snapshot()));
14215
14324
  }
14325
+ async drainSweepCommands() {
14326
+ const guard = this.sweepGuard;
14327
+ if (!guard || !this.agentConfig?.sweep.enabled)
14328
+ return;
14329
+ if (this.drainingSweepCommands)
14330
+ return;
14331
+ this.drainingSweepCommands = true;
14332
+ try {
14333
+ const commands = await claimSweepCommands(this.client, this.agentId, this.projectId, this.stateStore?.getDaemon().daemonId ?? "");
14334
+ if (commands.length === 0)
14335
+ return;
14336
+ const applied = await applySweepCommands(commands, {
14337
+ halt: () => guard.halt("operator"),
14338
+ resume: () => guard.resume()
14339
+ });
14340
+ if (!applied)
14341
+ return;
14342
+ this.lastSweepStopReported = null;
14343
+ this.publishSweepState();
14344
+ } finally {
14345
+ this.drainingSweepCommands = false;
14346
+ }
14347
+ }
14348
+ onSweepCommandNudge() {
14349
+ const now = Date.now();
14350
+ if (now - this.lastSweepNudgeAt < SWEEP_NUDGE_COOLDOWN_MS)
14351
+ return;
14352
+ this.lastSweepNudgeAt = now;
14353
+ this.drainSweepCommands().catch((err) => {
14354
+ log40.error(TAG37, `sweep command nudge failed: ${err instanceof Error ? err.message : err}`);
14355
+ });
14356
+ }
14216
14357
  resolveRepoDirectories() {
14217
14358
  if (this.repoDirectories)
14218
14359
  return this.repoDirectories;
14219
14360
  const dirs = readRepoDirectories(process.cwd());
14220
14361
  this.repoDirectories = dirs;
14221
14362
  if (dirs.size === 0) {
14222
- log39.warn(TAG36, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
14363
+ log40.warn(TAG37, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
14223
14364
  } else {
14224
- log39.debug(TAG36, `Path signal allows ${dirs.size} top-level directories`);
14365
+ log40.debug(TAG37, `Path signal allows ${dirs.size} top-level directories`);
14225
14366
  }
14226
14367
  return dirs;
14227
14368
  }
@@ -14265,21 +14406,21 @@ class Reconciler {
14265
14406
  const subtasks = card.subtasks ?? [];
14266
14407
  const mode = route.mode;
14267
14408
  if (route.stage) {
14268
- log39.info(TAG36, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
14409
+ log40.info(TAG37, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
14269
14410
  }
14270
14411
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
14271
- log39.debug(TAG36, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
14412
+ log40.debug(TAG37, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
14272
14413
  continue;
14273
14414
  }
14274
14415
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
14275
- log39.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
14416
+ log40.debug(TAG37, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
14276
14417
  continue;
14277
14418
  }
14278
14419
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
14279
- log39.debug(TAG36, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
14420
+ log40.debug(TAG37, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
14280
14421
  continue;
14281
14422
  }
14282
- log39.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
14423
+ log40.info(TAG37, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
14283
14424
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
14284
14425
  }
14285
14426
  }
@@ -14289,29 +14430,34 @@ class Reconciler {
14289
14430
  try {
14290
14431
  await this.pool.drainBudgetDecisions();
14291
14432
  } catch (err) {
14292
- log39.error(TAG36, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
14433
+ log40.error(TAG37, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
14293
14434
  }
14294
14435
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
14295
14436
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
14296
14437
  for (const knownId of knownCardIds) {
14297
14438
  if (!allAgentCardIds.has(knownId)) {
14298
- log39.info(TAG36, `Missed unassign: ${knownId} — removing`);
14439
+ log40.info(TAG37, `Missed unassign: ${knownId} — removing`);
14299
14440
  await this.pool.removeCard(knownId);
14300
14441
  }
14301
14442
  }
14302
14443
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
14444
+ try {
14445
+ await this.drainSweepCommands();
14446
+ } catch (err) {
14447
+ log40.error(TAG37, `sweep commands were not drained this tick: ${err instanceof Error ? err.message : err}`);
14448
+ }
14303
14449
  try {
14304
14450
  await this.sweepForWork(cards, columns, labelMap, pickupConfig);
14305
14451
  } catch (err) {
14306
- log39.error(TAG36, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
14452
+ log40.error(TAG37, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
14307
14453
  }
14308
- log39.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
14454
+ log40.debug(TAG37, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
14309
14455
  } catch (err) {
14310
- log39.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
14456
+ log40.error(TAG37, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
14311
14457
  }
14312
14458
  }
14313
14459
  }
14314
- var TAG36 = "reconcile";
14460
+ var TAG37 = "reconcile", SWEEP_NUDGE_COOLDOWN_MS = 15000;
14315
14461
  var init_reconcile = __esm(() => {
14316
14462
  init_board_helpers();
14317
14463
  init_recovery();
@@ -14320,6 +14466,7 @@ var init_reconcile = __esm(() => {
14320
14466
  init_state_store();
14321
14467
  init_strand_recovery();
14322
14468
  init_sweep();
14469
+ init_sweep_commands();
14323
14470
  init_sweep_guard();
14324
14471
  init_types2();
14325
14472
  });
@@ -14330,7 +14477,7 @@ __export(exports_startup_banner, {
14330
14477
  createStartupBanner: () => createStartupBanner,
14331
14478
  contextSourceLine: () => contextSourceLine
14332
14479
  });
14333
- import { isPretty, log as log40 } from "@gethmy/harness";
14480
+ import { isPretty, log as log41 } from "@gethmy/harness";
14334
14481
  function createStartupBanner(config, version) {
14335
14482
  return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
14336
14483
  }
@@ -14355,7 +14502,7 @@ function prettyBanner(config, version) {
14355
14502
  checks.push({ kind: "ok", message });
14356
14503
  },
14357
14504
  warn(message) {
14358
- log40.warn(TAG37, message);
14505
+ log41.warn(TAG38, message);
14359
14506
  checks.push({ kind: "warn", message: message.split(`
14360
14507
  `, 1)[0] });
14361
14508
  },
@@ -14380,26 +14527,26 @@ function prettyBanner(config, version) {
14380
14527
  };
14381
14528
  }
14382
14529
  function jsonBanner(config, version) {
14383
- log40.info(TAG37, `Harmony Agent Daemon v${version} starting...`);
14384
- log40.info(TAG37, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
14385
- log40.info(TAG37, `Config: ${contextSourceLine(config)}`);
14530
+ log41.info(TAG38, `Harmony Agent Daemon v${version} starting...`);
14531
+ log41.info(TAG38, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
14532
+ log41.info(TAG38, `Config: ${contextSourceLine(config)}`);
14386
14533
  if (config.agent.review.enabled) {
14387
- log40.info(TAG37, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
14534
+ log41.info(TAG38, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
14388
14535
  }
14389
14536
  let failed = false;
14390
14537
  return {
14391
14538
  setProjectName(_name) {},
14392
14539
  setGitProvider(provider) {
14393
- log40.info(TAG37, `Git provider: ${provider}`);
14540
+ log41.info(TAG38, `Git provider: ${provider}`);
14394
14541
  },
14395
14542
  setHttpPort(port) {
14396
- log40.info(TAG37, `HTTP server on port ${port}`);
14543
+ log41.info(TAG38, `HTTP server on port ${port}`);
14397
14544
  },
14398
14545
  check(message) {
14399
- log40.info(TAG37, message);
14546
+ log41.info(TAG38, message);
14400
14547
  },
14401
14548
  warn(message) {
14402
- log40.warn(TAG37, message);
14549
+ log41.warn(TAG38, message);
14403
14550
  },
14404
14551
  fail() {
14405
14552
  failed = true;
@@ -14407,7 +14554,7 @@ function jsonBanner(config, version) {
14407
14554
  async ready(message) {
14408
14555
  if (failed)
14409
14556
  return;
14410
- log40.info(TAG37, message);
14557
+ log41.info(TAG38, message);
14411
14558
  }
14412
14559
  };
14413
14560
  }
@@ -14459,7 +14606,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
14459
14606
  if (gitProvider)
14460
14607
  tail.push(gitProvider);
14461
14608
  if (config.agent.http.enabled) {
14462
- tail.push(`HTTP http://${config.agent.http.bindAddr}:${httpPort ?? config.agent.http.port}`);
14609
+ tail.push(`HTTP ${daemonBaseUrl(config.agent.http.bindAddr, httpPort ?? config.agent.http.port)}`);
14463
14610
  }
14464
14611
  if (tail.length > 0) {
14465
14612
  rows.push({ label: "Git", value: tail.join(" · ") });
@@ -14496,8 +14643,9 @@ function cyan(s) {
14496
14643
  function yellow(s) {
14497
14644
  return `${ANSI.yellow}${s}${ANSI.reset}`;
14498
14645
  }
14499
- var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
14646
+ var TAG38 = "daemon", RULE_WIDTH = 70, ANSI;
14500
14647
  var init_startup_banner = __esm(() => {
14648
+ init_port_registry();
14501
14649
  ANSI = {
14502
14650
  reset: "\x1B[0m",
14503
14651
  dim: "\x1B[2m",
@@ -14516,7 +14664,7 @@ import {
14516
14664
  writeFileSync as writeFileSync5
14517
14665
  } from "node:fs";
14518
14666
  import { dirname as dirname6, resolve as resolve2, sep as sep2 } from "node:path";
14519
- import { log as log41 } from "@gethmy/harness";
14667
+ import { log as log42 } from "@gethmy/harness";
14520
14668
  function migrationMarkerPath(targetPath) {
14521
14669
  return `${targetPath}.migrated`;
14522
14670
  }
@@ -14591,7 +14739,7 @@ function migrateLegacyStateFile(opts) {
14591
14739
  throw new Error("not an object");
14592
14740
  legacy = parsed;
14593
14741
  } catch (err) {
14594
- log41.warn(TAG38, `legacy state file unreadable — skipping the split this start; it retries on the next (${err instanceof Error ? err.message : err})`);
14742
+ log42.warn(TAG39, `legacy state file unreadable — skipping the split this start; it retries on the next (${err instanceof Error ? err.message : err})`);
14595
14743
  return NO_MIGRATION;
14596
14744
  }
14597
14745
  const runs = Array.isArray(legacy.runs) ? legacy.runs : [];
@@ -14613,13 +14761,13 @@ function migrateLegacyStateFile(opts) {
14613
14761
  const legacyVersion = typeof legacy.version === "number" ? legacy.version : SCHEMA_VERSION;
14614
14762
  const existing = existsSync4(targetPath) ? readTargetFile(targetPath) : null;
14615
14763
  if (existsSync4(targetPath) && !existing) {
14616
- log41.warn(TAG38, "existing project state file unreadable — the split overwrites it");
14764
+ log42.warn(TAG39, "existing project state file unreadable — the split overwrites it");
14617
14765
  }
14618
14766
  let target;
14619
14767
  if (existing) {
14620
14768
  const existingVersion = typeof existing.version === "number" ? existing.version : SCHEMA_VERSION;
14621
14769
  if (existingVersion !== legacyVersion) {
14622
- log41.warn(TAG38, `legacy state is schema v${legacyVersion} but the project store is v${existingVersion} — skipping the split`);
14770
+ log42.warn(TAG39, `legacy state is schema v${legacyVersion} but the project store is v${existingVersion} — skipping the split`);
14623
14771
  return NO_MIGRATION;
14624
14772
  }
14625
14773
  const existingRuns = Array.isArray(existing.runs) ? existing.runs : [];
@@ -14665,7 +14813,7 @@ function migrateLegacyStateFile(opts) {
14665
14813
  if (!existsSync4(backupPath))
14666
14814
  copyFileSync(legacyPath, backupPath);
14667
14815
  } catch (err) {
14668
- log41.warn(TAG38, `could not back up the legacy state file (continuing): ${err instanceof Error ? err.message : err}`);
14816
+ log42.warn(TAG39, `could not back up the legacy state file (continuing): ${err instanceof Error ? err.message : err}`);
14669
14817
  }
14670
14818
  const remainingRuns = runs.filter((r) => !ownRunIds.has(r.runId));
14671
14819
  const remainingCards = cards.filter((c) => !ownCardIds.has(c.cardId));
@@ -14689,10 +14837,10 @@ function migrateLegacyStateFile(opts) {
14689
14837
  copiedIndeterminateCards: indeterminateCards.length,
14690
14838
  foreignUnendedRuns: remainingRuns.filter((r) => r.endedAt === null).length
14691
14839
  };
14692
- log41.info(TAG38, `split legacy state into ${targetPath}: moved ${result.movedRuns} run(s) ` + `(${result.movedUnendedRuns} unended) and ${result.movedCards} card record(s), ` + `copied ${result.copiedIndeterminateCards} unattributable card record(s); ` + `left ${remainingRuns.length} run(s) (${result.foreignUnendedRuns} unended) for other projects`);
14840
+ log42.info(TAG39, `split legacy state into ${targetPath}: moved ${result.movedRuns} run(s) ` + `(${result.movedUnendedRuns} unended) and ${result.movedCards} card record(s), ` + `copied ${result.copiedIndeterminateCards} unattributable card record(s); ` + `left ${remainingRuns.length} run(s) (${result.foreignUnendedRuns} unended) for other projects`);
14693
14841
  return result;
14694
14842
  }
14695
- var TAG38 = "state-migration", NO_MIGRATION;
14843
+ var TAG39 = "state-migration", NO_MIGRATION;
14696
14844
  var init_state_migration = __esm(() => {
14697
14845
  init_state_store();
14698
14846
  NO_MIGRATION = {
@@ -14798,7 +14946,7 @@ var init_stream_parser_selftest = __esm(() => {
14798
14946
 
14799
14947
  // src/watcher.ts
14800
14948
  import { randomUUID as randomUUID3 } from "node:crypto";
14801
- import { isPretty as isPretty2, log as log42 } from "@gethmy/harness";
14949
+ import { isPretty as isPretty2, log as log43 } from "@gethmy/harness";
14802
14950
  import { createClient } from "@supabase/supabase-js";
14803
14951
  function toPresenceColumns(names) {
14804
14952
  return names.map((n) => n.trim()).filter((n) => n.length > 0).slice(0, MAX_PRESENCE_COLUMNS).map((n) => n.slice(0, MAX_PRESENCE_COLUMN_LENGTH));
@@ -14811,6 +14959,7 @@ class Watcher {
14811
14959
  routing;
14812
14960
  onCardBroadcast;
14813
14961
  onAgentCommand;
14962
+ onSweepCommandNudge;
14814
14963
  channel = null;
14815
14964
  presenceChannel = null;
14816
14965
  supabase = null;
@@ -14858,20 +15007,21 @@ class Watcher {
14858
15007
  const stillHeld = this.supabase?.getChannels().some((c) => c.topic === old.topic) ?? false;
14859
15008
  if (!stillHeld)
14860
15009
  return true;
14861
- log42.warn(TAG39, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
15010
+ log43.warn(TAG40, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
14862
15011
  return false;
14863
15012
  }
14864
- constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand) {
15013
+ constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand, onSweepCommandNudge) {
14865
15014
  this.credentials = credentials;
14866
15015
  this.projectId = projectId;
14867
15016
  this.identity = identity;
14868
15017
  this.routing = routing;
14869
15018
  this.onCardBroadcast = onCardBroadcast;
14870
15019
  this.onAgentCommand = onAgentCommand;
15020
+ this.onSweepCommandNudge = onSweepCommandNudge;
14871
15021
  }
14872
15022
  async start() {
14873
15023
  if (!isPretty2()) {
14874
- log42.info(TAG39, "Connecting to Supabase realtime (broadcast)...");
15024
+ log43.info(TAG40, "Connecting to Supabase realtime (broadcast)...");
14875
15025
  }
14876
15026
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
14877
15027
  this.subscribeBroadcast();
@@ -14909,7 +15059,7 @@ class Watcher {
14909
15059
  status = `threw (${String(err)})`;
14910
15060
  }
14911
15061
  if (status !== "ok") {
14912
- log42.debug(TAG39, `Presence sweep update returned "${status}"`);
15062
+ log43.debug(TAG40, `Presence sweep update returned "${status}"`);
14913
15063
  }
14914
15064
  })();
14915
15065
  }
@@ -14920,7 +15070,7 @@ class Watcher {
14920
15070
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
14921
15071
  this.presenceChannel = presenceChannel;
14922
15072
  presenceChannel.on("presence", { event: "sync" }, () => {
14923
- log42.debug(TAG39, "Presence sync");
15073
+ log43.debug(TAG40, "Presence sync");
14924
15074
  }).subscribe(async (status) => {
14925
15075
  if (gen !== this.presenceGen)
14926
15076
  return;
@@ -14936,13 +15086,13 @@ class Watcher {
14936
15086
  if (trackStatus !== "ok") {
14937
15087
  this.presenceTracked = false;
14938
15088
  if (!this.stopping) {
14939
- log42.warn(TAG39, `Presence track returned "${trackStatus}" — scheduling reconnect`);
15089
+ log43.warn(TAG40, `Presence track returned "${trackStatus}" — scheduling reconnect`);
14940
15090
  this.schedulePresenceReconnect();
14941
15091
  }
14942
15092
  return;
14943
15093
  }
14944
15094
  if (!isPretty2() || !this.suppressStartupLogs) {
14945
- log42.info(TAG39, "Presence tracked on board-presence channel");
15095
+ log43.info(TAG40, "Presence tracked on board-presence channel");
14946
15096
  }
14947
15097
  this.presenceTracked = true;
14948
15098
  this.presenceReconnectAttempts = 0;
@@ -14950,7 +15100,7 @@ class Watcher {
14950
15100
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
14951
15101
  this.presenceTracked = false;
14952
15102
  if (!this.stopping) {
14953
- log42.warn(TAG39, `Presence subscription ${status} — scheduling reconnect`);
15103
+ log43.warn(TAG40, `Presence subscription ${status} — scheduling reconnect`);
14954
15104
  this.schedulePresenceReconnect();
14955
15105
  }
14956
15106
  }
@@ -14969,7 +15119,7 @@ class Watcher {
14969
15119
  async reconnectPresence() {
14970
15120
  if (this.stopping || !this.supabase)
14971
15121
  return;
14972
- log42.warn(TAG39, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
15122
+ log43.warn(TAG40, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
14973
15123
  if (this.presenceChannel) {
14974
15124
  const old = this.presenceChannel;
14975
15125
  this.presenceChannel = null;
@@ -14992,13 +15142,13 @@ class Watcher {
14992
15142
  return;
14993
15143
  const gen = ++this.broadcastGen;
14994
15144
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
14995
- log42.debug(TAG39, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
15145
+ log43.debug(TAG40, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
14996
15146
  this.onCardBroadcast({
14997
15147
  event: "card_update",
14998
15148
  payload: msg.payload ?? {}
14999
15149
  });
15000
15150
  }).on("broadcast", { event: "card_created" }, (msg) => {
15001
- log42.debug(TAG39, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
15151
+ log43.debug(TAG40, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
15002
15152
  this.onCardBroadcast({
15003
15153
  event: "card_created",
15004
15154
  payload: msg.payload ?? {}
@@ -15008,9 +15158,12 @@ class Watcher {
15008
15158
  const cardId = payload.card_id;
15009
15159
  const command = payload.command;
15010
15160
  if (cardId && command) {
15011
- log42.info(TAG39, `Broadcast: agent_command ${command} for ${cardId}`);
15161
+ log43.info(TAG40, `Broadcast: agent_command ${command} for ${cardId}`);
15012
15162
  this.onAgentCommand?.({ cardId, command });
15013
15163
  }
15164
+ }).on("broadcast", { event: "sweep_command" }, () => {
15165
+ log43.debug(TAG40, "Broadcast: sweep_command — draining");
15166
+ this.onSweepCommandNudge?.();
15014
15167
  }).subscribe((status) => {
15015
15168
  if (gen !== this.broadcastGen)
15016
15169
  return;
@@ -15018,13 +15171,13 @@ class Watcher {
15018
15171
  this.connected = true;
15019
15172
  this.reconnectAttempts = 0;
15020
15173
  if (!isPretty2() || !this.suppressStartupLogs) {
15021
- log42.info(TAG39, "Broadcast subscription active");
15174
+ log43.info(TAG40, "Broadcast subscription active");
15022
15175
  }
15023
15176
  this.maybeResolveReady();
15024
15177
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
15025
15178
  this.connected = false;
15026
15179
  if (!this.stopping) {
15027
- log42.warn(TAG39, `Broadcast subscription ${status} — scheduling reconnect`);
15180
+ log43.warn(TAG40, `Broadcast subscription ${status} — scheduling reconnect`);
15028
15181
  this.scheduleReconnect();
15029
15182
  }
15030
15183
  }
@@ -15043,7 +15196,7 @@ class Watcher {
15043
15196
  async reconnectBroadcast() {
15044
15197
  if (this.stopping || !this.supabase)
15045
15198
  return;
15046
- log42.warn(TAG39, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
15199
+ log43.warn(TAG40, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
15047
15200
  if (this.channel) {
15048
15201
  const old = this.channel;
15049
15202
  this.channel = null;
@@ -15085,10 +15238,10 @@ class Watcher {
15085
15238
  }
15086
15239
  this.connected = false;
15087
15240
  this.presenceTracked = false;
15088
- log42.info(TAG39, "Broadcast subscription stopped");
15241
+ log43.info(TAG40, "Broadcast subscription stopped");
15089
15242
  }
15090
15243
  }
15091
- var TAG39 = "watcher", MAX_PRESENCE_COLUMNS = 24, MAX_PRESENCE_COLUMN_LENGTH = 120;
15244
+ var TAG40 = "watcher", MAX_PRESENCE_COLUMNS = 24, MAX_PRESENCE_COLUMN_LENGTH = 120;
15092
15245
  var init_watcher = __esm(() => {
15093
15246
  init_sweep_guard();
15094
15247
  });
@@ -15104,7 +15257,7 @@ __export(exports_worktree_gc, {
15104
15257
  import { execFileSync as execFileSync8 } from "node:child_process";
15105
15258
  import { existsSync as existsSync5, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
15106
15259
  import { resolve as resolve3 } from "node:path";
15107
- import { cleanupWorktree as cleanupWorktree5, GIT_NO_HOOKS as GIT_NO_HOOKS9, log as log43 } from "@gethmy/harness";
15260
+ import { cleanupWorktree as cleanupWorktree5, GIT_NO_HOOKS as GIT_NO_HOOKS9, log as log44 } from "@gethmy/harness";
15108
15261
  function isTransientGitNetworkError(message) {
15109
15262
  return TRANSIENT_GIT_NETWORK_ERROR.test(message);
15110
15263
  }
@@ -15217,10 +15370,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
15217
15370
  });
15218
15371
  } catch {}
15219
15372
  if (result.removed.length > 0) {
15220
- log43.info(TAG40, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
15373
+ log44.info(TAG41, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
15221
15374
  }
15222
15375
  if (result.errors.length > 0) {
15223
- log43.warn(TAG40, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
15376
+ log44.warn(TAG41, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
15224
15377
  }
15225
15378
  return result;
15226
15379
  }
@@ -15250,7 +15403,7 @@ function pruneFailedRemoteBranches(opts) {
15250
15403
  } catch (err) {
15251
15404
  const detail = gitErrorDetail2(err);
15252
15405
  if (isTransientGitNetworkError(detail)) {
15253
- log43.debug(TAG40, `Remote branch GC skipped — remote unreachable: ${detail}`);
15406
+ log44.debug(TAG41, `Remote branch GC skipped — remote unreachable: ${detail}`);
15254
15407
  return result;
15255
15408
  }
15256
15409
  result.errors.push({ ref: "fetch", error: detail });
@@ -15290,7 +15443,7 @@ function pruneFailedRemoteBranches(opts) {
15290
15443
  continue;
15291
15444
  }
15292
15445
  if (clock() > sweepDeadline) {
15293
- log43.debug(TAG40, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
15446
+ log44.debug(TAG41, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
15294
15447
  break;
15295
15448
  }
15296
15449
  try {
@@ -15303,17 +15456,17 @@ function pruneFailedRemoteBranches(opts) {
15303
15456
  } catch (err) {
15304
15457
  const detail = gitErrorDetail2(err);
15305
15458
  if (isTransientGitNetworkError(detail)) {
15306
- log43.debug(TAG40, `Remote branch GC interrupted — remote unreachable: ${detail}`);
15459
+ log44.debug(TAG41, `Remote branch GC interrupted — remote unreachable: ${detail}`);
15307
15460
  break;
15308
15461
  }
15309
15462
  result.errors.push({ ref, error: detail });
15310
15463
  }
15311
15464
  }
15312
15465
  if (result.removed.length > 0) {
15313
- log43.info(TAG40, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
15466
+ log44.info(TAG41, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
15314
15467
  }
15315
15468
  if (result.errors.length > 0) {
15316
- log43.warn(TAG40, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
15469
+ log44.warn(TAG41, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
15317
15470
  }
15318
15471
  return result;
15319
15472
  }
@@ -15344,13 +15497,13 @@ class WorktreeGc {
15344
15497
  try {
15345
15498
  runWorktreeGc(this.basePath, this.store);
15346
15499
  } catch (err) {
15347
- log43.warn(TAG40, `GC tick failed: ${err instanceof Error ? err.message : err}`);
15500
+ log44.warn(TAG41, `GC tick failed: ${err instanceof Error ? err.message : err}`);
15348
15501
  }
15349
15502
  if (this.remoteOpts) {
15350
15503
  try {
15351
15504
  pruneFailedRemoteBranches(this.remoteOpts);
15352
15505
  } catch (err) {
15353
- log43.warn(TAG40, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
15506
+ log44.warn(TAG41, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
15354
15507
  }
15355
15508
  }
15356
15509
  }
@@ -15364,7 +15517,7 @@ function getRepoRoot2() {
15364
15517
  return null;
15365
15518
  }
15366
15519
  }
15367
- var TAG40 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
15520
+ var TAG41 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
15368
15521
  var init_worktree_gc = __esm(() => {
15369
15522
  GIT_NETWORK_EXEC = {
15370
15523
  timeout: GIT_NETWORK_TIMEOUT_MS,
@@ -15403,7 +15556,7 @@ import { resolve as resolve4 } from "node:path";
15403
15556
  import {
15404
15557
  detectGitProvider as detectGitProvider6,
15405
15558
  GIT_NO_HOOKS as GIT_NO_HOOKS10,
15406
- log as log44,
15559
+ log as log45,
15407
15560
  validateGitProviderCli
15408
15561
  } from "@gethmy/harness";
15409
15562
  async function validatePrerequisites(config, banner) {
@@ -15480,7 +15633,7 @@ async function main() {
15480
15633
  } catch (err) {
15481
15634
  if (err instanceof ConfigValidationError) {
15482
15635
  banner.fail();
15483
- log44.error(TAG41, err.message);
15636
+ log45.error(TAG42, err.message);
15484
15637
  process.exit(1);
15485
15638
  }
15486
15639
  throw err;
@@ -15491,10 +15644,11 @@ async function main() {
15491
15644
  validateSweepConfig(config.agent);
15492
15645
  validateRankingConfig(config.agent);
15493
15646
  validateVerificationConfig(config.agent);
15647
+ validateHttpConfig(config.agent);
15494
15648
  } catch (err) {
15495
15649
  if (err instanceof ConfigValidationError) {
15496
15650
  banner.fail();
15497
- log44.error(TAG41, err.message);
15651
+ log45.error(TAG42, err.message);
15498
15652
  process.exit(1);
15499
15653
  }
15500
15654
  throw err;
@@ -15522,7 +15676,7 @@ async function main() {
15522
15676
  worktreePrefix: resolve4(config.repoRoot, config.agent.worktree.basePath)
15523
15677
  });
15524
15678
  } catch (err) {
15525
- log44.error(TAG41, `legacy state split failed — starting with the project store as-is: ${err instanceof Error ? err.message : err}`);
15679
+ log45.error(TAG42, `legacy state split failed — starting with the project store as-is: ${err instanceof Error ? err.message : err}`);
15526
15680
  }
15527
15681
  const stateStore = StateStore.open({ projectId: config.projectId });
15528
15682
  const daemonId = randomUUID4();
@@ -15666,14 +15820,14 @@ async function main() {
15666
15820
  await handleBroadcast(event, client, pool, config, agentId);
15667
15821
  }, async (command) => {
15668
15822
  await pool.handleAgentCommand(command.cardId, command.command);
15669
- });
15823
+ }, () => reconciler.onSweepCommandNudge());
15670
15824
  let shuttingDown = false;
15671
15825
  let exitCode = 0;
15672
15826
  const shutdown = async (signal) => {
15673
15827
  if (shuttingDown)
15674
15828
  return;
15675
15829
  shuttingDown = true;
15676
- log44.info(TAG41, `Received ${signal}, shutting down gracefully...`);
15830
+ log45.info(TAG42, `Received ${signal}, shutting down gracefully...`);
15677
15831
  reconciler.stop();
15678
15832
  mergeMonitor?.stop();
15679
15833
  worktreeGc.stop();
@@ -15684,19 +15838,19 @@ async function main() {
15684
15838
  }
15685
15839
  await watcher.stop();
15686
15840
  await pool.shutdown();
15687
- log44.info(TAG41, "Daemon stopped.");
15841
+ log45.info(TAG42, "Daemon stopped.");
15688
15842
  process.exit(exitCode);
15689
15843
  };
15690
15844
  process.on("SIGINT", () => shutdown("SIGINT"));
15691
15845
  process.on("SIGTERM", () => shutdown("SIGTERM"));
15692
15846
  process.on("SIGHUP", () => shutdown("SIGHUP"));
15693
15847
  process.on("uncaughtException", (err) => {
15694
- log44.error(TAG41, `Uncaught exception: ${err.message}`);
15848
+ log45.error(TAG42, `Uncaught exception: ${err.message}`);
15695
15849
  exitCode = 1;
15696
15850
  shutdown("uncaughtException");
15697
15851
  });
15698
15852
  process.on("unhandledRejection", (reason) => {
15699
- log44.error(TAG41, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
15853
+ log45.error(TAG42, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
15700
15854
  exitCode = 1;
15701
15855
  shutdown("unhandledRejection");
15702
15856
  });
@@ -15756,29 +15910,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
15756
15910
  if (assignedAgentId === undefined)
15757
15911
  return;
15758
15912
  if (assignedAgentId === agentId) {
15759
- log44.info(TAG41, `Broadcast: card ${cardId} assigned to agent`);
15913
+ log45.info(TAG42, `Broadcast: card ${cardId} assigned to agent`);
15760
15914
  try {
15761
15915
  await pool.resetAttemptsForReassign(cardId);
15762
15916
  await tryEnqueueCard(cardId, client, pool, config, agentId);
15763
15917
  } catch (err) {
15764
- log44.error(TAG41, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
15918
+ log45.error(TAG42, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
15765
15919
  }
15766
15920
  } else if (pool.isCardKnown(cardId)) {
15767
- log44.info(TAG41, `Broadcast: card ${cardId} unassigned from agent`);
15921
+ log45.info(TAG42, `Broadcast: card ${cardId} unassigned from agent`);
15768
15922
  await pool.removeCard(cardId);
15769
15923
  }
15770
15924
  }
15771
15925
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
15772
15926
  const { card } = await client.getCard(cardId);
15773
15927
  if (card.assigned_agent_id !== agentId) {
15774
- log44.debug(TAG41, `Card ${cardId} no longer assigned to agent — skipping`);
15928
+ log45.debug(TAG42, `Card ${cardId} no longer assigned to agent — skipping`);
15775
15929
  return;
15776
15930
  }
15777
15931
  const board = await client.getBoard(config.projectId, { summary: true });
15778
15932
  const columns = board.columns;
15779
15933
  const column = columns.find((c) => c.id === card.column_id);
15780
15934
  if (!column) {
15781
- log44.warn(TAG41, `Column not found for card ${cardId}`);
15935
+ log45.warn(TAG42, `Column not found for card ${cardId}`);
15782
15936
  return;
15783
15937
  }
15784
15938
  const route = classifyPickup(card, column.name, {
@@ -15787,31 +15941,31 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
15787
15941
  playbooks: config.agent.playbooks
15788
15942
  });
15789
15943
  if (!route) {
15790
- log44.info(TAG41, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
15944
+ log45.info(TAG42, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
15791
15945
  return;
15792
15946
  }
15793
15947
  if (route.stage) {
15794
- log44.info(TAG41, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
15948
+ log45.info(TAG42, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
15795
15949
  }
15796
15950
  const mode = route.mode;
15797
15951
  const labelMap = buildLabelMap(board.labels ?? []);
15798
15952
  const cardLabels = resolveCardLabels(card, labelMap);
15799
15953
  const subtasks = card.subtasks ?? [];
15800
15954
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
15801
- log44.debug(TAG41, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
15955
+ log45.debug(TAG42, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
15802
15956
  return;
15803
15957
  }
15804
15958
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
15805
- log44.debug(TAG41, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
15959
+ log45.debug(TAG42, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
15806
15960
  return;
15807
15961
  }
15808
15962
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
15809
- log44.info(TAG41, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
15963
+ log45.info(TAG42, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
15810
15964
  return;
15811
15965
  }
15812
15966
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
15813
15967
  }
15814
- var TAG41 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
15968
+ var TAG42 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
15815
15969
  var init_src = __esm(() => {
15816
15970
  init_account_guard();
15817
15971
  init_account_probe();