@gethmy/agent 1.45.0 → 1.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,10 +19,6 @@ Built for **failsafe auto mode**: crashed daemons recover on restart, misconfigu
19
19
  npm i -g @gethmy/cli
20
20
  hmy agent
21
21
 
22
- # Or the daemon package alone
23
- npm install -g @gethmy/agent
24
- harmony-agent
25
-
26
22
  # Or nothing installed at all (works with any package manager)
27
23
  npx @gethmy/agent@latest
28
24
  ```
@@ -30,12 +26,11 @@ npx @gethmy/agent@latest
30
26
  Install when the machine is yours; use `npx` for a container, a CI step or a
31
27
  one-off. Both forms stay supported.
32
28
 
33
- > **Always pin `@latest`.** A bare `npx @gethmy/agent` reuses any previously
34
- > cached version that satisfies the spec — so an old install in `~/.npm/_npx`
35
- > can shadow the current release and you'll get stale startup logs and CLI
36
- > behavior. `npx @gethmy/agent@latest` re-resolves to the newest published
37
- > version. If you ever suspect a stale run, clear the cache with
38
- > `rm -rf ~/.npm/_npx` (or install globally to skip npx caching entirely).
29
+ > **`npx` can serve a stale build.** The npx cache keeps serving the version it
30
+ > first installed, `@latest` included — a restart six minutes after 1.38.0
31
+ > published still ran 1.37.0 — so you get stale startup logs and CLI behavior.
32
+ > If you suspect a stale run, clear the cache with `rm -rf ~/.npm/_npx`, or
33
+ > install `@gethmy/cli` and run `hmy agent` to skip npx caching entirely.
39
34
 
40
35
  ## Configuration
41
36
 
package/dist/cli.js CHANGED
@@ -1103,9 +1103,14 @@ var init_commentSerializer = __esm(() => {
1103
1103
  });
1104
1104
 
1105
1105
  // ../harmony-shared/src/connectCommands.ts
1106
- var HMY_INSTALL_URL = "https://gethmy.com/install.sh", HMY_INSTALL_COMMAND;
1106
+ function hmyConnectCommand(projectSlug) {
1107
+ const slug = projectSlug?.trim();
1108
+ return slug ? `${CONNECT_VERB} ${slug}` : CONNECT_VERB;
1109
+ }
1110
+ var HMY_INSTALL_URL = "https://gethmy.com/install.sh", HMY_INSTALL_COMMAND, HMY_AGENT_COMMAND = "hmy agent", CONNECT_VERB = "hmy connect", HOSTED_MCP_URL = "https://mcp.gethmy.com/mcp", HOSTED_MCP_CONFIG;
1107
1111
  var init_connectCommands = __esm(() => {
1108
1112
  HMY_INSTALL_COMMAND = `curl -fsSL ${HMY_INSTALL_URL} | bash`;
1113
+ HOSTED_MCP_CONFIG = JSON.stringify({ mcpServers: { harmony: { type: "http", url: HOSTED_MCP_URL } } }, null, 2);
1109
1114
  });
1110
1115
 
1111
1116
  // ../harmony-shared/src/constants.ts
@@ -1578,6 +1583,18 @@ function own(source, key) {
1578
1583
  return Object.hasOwn(source, key) ? source[key] : undefined;
1579
1584
  }
1580
1585
 
1586
+ // ../harmony-shared/src/packageCommands.ts
1587
+ function npxArgs(pkg, args) {
1588
+ return ["-y", `${SCOPE}/${pkg}@latest`, ...args];
1589
+ }
1590
+ function npxCommand(pkg, args = []) {
1591
+ return ["npx", ...npxArgs(pkg, args)].join(" ");
1592
+ }
1593
+ var SCOPE = "@gethmy", HMY_NPM_INSTALL_COMMAND;
1594
+ var init_packageCommands = __esm(() => {
1595
+ HMY_NPM_INSTALL_COMMAND = `npm i -g ${SCOPE}/cli`;
1596
+ });
1597
+
1581
1598
  // ../harmony-shared/src/parsedState.ts
1582
1599
  function parseJsonWithoutPrototypes(text) {
1583
1600
  return JSON.parse(text, (_key, value) => {
@@ -2575,6 +2592,7 @@ var init_src = __esm(() => {
2575
2592
  init_gateEvaluate();
2576
2593
  init_logger();
2577
2594
  init_memoryQuery();
2595
+ init_packageCommands();
2578
2596
  init_playbookAutoBind();
2579
2597
  init_playbookCatalog();
2580
2598
  init_playbookStage();
@@ -3516,7 +3534,7 @@ function agentIdentifier(workerId) {
3516
3534
  function endStatusForCancel(reason) {
3517
3535
  return reason === "human_stop" ? "cancelled" : "paused";
3518
3536
  }
3519
- var DEFAULT_FINDING_AUTHORS, DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", REVIEW_FAILURE_REASON_MAX = 300, AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
3537
+ var DEFAULT_FINDING_AUTHORS, DEFAULT_HUMAN_STAGE_HOLD_MINUTES = 60, DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", REVIEW_FAILURE_REASON_MAX = 300, AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
3520
3538
  var init_types = __esm(() => {
3521
3539
  init_src();
3522
3540
  init_board_review();
@@ -3660,7 +3678,12 @@ var init_types = __esm(() => {
3660
3678
  runLogDays: 30
3661
3679
  },
3662
3680
  planning: DEFAULT_PLANNING_CONFIG,
3663
- playbooks: { enabled: true, humanStageColumns: [], metrics: {} },
3681
+ playbooks: {
3682
+ enabled: true,
3683
+ humanStageColumns: [],
3684
+ humanStageHoldMinutes: DEFAULT_HUMAN_STAGE_HOLD_MINUTES,
3685
+ metrics: {}
3686
+ },
3664
3687
  contractFirst: DEFAULT_CONTRACT_CONFIG,
3665
3688
  boardReview: DEFAULT_BOARD_REVIEW_CONFIG,
3666
3689
  sweep: {
@@ -4041,13 +4064,13 @@ function loadDaemonConfig() {
4041
4064
  const projectId = getActiveProjectId(repoRoot);
4042
4065
  const userEmail = getUserEmail();
4043
4066
  if (!workspaceId) {
4044
- throw new Error(`No active workspace configured in ${contextSource.path}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4067
+ throw new Error(`No active workspace configured in ${contextSource.path}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4045
4068
  }
4046
4069
  if (!projectId) {
4047
- throw new Error(`No active project configured in ${contextSource.path}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4070
+ throw new Error(`No active project configured in ${contextSource.path}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4048
4071
  }
4049
4072
  if (!userEmail) {
4050
- throw new Error(`No user email configured in ${contextSource.globalPath}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4073
+ throw new Error(`No user email configured in ${contextSource.globalPath}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4051
4074
  }
4052
4075
  let agentOverrides = {};
4053
4076
  let agentName = "Harmony Agent";
@@ -4160,6 +4183,7 @@ function createApiClient(config) {
4160
4183
  }
4161
4184
  var TAG5 = "config", NOT_A_REPOSITORY_EXIT = 128, warnedLegacyConfigDir = false;
4162
4185
  var init_config = __esm(() => {
4186
+ init_src();
4163
4187
  init_config_merge();
4164
4188
  init_config_validation();
4165
4189
  init_paths();
@@ -8817,7 +8841,7 @@ class StateStore {
8817
8841
  const rec = this.getCard(cardId);
8818
8842
  if (!rec)
8819
8843
  return;
8820
- if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && (!clearTurns || !rec.totalTurns && !rec.runStarts)) {
8844
+ if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && rec.stageHold == null && (!clearTurns || !rec.totalTurns && !rec.runStarts)) {
8821
8845
  return;
8822
8846
  }
8823
8847
  rec.attempts = 0;
@@ -8827,6 +8851,7 @@ class StateStore {
8827
8851
  rec.totalTurns = 0;
8828
8852
  if (clearTurns)
8829
8853
  rec.runStarts = 0;
8854
+ rec.stageHold = null;
8830
8855
  await this.persist();
8831
8856
  }
8832
8857
  getCiRepairAttempts(cardId) {
@@ -8909,6 +8934,29 @@ class StateStore {
8909
8934
  getCardRunStarts(cardId) {
8910
8935
  return this.getCard(cardId)?.runStarts ?? 0;
8911
8936
  }
8937
+ getStageHold(cardId) {
8938
+ return this.getCard(cardId)?.stageHold ?? null;
8939
+ }
8940
+ async recordStageHold(cardId, hold) {
8941
+ const rec = this.ensureCard(cardId);
8942
+ const prior = rec.stageHold ?? null;
8943
+ const announced = prior != null && prior.stageId === hold.stageId && prior.reason === hold.reason;
8944
+ rec.stageHold = cloneWithoutPrototype({
8945
+ stageId: hold.stageId,
8946
+ reason: hold.reason,
8947
+ until: hold.untilMs,
8948
+ announcedAt: announced ? prior.announcedAt : Date.now()
8949
+ });
8950
+ await this.persist();
8951
+ return { announced };
8952
+ }
8953
+ async clearStageHold(cardId) {
8954
+ const rec = this.getCard(cardId);
8955
+ if (!rec || rec.stageHold == null)
8956
+ return;
8957
+ rec.stageHold = null;
8958
+ await this.persist();
8959
+ }
8912
8960
  getCardCostCents(cardId) {
8913
8961
  return this.getCard(cardId)?.totalCostCents ?? 0;
8914
8962
  }
@@ -10219,6 +10267,27 @@ class SleepGuard {
10219
10267
  var TAG26 = "sleep-guard";
10220
10268
  var init_sleep_guard = () => {};
10221
10269
 
10270
+ // src/stage-hold.ts
10271
+ function classifyStageHold(card, hold, nowMs) {
10272
+ if (!hold)
10273
+ return { kind: "none" };
10274
+ if (!card.current_stage || card.current_stage !== hold.stageId) {
10275
+ return { kind: "stale" };
10276
+ }
10277
+ if (nowMs >= hold.until)
10278
+ return { kind: "lapsed" };
10279
+ const remainingMin = Math.ceil((hold.until - nowMs) / 60000);
10280
+ return {
10281
+ kind: "holding",
10282
+ detail: `parked on human-owned stage "${hold.stageId}" — holding for ~${remainingMin}m`
10283
+ };
10284
+ }
10285
+ function stageHoldUntil(holdMinutes, nowMs) {
10286
+ if (!Number.isFinite(holdMinutes) || holdMinutes <= 0)
10287
+ return nowMs;
10288
+ return nowMs + holdMinutes * 60000;
10289
+ }
10290
+
10222
10291
  // src/unblock.ts
10223
10292
  import { log as log30 } from "@gethmy/harness";
10224
10293
  async function fetchBlocksLinks(client, cardId) {
@@ -11455,6 +11524,29 @@ var init_stage_advance = __esm(() => {
11455
11524
  });
11456
11525
 
11457
11526
  // src/workspace-model-config.ts
11527
+ function layerPlatformDefaults(platform, ws) {
11528
+ if (!platform)
11529
+ return ws ?? null;
11530
+ if (!ws)
11531
+ return platform;
11532
+ const merged = { ...platform };
11533
+ for (const [key, value] of Object.entries(ws)) {
11534
+ if (value !== null && value !== undefined) {
11535
+ merged[key] = value;
11536
+ }
11537
+ }
11538
+ if (platform.tiers || ws.tiers) {
11539
+ const tiers = {};
11540
+ for (const layer of [platform.tiers, ws.tiers]) {
11541
+ for (const [tier, model] of Object.entries(layer ?? {})) {
11542
+ if (model)
11543
+ tiers[tier] = model;
11544
+ }
11545
+ }
11546
+ merged.tiers = tiers;
11547
+ }
11548
+ return merged;
11549
+ }
11458
11550
  function workspaceConfigToPartial(ws) {
11459
11551
  const claude = {};
11460
11552
  if (ws.implement_model)
@@ -11977,7 +12069,7 @@ class Worker {
11977
12069
  stageCtx = await this.resolveStageContext(card);
11978
12070
  if (stageCtx.kind === "hold") {
11979
12071
  this.held = true;
11980
- await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
12072
+ await this.holdStageCard(card, stageCtx.reason, stageCtx.wait, stageCtx.wait);
11981
12073
  return;
11982
12074
  }
11983
12075
  if (stageCtx.kind === "fanout") {
@@ -12535,15 +12627,35 @@ class Worker {
12535
12627
  }
12536
12628
  }
12537
12629
  }
12538
- async holdStageCard(card, reason, wait = false) {
12630
+ async holdStageCard(card, reason, wait = false, armCooldown = false) {
12539
12631
  log34.info(this.tag, `Holding #${card.short_id}: ${reason}`);
12540
12632
  await this.stateStore.decrementAttempt(card.id);
12541
- try {
12542
- await this.client.addComment(card.id, reason, {
12543
- commentType: "blocker",
12544
- agentSessionId: this.sessionId ?? undefined
12545
- });
12546
- } catch {}
12633
+ let announced = false;
12634
+ const holdStage = armCooldown ? card.current_stage : null;
12635
+ if (holdStage) {
12636
+ try {
12637
+ const recorded = await this.stateStore.recordStageHold(card.id, {
12638
+ stageId: holdStage,
12639
+ reason,
12640
+ untilMs: stageHoldUntil(this.config.playbooks.humanStageHoldMinutes ?? DEFAULT_HUMAN_STAGE_HOLD_MINUTES, Date.now())
12641
+ });
12642
+ announced = recorded.announced;
12643
+ } catch (err) {
12644
+ log34.warn(this.tag, `stage hold not recorded on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
12645
+ }
12646
+ }
12647
+ if (announced) {
12648
+ log34.debug(this.tag, `#${card.short_id} already carries this blocker — not reposting`);
12649
+ } else {
12650
+ try {
12651
+ await this.client.addComment(card.id, reason, {
12652
+ commentType: "blocker",
12653
+ agentSessionId: this.sessionId ?? undefined
12654
+ });
12655
+ } catch {
12656
+ await this.stateStore.clearStageHold(card.id).catch(() => {});
12657
+ }
12658
+ }
12547
12659
  try {
12548
12660
  await runTransition(this.client, card, {
12549
12661
  removeLabels: ["agent"],
@@ -13970,8 +14082,8 @@ class Pool {
13970
14082
  this.reviewQueue = new PriorityQueue(config);
13971
14083
  this.budget = new BudgetGuard(config.budget, this.stateStore);
13972
14084
  const workspaceModels = daemonConfig ? new WorkspaceModelCache(async () => {
13973
- const { config: wsConfig } = await client.getWorkspaceModelConfig(workspaceId);
13974
- return wsConfig ?? null;
14085
+ const { config: wsConfig, platformDefaults } = await client.getWorkspaceModelConfig(workspaceId);
14086
+ return layerPlatformDefaults(platformDefaults, wsConfig);
13975
14087
  }) : undefined;
13976
14088
  const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => {
13977
14089
  const { models } = await client.getModelCatalog();
@@ -14038,6 +14150,14 @@ class Pool {
14038
14150
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
14039
14151
  return;
14040
14152
  }
14153
+ const stageHold = classifyStageHold(card, this.stateStore.getStageHold(card.id), Date.now());
14154
+ if (stageHold.kind === "holding") {
14155
+ log36.debug(TAG32, `#${card.short_id} held — ${stageHold.detail}`);
14156
+ return;
14157
+ }
14158
+ if (stageHold.kind === "stale") {
14159
+ await this.stateStore.clearStageHold(card.id);
14160
+ }
14041
14161
  const decision = this.budget.check(card.id);
14042
14162
  if (!decision.allow) {
14043
14163
  if (decision.reason === "daily_budget") {
@@ -14736,6 +14856,7 @@ var init_pool = __esm(() => {
14736
14856
  init_unblock();
14737
14857
  init_worker();
14738
14858
  init_workspace_model_cache();
14859
+ init_workspace_model_config();
14739
14860
  });
14740
14861
 
14741
14862
  // src/port-registry.ts
@@ -14974,6 +15095,8 @@ var init_repo_dirs = __esm(() => {
14974
15095
 
14975
15096
  // src/card-trust.ts
14976
15097
  function isTrustedCardAuthor(card, trustedAuthors) {
15098
+ if (card.source != null)
15099
+ return false;
14977
15100
  return card.created_by != null && trustedAuthors.has(card.created_by);
14978
15101
  }
14979
15102
 
@@ -15033,7 +15156,7 @@ async function reclaimPreReviewStrands(opts) {
15033
15156
  continue;
15034
15157
  const daemonBranch = daemonBranches.get(card.id);
15035
15158
  if (!isTrustedCardAuthor(card, trustedAuthors) && daemonBranch !== branch) {
15036
- log40.debug(TAG36, `#${card.short_id} - skipped: author ${card.created_by ?? "(none)"} is not trusted, and ${daemonBranch ? `the card records ${branch} while this daemon pushed ${daemonBranch}` : "this daemon has pushed no branch for it"}`);
15159
+ log40.debug(TAG36, `#${card.short_id} - skipped: ${card.source != null ? `card was filed externally (source: ${card.source})` : `author ${card.created_by ?? "(none)"} is not trusted`}, and ${daemonBranch ? `the card records ${branch} while this daemon pushed ${daemonBranch}` : "this daemon has pushed no branch for it"}`);
15037
15160
  continue;
15038
15161
  }
15039
15162
  const labels = resolveCardLabels(card, labelMap);
@@ -15271,7 +15394,7 @@ function rankSweepCandidates(opts) {
15271
15394
  continue;
15272
15395
  }
15273
15396
  if (!isTrustedCardAuthor(card, trustedAuthors)) {
15274
- skip2(card, "untrusted_author", card.created_by == null ? "card has no recorded author" : `author ${card.created_by} is not in sweep.trustedAuthors`);
15397
+ skip2(card, "untrusted_author", card.source != null ? `card was filed externally (source: ${card.source})` : card.created_by == null ? "card has no recorded author" : `author ${card.created_by} is not in sweep.trustedAuthors`);
15275
15398
  continue;
15276
15399
  }
15277
15400
  if (card.assigned_agent_id != null) {
@@ -18082,7 +18205,7 @@ var init_head_anchor = () => {};
18082
18205
 
18083
18206
  // src/merge-cleanup.ts
18084
18207
  import { execFileSync as execFileSync11 } from "node:child_process";
18085
- import { existsSync as existsSync7, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
18208
+ import { existsSync as existsSync7, lstatSync as lstatSync3, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
18086
18209
  import { basename, dirname as dirname7, isAbsolute, resolve as resolve6, sep as sep4 } from "node:path";
18087
18210
  import { cleanupWorktree as cleanupWorktree6, GIT_NO_HOOKS as GIT_NO_HOOKS12, log as log52 } from "@gethmy/harness";
18088
18211
  function canonicalize(p) {
@@ -18187,8 +18310,20 @@ function classifyWorktreeRegistration(worktreePath, cwd) {
18187
18310
  function isRegisteredWorktree(worktreePath, cwd) {
18188
18311
  return classifyWorktreeRegistration(worktreePath, cwd).kind === "registered";
18189
18312
  }
18313
+ function pathAbsent(p) {
18314
+ try {
18315
+ lstatSync3(p);
18316
+ return false;
18317
+ } catch {
18318
+ return true;
18319
+ }
18320
+ }
18190
18321
  function cleanupMergedWorktree(args) {
18191
- if (!isRegisteredWorktree(args.worktreePath, args.cwd)) {
18322
+ const registration = classifyWorktreeRegistration(args.worktreePath, args.cwd);
18323
+ if (registration.kind === "unregistered" && pathAbsent(args.worktreePath)) {
18324
+ return { removed: true, alreadyGone: true };
18325
+ }
18326
+ if (registration.kind !== "registered") {
18192
18327
  const reason = `refusing to clean up ${args.worktreePath} — git does not list it as a ` + `worktree of the repository at ${args.cwd}, and removing a path git ` + `does not manage would be a recursive delete of an unknown directory. ` + `Pass the worktree path Step B5 created, and re-run from the ` + `repository root.`;
18193
18328
  log52.warn(TAG48, reason);
18194
18329
  return { removed: false, reason };
@@ -18461,7 +18596,8 @@ async function finish(deps) {
18461
18596
  deps.emit({
18462
18597
  type: "cleaned",
18463
18598
  worktree: worktreePath,
18464
- branch: branchGone ? branch : null
18599
+ branch: branchGone ? branch : null,
18600
+ ...out.alreadyGone ? { alreadyRemoved: true } : {}
18465
18601
  });
18466
18602
  }
18467
18603
  } catch (err) {
@@ -18632,6 +18768,7 @@ var init_scan_commands = __esm(() => {
18632
18768
  });
18633
18769
 
18634
18770
  // src/cli.ts
18771
+ init_src();
18635
18772
  init_package_version();
18636
18773
  import { readFileSync as readFileSync11, realpathSync as realpathSync4 } from "node:fs";
18637
18774
  import { dirname as dirname8, join as join10 } from "node:path";
@@ -18911,7 +19048,7 @@ async function reportBuildFreshness(version) {
18911
19048
  }
18912
19049
  if (published === version)
18913
19050
  return;
18914
- process.stderr.write(`preflight: npm serves ${published}. If that is NEWER than the build above, this one is stale — ` + `\`npm i -g @gethmy/agent@latest\`, or clear the npx cache (\`rm -rf ~/.npm/_npx\`), because \`@latest\` alone does not re-resolve it.
19051
+ process.stderr.write(`preflight: npm serves ${published}. If that is NEWER than the build above, this one is stale — ` + `\`${HMY_NPM_INSTALL_COMMAND}\` and run it as \`${HMY_AGENT_COMMAND}\`, or clear the npx cache (\`rm -rf ~/.npm/_npx\`), because \`@latest\` alone does not re-resolve it.
18915
19052
  `);
18916
19053
  }
18917
19054
  async function doctorCommand() {
package/dist/index.js CHANGED
@@ -1102,9 +1102,14 @@ var init_commentSerializer = __esm(() => {
1102
1102
  });
1103
1103
 
1104
1104
  // ../harmony-shared/src/connectCommands.ts
1105
- var HMY_INSTALL_URL = "https://gethmy.com/install.sh", HMY_INSTALL_COMMAND;
1105
+ function hmyConnectCommand(projectSlug) {
1106
+ const slug = projectSlug?.trim();
1107
+ return slug ? `${CONNECT_VERB} ${slug}` : CONNECT_VERB;
1108
+ }
1109
+ var HMY_INSTALL_URL = "https://gethmy.com/install.sh", HMY_INSTALL_COMMAND, HMY_AGENT_COMMAND = "hmy agent", CONNECT_VERB = "hmy connect", HOSTED_MCP_URL = "https://mcp.gethmy.com/mcp", HOSTED_MCP_CONFIG;
1106
1110
  var init_connectCommands = __esm(() => {
1107
1111
  HMY_INSTALL_COMMAND = `curl -fsSL ${HMY_INSTALL_URL} | bash`;
1112
+ HOSTED_MCP_CONFIG = JSON.stringify({ mcpServers: { harmony: { type: "http", url: HOSTED_MCP_URL } } }, null, 2);
1108
1113
  });
1109
1114
 
1110
1115
  // ../harmony-shared/src/constants.ts
@@ -1577,6 +1582,18 @@ function own(source, key) {
1577
1582
  return Object.hasOwn(source, key) ? source[key] : undefined;
1578
1583
  }
1579
1584
 
1585
+ // ../harmony-shared/src/packageCommands.ts
1586
+ function npxArgs(pkg, args) {
1587
+ return ["-y", `${SCOPE}/${pkg}@latest`, ...args];
1588
+ }
1589
+ function npxCommand(pkg, args = []) {
1590
+ return ["npx", ...npxArgs(pkg, args)].join(" ");
1591
+ }
1592
+ var SCOPE = "@gethmy", HMY_NPM_INSTALL_COMMAND;
1593
+ var init_packageCommands = __esm(() => {
1594
+ HMY_NPM_INSTALL_COMMAND = `npm i -g ${SCOPE}/cli`;
1595
+ });
1596
+
1580
1597
  // ../harmony-shared/src/parsedState.ts
1581
1598
  function parseJsonWithoutPrototypes(text) {
1582
1599
  return JSON.parse(text, (_key, value) => {
@@ -2574,6 +2591,7 @@ var init_src = __esm(() => {
2574
2591
  init_gateEvaluate();
2575
2592
  init_logger();
2576
2593
  init_memoryQuery();
2594
+ init_packageCommands();
2577
2595
  init_playbookAutoBind();
2578
2596
  init_playbookCatalog();
2579
2597
  init_playbookStage();
@@ -3515,7 +3533,7 @@ function agentIdentifier(workerId) {
3515
3533
  function endStatusForCancel(reason) {
3516
3534
  return reason === "human_stop" ? "cancelled" : "paused";
3517
3535
  }
3518
- var DEFAULT_FINDING_AUTHORS, DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", REVIEW_FAILURE_REASON_MAX = 300, AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
3536
+ var DEFAULT_FINDING_AUTHORS, DEFAULT_HUMAN_STAGE_HOLD_MINUTES = 60, DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", REVIEW_FAILURE_REASON_MAX = 300, AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
3519
3537
  var init_types = __esm(() => {
3520
3538
  init_src();
3521
3539
  init_board_review();
@@ -3659,7 +3677,12 @@ var init_types = __esm(() => {
3659
3677
  runLogDays: 30
3660
3678
  },
3661
3679
  planning: DEFAULT_PLANNING_CONFIG,
3662
- playbooks: { enabled: true, humanStageColumns: [], metrics: {} },
3680
+ playbooks: {
3681
+ enabled: true,
3682
+ humanStageColumns: [],
3683
+ humanStageHoldMinutes: DEFAULT_HUMAN_STAGE_HOLD_MINUTES,
3684
+ metrics: {}
3685
+ },
3663
3686
  contractFirst: DEFAULT_CONTRACT_CONFIG,
3664
3687
  boardReview: DEFAULT_BOARD_REVIEW_CONFIG,
3665
3688
  sweep: {
@@ -4040,13 +4063,13 @@ function loadDaemonConfig() {
4040
4063
  const projectId = getActiveProjectId(repoRoot);
4041
4064
  const userEmail = getUserEmail();
4042
4065
  if (!workspaceId) {
4043
- throw new Error(`No active workspace configured in ${contextSource.path}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4066
+ throw new Error(`No active workspace configured in ${contextSource.path}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4044
4067
  }
4045
4068
  if (!projectId) {
4046
- throw new Error(`No active project configured in ${contextSource.path}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4069
+ throw new Error(`No active project configured in ${contextSource.path}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4047
4070
  }
4048
4071
  if (!userEmail) {
4049
- throw new Error(`No user email configured in ${contextSource.globalPath}. Run \`hmy connect\` first (no install: \`npx @gethmy/mcp connect\`).`);
4072
+ throw new Error(`No user email configured in ${contextSource.globalPath}. Run \`${hmyConnectCommand()}\` first (no install: \`${npxCommand("mcp", ["connect"])}\`).`);
4050
4073
  }
4051
4074
  let agentOverrides = {};
4052
4075
  let agentName = "Harmony Agent";
@@ -4159,6 +4182,7 @@ function createApiClient(config) {
4159
4182
  }
4160
4183
  var TAG5 = "config", NOT_A_REPOSITORY_EXIT = 128, warnedLegacyConfigDir = false;
4161
4184
  var init_config = __esm(() => {
4185
+ init_src();
4162
4186
  init_config_merge();
4163
4187
  init_config_validation();
4164
4188
  init_paths();
@@ -8816,7 +8840,7 @@ class StateStore {
8816
8840
  const rec = this.getCard(cardId);
8817
8841
  if (!rec)
8818
8842
  return;
8819
- if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && (!clearTurns || !rec.totalTurns && !rec.runStarts)) {
8843
+ if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && rec.stageHold == null && (!clearTurns || !rec.totalTurns && !rec.runStarts)) {
8820
8844
  return;
8821
8845
  }
8822
8846
  rec.attempts = 0;
@@ -8826,6 +8850,7 @@ class StateStore {
8826
8850
  rec.totalTurns = 0;
8827
8851
  if (clearTurns)
8828
8852
  rec.runStarts = 0;
8853
+ rec.stageHold = null;
8829
8854
  await this.persist();
8830
8855
  }
8831
8856
  getCiRepairAttempts(cardId) {
@@ -8908,6 +8933,29 @@ class StateStore {
8908
8933
  getCardRunStarts(cardId) {
8909
8934
  return this.getCard(cardId)?.runStarts ?? 0;
8910
8935
  }
8936
+ getStageHold(cardId) {
8937
+ return this.getCard(cardId)?.stageHold ?? null;
8938
+ }
8939
+ async recordStageHold(cardId, hold) {
8940
+ const rec = this.ensureCard(cardId);
8941
+ const prior = rec.stageHold ?? null;
8942
+ const announced = prior != null && prior.stageId === hold.stageId && prior.reason === hold.reason;
8943
+ rec.stageHold = cloneWithoutPrototype({
8944
+ stageId: hold.stageId,
8945
+ reason: hold.reason,
8946
+ until: hold.untilMs,
8947
+ announcedAt: announced ? prior.announcedAt : Date.now()
8948
+ });
8949
+ await this.persist();
8950
+ return { announced };
8951
+ }
8952
+ async clearStageHold(cardId) {
8953
+ const rec = this.getCard(cardId);
8954
+ if (!rec || rec.stageHold == null)
8955
+ return;
8956
+ rec.stageHold = null;
8957
+ await this.persist();
8958
+ }
8911
8959
  getCardCostCents(cardId) {
8912
8960
  return this.getCard(cardId)?.totalCostCents ?? 0;
8913
8961
  }
@@ -10218,6 +10266,27 @@ class SleepGuard {
10218
10266
  var TAG26 = "sleep-guard";
10219
10267
  var init_sleep_guard = () => {};
10220
10268
 
10269
+ // src/stage-hold.ts
10270
+ function classifyStageHold(card, hold, nowMs) {
10271
+ if (!hold)
10272
+ return { kind: "none" };
10273
+ if (!card.current_stage || card.current_stage !== hold.stageId) {
10274
+ return { kind: "stale" };
10275
+ }
10276
+ if (nowMs >= hold.until)
10277
+ return { kind: "lapsed" };
10278
+ const remainingMin = Math.ceil((hold.until - nowMs) / 60000);
10279
+ return {
10280
+ kind: "holding",
10281
+ detail: `parked on human-owned stage "${hold.stageId}" — holding for ~${remainingMin}m`
10282
+ };
10283
+ }
10284
+ function stageHoldUntil(holdMinutes, nowMs) {
10285
+ if (!Number.isFinite(holdMinutes) || holdMinutes <= 0)
10286
+ return nowMs;
10287
+ return nowMs + holdMinutes * 60000;
10288
+ }
10289
+
10221
10290
  // src/unblock.ts
10222
10291
  import { log as log30 } from "@gethmy/harness";
10223
10292
  async function fetchBlocksLinks(client, cardId) {
@@ -11454,6 +11523,29 @@ var init_stage_advance = __esm(() => {
11454
11523
  });
11455
11524
 
11456
11525
  // src/workspace-model-config.ts
11526
+ function layerPlatformDefaults(platform, ws) {
11527
+ if (!platform)
11528
+ return ws ?? null;
11529
+ if (!ws)
11530
+ return platform;
11531
+ const merged = { ...platform };
11532
+ for (const [key, value] of Object.entries(ws)) {
11533
+ if (value !== null && value !== undefined) {
11534
+ merged[key] = value;
11535
+ }
11536
+ }
11537
+ if (platform.tiers || ws.tiers) {
11538
+ const tiers = {};
11539
+ for (const layer of [platform.tiers, ws.tiers]) {
11540
+ for (const [tier, model] of Object.entries(layer ?? {})) {
11541
+ if (model)
11542
+ tiers[tier] = model;
11543
+ }
11544
+ }
11545
+ merged.tiers = tiers;
11546
+ }
11547
+ return merged;
11548
+ }
11457
11549
  function workspaceConfigToPartial(ws) {
11458
11550
  const claude = {};
11459
11551
  if (ws.implement_model)
@@ -11976,7 +12068,7 @@ class Worker {
11976
12068
  stageCtx = await this.resolveStageContext(card);
11977
12069
  if (stageCtx.kind === "hold") {
11978
12070
  this.held = true;
11979
- await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
12071
+ await this.holdStageCard(card, stageCtx.reason, stageCtx.wait, stageCtx.wait);
11980
12072
  return;
11981
12073
  }
11982
12074
  if (stageCtx.kind === "fanout") {
@@ -12534,15 +12626,35 @@ class Worker {
12534
12626
  }
12535
12627
  }
12536
12628
  }
12537
- async holdStageCard(card, reason, wait = false) {
12629
+ async holdStageCard(card, reason, wait = false, armCooldown = false) {
12538
12630
  log34.info(this.tag, `Holding #${card.short_id}: ${reason}`);
12539
12631
  await this.stateStore.decrementAttempt(card.id);
12540
- try {
12541
- await this.client.addComment(card.id, reason, {
12542
- commentType: "blocker",
12543
- agentSessionId: this.sessionId ?? undefined
12544
- });
12545
- } catch {}
12632
+ let announced = false;
12633
+ const holdStage = armCooldown ? card.current_stage : null;
12634
+ if (holdStage) {
12635
+ try {
12636
+ const recorded = await this.stateStore.recordStageHold(card.id, {
12637
+ stageId: holdStage,
12638
+ reason,
12639
+ untilMs: stageHoldUntil(this.config.playbooks.humanStageHoldMinutes ?? DEFAULT_HUMAN_STAGE_HOLD_MINUTES, Date.now())
12640
+ });
12641
+ announced = recorded.announced;
12642
+ } catch (err) {
12643
+ log34.warn(this.tag, `stage hold not recorded on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
12644
+ }
12645
+ }
12646
+ if (announced) {
12647
+ log34.debug(this.tag, `#${card.short_id} already carries this blocker — not reposting`);
12648
+ } else {
12649
+ try {
12650
+ await this.client.addComment(card.id, reason, {
12651
+ commentType: "blocker",
12652
+ agentSessionId: this.sessionId ?? undefined
12653
+ });
12654
+ } catch {
12655
+ await this.stateStore.clearStageHold(card.id).catch(() => {});
12656
+ }
12657
+ }
12546
12658
  try {
12547
12659
  await runTransition(this.client, card, {
12548
12660
  removeLabels: ["agent"],
@@ -13969,8 +14081,8 @@ class Pool {
13969
14081
  this.reviewQueue = new PriorityQueue(config);
13970
14082
  this.budget = new BudgetGuard(config.budget, this.stateStore);
13971
14083
  const workspaceModels = daemonConfig ? new WorkspaceModelCache(async () => {
13972
- const { config: wsConfig } = await client.getWorkspaceModelConfig(workspaceId);
13973
- return wsConfig ?? null;
14084
+ const { config: wsConfig, platformDefaults } = await client.getWorkspaceModelConfig(workspaceId);
14085
+ return layerPlatformDefaults(platformDefaults, wsConfig);
13974
14086
  }) : undefined;
13975
14087
  const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => {
13976
14088
  const { models } = await client.getModelCatalog();
@@ -14037,6 +14149,14 @@ class Pool {
14037
14149
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
14038
14150
  return;
14039
14151
  }
14152
+ const stageHold = classifyStageHold(card, this.stateStore.getStageHold(card.id), Date.now());
14153
+ if (stageHold.kind === "holding") {
14154
+ log36.debug(TAG32, `#${card.short_id} held — ${stageHold.detail}`);
14155
+ return;
14156
+ }
14157
+ if (stageHold.kind === "stale") {
14158
+ await this.stateStore.clearStageHold(card.id);
14159
+ }
14040
14160
  const decision = this.budget.check(card.id);
14041
14161
  if (!decision.allow) {
14042
14162
  if (decision.reason === "daily_budget") {
@@ -14735,6 +14855,7 @@ var init_pool = __esm(() => {
14735
14855
  init_unblock();
14736
14856
  init_worker();
14737
14857
  init_workspace_model_cache();
14858
+ init_workspace_model_config();
14738
14859
  });
14739
14860
 
14740
14861
  // src/port-registry.ts
@@ -14973,6 +15094,8 @@ var init_repo_dirs = __esm(() => {
14973
15094
 
14974
15095
  // src/card-trust.ts
14975
15096
  function isTrustedCardAuthor(card, trustedAuthors) {
15097
+ if (card.source != null)
15098
+ return false;
14976
15099
  return card.created_by != null && trustedAuthors.has(card.created_by);
14977
15100
  }
14978
15101
 
@@ -15032,7 +15155,7 @@ async function reclaimPreReviewStrands(opts) {
15032
15155
  continue;
15033
15156
  const daemonBranch = daemonBranches.get(card.id);
15034
15157
  if (!isTrustedCardAuthor(card, trustedAuthors) && daemonBranch !== branch) {
15035
- log40.debug(TAG36, `#${card.short_id} - skipped: author ${card.created_by ?? "(none)"} is not trusted, and ${daemonBranch ? `the card records ${branch} while this daemon pushed ${daemonBranch}` : "this daemon has pushed no branch for it"}`);
15158
+ log40.debug(TAG36, `#${card.short_id} - skipped: ${card.source != null ? `card was filed externally (source: ${card.source})` : `author ${card.created_by ?? "(none)"} is not trusted`}, and ${daemonBranch ? `the card records ${branch} while this daemon pushed ${daemonBranch}` : "this daemon has pushed no branch for it"}`);
15036
15159
  continue;
15037
15160
  }
15038
15161
  const labels = resolveCardLabels(card, labelMap);
@@ -15270,7 +15393,7 @@ function rankSweepCandidates(opts) {
15270
15393
  continue;
15271
15394
  }
15272
15395
  if (!isTrustedCardAuthor(card, trustedAuthors)) {
15273
- skip2(card, "untrusted_author", card.created_by == null ? "card has no recorded author" : `author ${card.created_by} is not in sweep.trustedAuthors`);
15396
+ skip2(card, "untrusted_author", card.source != null ? `card was filed externally (source: ${card.source})` : card.created_by == null ? "card has no recorded author" : `author ${card.created_by} is not in sweep.trustedAuthors`);
15274
15397
  continue;
15275
15398
  }
15276
15399
  if (card.assigned_agent_id != null) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "Push-based agent daemon for Harmony — picks up assigned cards, builds them in isolated git worktrees, and opens PRs. It runs where you put it.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -45,8 +45,8 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@anthropic-ai/claude-agent-sdk": "^0.3.178",
48
- "@gethmy/harness": "1.19.0",
49
- "@gethmy/mcp": "3.15.0",
48
+ "@gethmy/harness": "1.19.1",
49
+ "@gethmy/mcp": "3.16.0",
50
50
  "@supabase/supabase-js": "2.95.3"
51
51
  },
52
52
  "devDependencies": {