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