@gethmy/agent 1.45.0 → 1.47.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();
@@ -3548,12 +3566,12 @@ var init_types = __esm(() => {
3548
3566
  postSummary: true
3549
3567
  },
3550
3568
  claude: {
3551
- model: "claude-opus-5",
3569
+ model: "claude-opus-5-5",
3552
3570
  escalateModel: "claude-fable-5-1",
3553
3571
  escalateAfterAttempts: 2,
3554
3572
  tiers: {
3555
3573
  simple: "claude-sonnet-5",
3556
- advanced: "claude-opus-5",
3574
+ advanced: "claude-opus-5-5",
3557
3575
  research: "claude-fable-5-1"
3558
3576
  },
3559
3577
  scoreBands: DEFAULT_SCORE_BANDS,
@@ -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)
@@ -11503,6 +11595,15 @@ function resolveEffectiveAgentConfig(cfg, ws) {
11503
11595
  const withWorkspace = ws ? mergeAgentConfig(DEFAULT_AGENT_CONFIG, workspaceConfigToPartial(ws)) : DEFAULT_AGENT_CONFIG;
11504
11596
  return mergeAgentConfig(mergeAgentConfig(withWorkspace, cfg.agentOverrides), cfg.repoAgentOverrides);
11505
11597
  }
11598
+ function withdrawnMapFrom(models) {
11599
+ const withdrawn = {};
11600
+ for (const m of models) {
11601
+ if (m.status === "withdrawn" && m.replaced_by) {
11602
+ withdrawn[m.id] = m.replaced_by;
11603
+ }
11604
+ }
11605
+ return withdrawn;
11606
+ }
11506
11607
  var init_workspace_model_config = __esm(() => {
11507
11608
  init_config_merge();
11508
11609
  init_types();
@@ -11977,7 +12078,7 @@ class Worker {
11977
12078
  stageCtx = await this.resolveStageContext(card);
11978
12079
  if (stageCtx.kind === "hold") {
11979
12080
  this.held = true;
11980
- await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
12081
+ await this.holdStageCard(card, stageCtx.reason, stageCtx.wait, stageCtx.wait);
11981
12082
  return;
11982
12083
  }
11983
12084
  if (stageCtx.kind === "fanout") {
@@ -12535,15 +12636,35 @@ class Worker {
12535
12636
  }
12536
12637
  }
12537
12638
  }
12538
- async holdStageCard(card, reason, wait = false) {
12639
+ async holdStageCard(card, reason, wait = false, armCooldown = false) {
12539
12640
  log34.info(this.tag, `Holding #${card.short_id}: ${reason}`);
12540
12641
  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 {}
12642
+ let announced = false;
12643
+ const holdStage = armCooldown ? card.current_stage : null;
12644
+ if (holdStage) {
12645
+ try {
12646
+ const recorded = await this.stateStore.recordStageHold(card.id, {
12647
+ stageId: holdStage,
12648
+ reason,
12649
+ untilMs: stageHoldUntil(this.config.playbooks.humanStageHoldMinutes ?? DEFAULT_HUMAN_STAGE_HOLD_MINUTES, Date.now())
12650
+ });
12651
+ announced = recorded.announced;
12652
+ } catch (err) {
12653
+ log34.warn(this.tag, `stage hold not recorded on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
12654
+ }
12655
+ }
12656
+ if (announced) {
12657
+ log34.debug(this.tag, `#${card.short_id} already carries this blocker — not reposting`);
12658
+ } else {
12659
+ try {
12660
+ await this.client.addComment(card.id, reason, {
12661
+ commentType: "blocker",
12662
+ agentSessionId: this.sessionId ?? undefined
12663
+ });
12664
+ } catch {
12665
+ await this.stateStore.clearStageHold(card.id).catch(() => {});
12666
+ }
12667
+ }
12547
12668
  try {
12548
12669
  await runTransition(this.client, card, {
12549
12670
  removeLabels: ["agent"],
@@ -13970,19 +14091,10 @@ class Pool {
13970
14091
  this.reviewQueue = new PriorityQueue(config);
13971
14092
  this.budget = new BudgetGuard(config.budget, this.stateStore);
13972
14093
  const workspaceModels = daemonConfig ? new WorkspaceModelCache(async () => {
13973
- const { config: wsConfig } = await client.getWorkspaceModelConfig(workspaceId);
13974
- return wsConfig ?? null;
13975
- }) : undefined;
13976
- const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => {
13977
- const { models } = await client.getModelCatalog();
13978
- const withdrawn = {};
13979
- for (const m of models) {
13980
- if (m.status === "withdrawn" && m.replaced_by) {
13981
- withdrawn[m.id] = m.replaced_by;
13982
- }
13983
- }
13984
- return withdrawn;
14094
+ const { config: wsConfig, platformDefaults } = await client.getWorkspaceModelConfig(workspaceId);
14095
+ return layerPlatformDefaults(platformDefaults, wsConfig);
13985
14096
  }) : undefined;
14097
+ const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => withdrawnMapFrom((await client.getModelCatalog()).models)) : undefined;
13986
14098
  const reportedConfig = daemonConfig ? new ReportedConfigCache(client, effectiveModelReport(config)) : undefined;
13987
14099
  for (let i = 0;i < config.poolSize; i++) {
13988
14100
  this.implWorkers.push(new Worker(i, config, client, this.identity, () => {
@@ -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) {
@@ -16107,6 +16230,93 @@ var init_reconcile = __esm(() => {
16107
16230
  init_types();
16108
16231
  });
16109
16232
 
16233
+ // src/startup-model.ts
16234
+ function describeStartupModel(config, layers, withdrawn) {
16235
+ const workspace = layers?.workspace ?? null;
16236
+ const platform = layers?.platform ?? null;
16237
+ const database = layerPlatformDefaults(platform, workspace);
16238
+ const claude = resolveEffectiveAgentConfig(config, database).claude;
16239
+ const pinned = ownObject(config.agentOverrides, "claude");
16240
+ const pinnedTiers = ownObject(pinned, "tiers");
16241
+ const pinnedModel = ownString(pinned, "model");
16242
+ let source = "built-in default";
16243
+ if (pinnedModel)
16244
+ source = "config.json";
16245
+ else if (workspace?.implement_model)
16246
+ source = "workspace";
16247
+ else if (platform?.implement_model)
16248
+ source = "platform";
16249
+ const warnings = [];
16250
+ const pins = [
16251
+ ["claude.model", pinnedModel, database?.implement_model],
16252
+ [
16253
+ "claude.escalateModel",
16254
+ ownString(pinned, "escalateModel"),
16255
+ database?.escalate_model
16256
+ ],
16257
+ ...TIERS.map((tier) => [
16258
+ `claude.tiers.${tier}`,
16259
+ ownString(pinnedTiers, tier),
16260
+ database?.tiers?.[tier]
16261
+ ])
16262
+ ];
16263
+ for (const [key, local, db] of pins) {
16264
+ if (local && db && local !== db) {
16265
+ warnings.push(`config.json pins ${key} to "${local}", which overrides the database default "${db}" — remove it from config.json to follow the app's model settings`);
16266
+ }
16267
+ }
16268
+ const effective = [
16269
+ ["claude.model", claude.model],
16270
+ ["claude.escalateModel", claude.escalateModel],
16271
+ ...TIERS.map((tier) => [`claude.tiers.${tier}`, claude.tiers[tier]])
16272
+ ];
16273
+ for (const [key, id] of effective) {
16274
+ const replacement = withdrawn?.[id];
16275
+ if (replacement) {
16276
+ warnings.push(`${key} resolves to "${id}", which is withdrawn in the model catalog — runs use its replacement "${replacement}"; update the setting to match`);
16277
+ }
16278
+ }
16279
+ return {
16280
+ model: claude.model,
16281
+ escalateModel: claude.escalateModel,
16282
+ reviewModel: config.agent.claude.reviewModel,
16283
+ source,
16284
+ resolved: layers !== null && withdrawn !== null,
16285
+ warnings
16286
+ };
16287
+ }
16288
+ async function resolveStartupModel(config, client) {
16289
+ const [layers, withdrawn] = await Promise.all([
16290
+ client.getWorkspaceModelConfig(config.workspaceId).then(({ config: workspace, platformDefaults }) => ({
16291
+ workspace,
16292
+ platform: platformDefaults ?? null
16293
+ })).catch(() => null),
16294
+ client.getModelCatalog().then(({ models }) => withdrawnMapFrom(models)).catch(() => null)
16295
+ ]);
16296
+ return describeStartupModel(config, layers, withdrawn);
16297
+ }
16298
+ function ownObject(source, key) {
16299
+ if (typeof source !== "object" || source === null)
16300
+ return {};
16301
+ const value = own(source, key);
16302
+ return typeof value === "object" && value !== null ? value : {};
16303
+ }
16304
+ function ownString(source, key) {
16305
+ const value = own(source, key);
16306
+ return typeof value === "string" && value !== "" ? value : undefined;
16307
+ }
16308
+ function startupModelLine(m) {
16309
+ const models = m.model === m.escalateModel ? m.model : `${m.model} → ${m.escalateModel} on retry/high · review ${m.reviewModel}`;
16310
+ const origin = m.resolved ? `from ${m.source}` : `from ${m.source}; database model settings unreachable, a pickup may resolve differently`;
16311
+ return `${models} (${origin})`;
16312
+ }
16313
+ var TIERS;
16314
+ var init_startup_model = __esm(() => {
16315
+ init_src();
16316
+ init_workspace_model_config();
16317
+ TIERS = ["simple", "advanced", "research"];
16318
+ });
16319
+
16110
16320
  // src/startup-banner.ts
16111
16321
  var exports_startup_banner = {};
16112
16322
  __export(exports_startup_banner, {
@@ -16122,6 +16332,7 @@ function prettyBanner(config, version) {
16122
16332
  let projectName;
16123
16333
  let gitProvider;
16124
16334
  let httpPort;
16335
+ let model;
16125
16336
  let failed = false;
16126
16337
  let rendered = false;
16127
16338
  return {
@@ -16134,6 +16345,9 @@ function prettyBanner(config, version) {
16134
16345
  setHttpPort(port) {
16135
16346
  httpPort = port;
16136
16347
  },
16348
+ setModel(resolved) {
16349
+ model = resolved;
16350
+ },
16137
16351
  check(message) {
16138
16352
  checks.push({ kind: "ok", message });
16139
16353
  },
@@ -16155,6 +16369,7 @@ function prettyBanner(config, version) {
16155
16369
  projectName,
16156
16370
  gitProvider,
16157
16371
  httpPort,
16372
+ model,
16158
16373
  checks,
16159
16374
  readyMessage: message
16160
16375
  });
@@ -16164,12 +16379,13 @@ function prettyBanner(config, version) {
16164
16379
  }
16165
16380
  function jsonBanner(config, version) {
16166
16381
  log45.info(TAG41, `Harmony Agent Daemon v${version} starting...`);
16167
- log45.info(TAG41, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
16382
+ log45.info(TAG41, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
16168
16383
  log45.info(TAG41, `Config: ${contextSourceLine(config)}`);
16169
16384
  if (config.agent.review.enabled) {
16170
16385
  log45.info(TAG41, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
16171
16386
  }
16172
16387
  let failed = false;
16388
+ let modelLogged = false;
16173
16389
  return {
16174
16390
  setProjectName(_name) {},
16175
16391
  setGitProvider(provider) {
@@ -16178,6 +16394,10 @@ function jsonBanner(config, version) {
16178
16394
  setHttpPort(port) {
16179
16395
  log45.info(TAG41, `HTTP server on port ${port}`);
16180
16396
  },
16397
+ setModel(resolved) {
16398
+ modelLogged = true;
16399
+ log45.info(TAG41, `Model: ${startupModelLine(resolved)}`);
16400
+ },
16181
16401
  check(message) {
16182
16402
  log45.info(TAG41, message);
16183
16403
  },
@@ -16190,6 +16410,8 @@ function jsonBanner(config, version) {
16190
16410
  async ready(message) {
16191
16411
  if (failed)
16192
16412
  return;
16413
+ if (!modelLogged)
16414
+ log45.info(TAG41, `Model: ${localModelLine(config)}`);
16193
16415
  log45.info(TAG41, message);
16194
16416
  }
16195
16417
  };
@@ -16201,6 +16423,7 @@ function renderPretty(input) {
16201
16423
  projectName,
16202
16424
  gitProvider,
16203
16425
  httpPort,
16426
+ model,
16204
16427
  checks,
16205
16428
  readyMessage
16206
16429
  } = input;
@@ -16208,7 +16431,7 @@ function renderPretty(input) {
16208
16431
  lines.push("");
16209
16432
  lines.push(titleRule(`Harmony Agent Daemon v${version}`));
16210
16433
  lines.push("");
16211
- for (const row of configRows(config, projectName, gitProvider, httpPort)) {
16434
+ for (const row of configRows(config, projectName, gitProvider, httpPort, model)) {
16212
16435
  lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
16213
16436
  }
16214
16437
  lines.push("");
@@ -16222,7 +16445,7 @@ function renderPretty(input) {
16222
16445
  return lines.join(`
16223
16446
  `);
16224
16447
  }
16225
- function configRows(config, projectName, gitProvider, httpPort) {
16448
+ function configRows(config, projectName, gitProvider, httpPort, model) {
16226
16449
  const rows = [];
16227
16450
  const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
16228
16451
  rows.push({ label: "Project", value: projectLabel });
@@ -16231,8 +16454,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
16231
16454
  const reviewEnabled = config.agent.review.enabled;
16232
16455
  const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
16233
16456
  const flowDesc = reviewEnabled ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}` : `Pickup ${config.agent.pickupColumns.join(", ")}`;
16234
- const { model, escalateModel, reviewModel } = config.agent.claude;
16235
- const modelDesc = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
16457
+ const modelDesc = model ? startupModelLine(model) : localModelLine(config);
16236
16458
  rows.push({
16237
16459
  label: "Model",
16238
16460
  value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
@@ -16266,6 +16488,11 @@ function titleRule(title) {
16266
16488
  const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
16267
16489
  return dim(`${prefix}${title}${surround}${suffix}`);
16268
16490
  }
16491
+ function localModelLine(config) {
16492
+ const { model, escalateModel, reviewModel } = config.agent.claude;
16493
+ const models = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
16494
+ return `${models} (local config only; database model settings not read)`;
16495
+ }
16269
16496
  function runnerDesc(runner) {
16270
16497
  return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
16271
16498
  }
@@ -16286,6 +16513,7 @@ function yellow(s) {
16286
16513
  var TAG41 = "daemon", RULE_WIDTH = 70, ANSI;
16287
16514
  var init_startup_banner = __esm(() => {
16288
16515
  init_port_registry();
16516
+ init_startup_model();
16289
16517
  ANSI = {
16290
16518
  reset: "\x1B[0m",
16291
16519
  dim: "\x1B[2m",
@@ -17643,6 +17871,11 @@ async function main() {
17643
17871
  } catch (err) {
17644
17872
  log50.warn(TAG46, `Could not report model config: ${err instanceof Error ? err.message : String(err)}`);
17645
17873
  }
17874
+ const startupModel = await resolveStartupModel(config, client);
17875
+ banner.setModel(startupModel);
17876
+ for (const warning of startupModel.warnings) {
17877
+ banner.warn(warning);
17878
+ }
17646
17879
  migrateConfigDir();
17647
17880
  try {
17648
17881
  migrateLegacyStateFile({
@@ -17965,6 +18198,7 @@ var init_src2 = __esm(() => {
17965
18198
  init_recovery();
17966
18199
  init_review_worktree();
17967
18200
  init_startup_banner();
18201
+ init_startup_model();
17968
18202
  init_state_migration();
17969
18203
  init_state_store();
17970
18204
  init_stream_parser_selftest();
@@ -18082,7 +18316,7 @@ var init_head_anchor = () => {};
18082
18316
 
18083
18317
  // src/merge-cleanup.ts
18084
18318
  import { execFileSync as execFileSync11 } from "node:child_process";
18085
- import { existsSync as existsSync7, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
18319
+ import { existsSync as existsSync7, lstatSync as lstatSync3, realpathSync as realpathSync2, statSync as statSync6 } from "node:fs";
18086
18320
  import { basename, dirname as dirname7, isAbsolute, resolve as resolve6, sep as sep4 } from "node:path";
18087
18321
  import { cleanupWorktree as cleanupWorktree6, GIT_NO_HOOKS as GIT_NO_HOOKS12, log as log52 } from "@gethmy/harness";
18088
18322
  function canonicalize(p) {
@@ -18187,8 +18421,20 @@ function classifyWorktreeRegistration(worktreePath, cwd) {
18187
18421
  function isRegisteredWorktree(worktreePath, cwd) {
18188
18422
  return classifyWorktreeRegistration(worktreePath, cwd).kind === "registered";
18189
18423
  }
18424
+ function pathAbsent(p) {
18425
+ try {
18426
+ lstatSync3(p);
18427
+ return false;
18428
+ } catch {
18429
+ return true;
18430
+ }
18431
+ }
18190
18432
  function cleanupMergedWorktree(args) {
18191
- if (!isRegisteredWorktree(args.worktreePath, args.cwd)) {
18433
+ const registration = classifyWorktreeRegistration(args.worktreePath, args.cwd);
18434
+ if (registration.kind === "unregistered" && pathAbsent(args.worktreePath)) {
18435
+ return { removed: true, alreadyGone: true };
18436
+ }
18437
+ if (registration.kind !== "registered") {
18192
18438
  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
18439
  log52.warn(TAG48, reason);
18194
18440
  return { removed: false, reason };
@@ -18461,7 +18707,8 @@ async function finish(deps) {
18461
18707
  deps.emit({
18462
18708
  type: "cleaned",
18463
18709
  worktree: worktreePath,
18464
- branch: branchGone ? branch : null
18710
+ branch: branchGone ? branch : null,
18711
+ ...out.alreadyGone ? { alreadyRemoved: true } : {}
18465
18712
  });
18466
18713
  }
18467
18714
  } catch (err) {
@@ -18632,6 +18879,7 @@ var init_scan_commands = __esm(() => {
18632
18879
  });
18633
18880
 
18634
18881
  // src/cli.ts
18882
+ init_src();
18635
18883
  init_package_version();
18636
18884
  import { readFileSync as readFileSync11, realpathSync as realpathSync4 } from "node:fs";
18637
18885
  import { dirname as dirname8, join as join10 } from "node:path";
@@ -18911,7 +19159,7 @@ async function reportBuildFreshness(version) {
18911
19159
  }
18912
19160
  if (published === version)
18913
19161
  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.
19162
+ 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
19163
  `);
18916
19164
  }
18917
19165
  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();
@@ -3547,12 +3565,12 @@ var init_types = __esm(() => {
3547
3565
  postSummary: true
3548
3566
  },
3549
3567
  claude: {
3550
- model: "claude-opus-5",
3568
+ model: "claude-opus-5-5",
3551
3569
  escalateModel: "claude-fable-5-1",
3552
3570
  escalateAfterAttempts: 2,
3553
3571
  tiers: {
3554
3572
  simple: "claude-sonnet-5",
3555
- advanced: "claude-opus-5",
3573
+ advanced: "claude-opus-5-5",
3556
3574
  research: "claude-fable-5-1"
3557
3575
  },
3558
3576
  scoreBands: DEFAULT_SCORE_BANDS,
@@ -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)
@@ -11502,6 +11594,15 @@ function resolveEffectiveAgentConfig(cfg, ws) {
11502
11594
  const withWorkspace = ws ? mergeAgentConfig(DEFAULT_AGENT_CONFIG, workspaceConfigToPartial(ws)) : DEFAULT_AGENT_CONFIG;
11503
11595
  return mergeAgentConfig(mergeAgentConfig(withWorkspace, cfg.agentOverrides), cfg.repoAgentOverrides);
11504
11596
  }
11597
+ function withdrawnMapFrom(models) {
11598
+ const withdrawn = {};
11599
+ for (const m of models) {
11600
+ if (m.status === "withdrawn" && m.replaced_by) {
11601
+ withdrawn[m.id] = m.replaced_by;
11602
+ }
11603
+ }
11604
+ return withdrawn;
11605
+ }
11505
11606
  var init_workspace_model_config = __esm(() => {
11506
11607
  init_config_merge();
11507
11608
  init_types();
@@ -11976,7 +12077,7 @@ class Worker {
11976
12077
  stageCtx = await this.resolveStageContext(card);
11977
12078
  if (stageCtx.kind === "hold") {
11978
12079
  this.held = true;
11979
- await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
12080
+ await this.holdStageCard(card, stageCtx.reason, stageCtx.wait, stageCtx.wait);
11980
12081
  return;
11981
12082
  }
11982
12083
  if (stageCtx.kind === "fanout") {
@@ -12534,15 +12635,35 @@ class Worker {
12534
12635
  }
12535
12636
  }
12536
12637
  }
12537
- async holdStageCard(card, reason, wait = false) {
12638
+ async holdStageCard(card, reason, wait = false, armCooldown = false) {
12538
12639
  log34.info(this.tag, `Holding #${card.short_id}: ${reason}`);
12539
12640
  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 {}
12641
+ let announced = false;
12642
+ const holdStage = armCooldown ? card.current_stage : null;
12643
+ if (holdStage) {
12644
+ try {
12645
+ const recorded = await this.stateStore.recordStageHold(card.id, {
12646
+ stageId: holdStage,
12647
+ reason,
12648
+ untilMs: stageHoldUntil(this.config.playbooks.humanStageHoldMinutes ?? DEFAULT_HUMAN_STAGE_HOLD_MINUTES, Date.now())
12649
+ });
12650
+ announced = recorded.announced;
12651
+ } catch (err) {
12652
+ log34.warn(this.tag, `stage hold not recorded on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
12653
+ }
12654
+ }
12655
+ if (announced) {
12656
+ log34.debug(this.tag, `#${card.short_id} already carries this blocker — not reposting`);
12657
+ } else {
12658
+ try {
12659
+ await this.client.addComment(card.id, reason, {
12660
+ commentType: "blocker",
12661
+ agentSessionId: this.sessionId ?? undefined
12662
+ });
12663
+ } catch {
12664
+ await this.stateStore.clearStageHold(card.id).catch(() => {});
12665
+ }
12666
+ }
12546
12667
  try {
12547
12668
  await runTransition(this.client, card, {
12548
12669
  removeLabels: ["agent"],
@@ -13969,19 +14090,10 @@ class Pool {
13969
14090
  this.reviewQueue = new PriorityQueue(config);
13970
14091
  this.budget = new BudgetGuard(config.budget, this.stateStore);
13971
14092
  const workspaceModels = daemonConfig ? new WorkspaceModelCache(async () => {
13972
- const { config: wsConfig } = await client.getWorkspaceModelConfig(workspaceId);
13973
- return wsConfig ?? null;
13974
- }) : undefined;
13975
- const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => {
13976
- const { models } = await client.getModelCatalog();
13977
- const withdrawn = {};
13978
- for (const m of models) {
13979
- if (m.status === "withdrawn" && m.replaced_by) {
13980
- withdrawn[m.id] = m.replaced_by;
13981
- }
13982
- }
13983
- return withdrawn;
14093
+ const { config: wsConfig, platformDefaults } = await client.getWorkspaceModelConfig(workspaceId);
14094
+ return layerPlatformDefaults(platformDefaults, wsConfig);
13984
14095
  }) : undefined;
14096
+ const modelCatalog = daemonConfig ? new WorkspaceModelCache(async () => withdrawnMapFrom((await client.getModelCatalog()).models)) : undefined;
13985
14097
  const reportedConfig = daemonConfig ? new ReportedConfigCache(client, effectiveModelReport(config)) : undefined;
13986
14098
  for (let i = 0;i < config.poolSize; i++) {
13987
14099
  this.implWorkers.push(new Worker(i, config, client, this.identity, () => {
@@ -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) {
@@ -16106,6 +16229,93 @@ var init_reconcile = __esm(() => {
16106
16229
  init_types();
16107
16230
  });
16108
16231
 
16232
+ // src/startup-model.ts
16233
+ function describeStartupModel(config, layers, withdrawn) {
16234
+ const workspace = layers?.workspace ?? null;
16235
+ const platform = layers?.platform ?? null;
16236
+ const database = layerPlatformDefaults(platform, workspace);
16237
+ const claude = resolveEffectiveAgentConfig(config, database).claude;
16238
+ const pinned = ownObject(config.agentOverrides, "claude");
16239
+ const pinnedTiers = ownObject(pinned, "tiers");
16240
+ const pinnedModel = ownString(pinned, "model");
16241
+ let source = "built-in default";
16242
+ if (pinnedModel)
16243
+ source = "config.json";
16244
+ else if (workspace?.implement_model)
16245
+ source = "workspace";
16246
+ else if (platform?.implement_model)
16247
+ source = "platform";
16248
+ const warnings = [];
16249
+ const pins = [
16250
+ ["claude.model", pinnedModel, database?.implement_model],
16251
+ [
16252
+ "claude.escalateModel",
16253
+ ownString(pinned, "escalateModel"),
16254
+ database?.escalate_model
16255
+ ],
16256
+ ...TIERS.map((tier) => [
16257
+ `claude.tiers.${tier}`,
16258
+ ownString(pinnedTiers, tier),
16259
+ database?.tiers?.[tier]
16260
+ ])
16261
+ ];
16262
+ for (const [key, local, db] of pins) {
16263
+ if (local && db && local !== db) {
16264
+ warnings.push(`config.json pins ${key} to "${local}", which overrides the database default "${db}" — remove it from config.json to follow the app's model settings`);
16265
+ }
16266
+ }
16267
+ const effective = [
16268
+ ["claude.model", claude.model],
16269
+ ["claude.escalateModel", claude.escalateModel],
16270
+ ...TIERS.map((tier) => [`claude.tiers.${tier}`, claude.tiers[tier]])
16271
+ ];
16272
+ for (const [key, id] of effective) {
16273
+ const replacement = withdrawn?.[id];
16274
+ if (replacement) {
16275
+ warnings.push(`${key} resolves to "${id}", which is withdrawn in the model catalog — runs use its replacement "${replacement}"; update the setting to match`);
16276
+ }
16277
+ }
16278
+ return {
16279
+ model: claude.model,
16280
+ escalateModel: claude.escalateModel,
16281
+ reviewModel: config.agent.claude.reviewModel,
16282
+ source,
16283
+ resolved: layers !== null && withdrawn !== null,
16284
+ warnings
16285
+ };
16286
+ }
16287
+ async function resolveStartupModel(config, client) {
16288
+ const [layers, withdrawn] = await Promise.all([
16289
+ client.getWorkspaceModelConfig(config.workspaceId).then(({ config: workspace, platformDefaults }) => ({
16290
+ workspace,
16291
+ platform: platformDefaults ?? null
16292
+ })).catch(() => null),
16293
+ client.getModelCatalog().then(({ models }) => withdrawnMapFrom(models)).catch(() => null)
16294
+ ]);
16295
+ return describeStartupModel(config, layers, withdrawn);
16296
+ }
16297
+ function ownObject(source, key) {
16298
+ if (typeof source !== "object" || source === null)
16299
+ return {};
16300
+ const value = own(source, key);
16301
+ return typeof value === "object" && value !== null ? value : {};
16302
+ }
16303
+ function ownString(source, key) {
16304
+ const value = own(source, key);
16305
+ return typeof value === "string" && value !== "" ? value : undefined;
16306
+ }
16307
+ function startupModelLine(m) {
16308
+ const models = m.model === m.escalateModel ? m.model : `${m.model} → ${m.escalateModel} on retry/high · review ${m.reviewModel}`;
16309
+ const origin = m.resolved ? `from ${m.source}` : `from ${m.source}; database model settings unreachable, a pickup may resolve differently`;
16310
+ return `${models} (${origin})`;
16311
+ }
16312
+ var TIERS;
16313
+ var init_startup_model = __esm(() => {
16314
+ init_src();
16315
+ init_workspace_model_config();
16316
+ TIERS = ["simple", "advanced", "research"];
16317
+ });
16318
+
16109
16319
  // src/startup-banner.ts
16110
16320
  var exports_startup_banner = {};
16111
16321
  __export(exports_startup_banner, {
@@ -16121,6 +16331,7 @@ function prettyBanner(config, version) {
16121
16331
  let projectName;
16122
16332
  let gitProvider;
16123
16333
  let httpPort;
16334
+ let model;
16124
16335
  let failed = false;
16125
16336
  let rendered = false;
16126
16337
  return {
@@ -16133,6 +16344,9 @@ function prettyBanner(config, version) {
16133
16344
  setHttpPort(port) {
16134
16345
  httpPort = port;
16135
16346
  },
16347
+ setModel(resolved) {
16348
+ model = resolved;
16349
+ },
16136
16350
  check(message) {
16137
16351
  checks.push({ kind: "ok", message });
16138
16352
  },
@@ -16154,6 +16368,7 @@ function prettyBanner(config, version) {
16154
16368
  projectName,
16155
16369
  gitProvider,
16156
16370
  httpPort,
16371
+ model,
16157
16372
  checks,
16158
16373
  readyMessage: message
16159
16374
  });
@@ -16163,12 +16378,13 @@ function prettyBanner(config, version) {
16163
16378
  }
16164
16379
  function jsonBanner(config, version) {
16165
16380
  log45.info(TAG41, `Harmony Agent Daemon v${version} starting...`);
16166
- log45.info(TAG41, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
16381
+ log45.info(TAG41, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
16167
16382
  log45.info(TAG41, `Config: ${contextSourceLine(config)}`);
16168
16383
  if (config.agent.review.enabled) {
16169
16384
  log45.info(TAG41, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
16170
16385
  }
16171
16386
  let failed = false;
16387
+ let modelLogged = false;
16172
16388
  return {
16173
16389
  setProjectName(_name) {},
16174
16390
  setGitProvider(provider) {
@@ -16177,6 +16393,10 @@ function jsonBanner(config, version) {
16177
16393
  setHttpPort(port) {
16178
16394
  log45.info(TAG41, `HTTP server on port ${port}`);
16179
16395
  },
16396
+ setModel(resolved) {
16397
+ modelLogged = true;
16398
+ log45.info(TAG41, `Model: ${startupModelLine(resolved)}`);
16399
+ },
16180
16400
  check(message) {
16181
16401
  log45.info(TAG41, message);
16182
16402
  },
@@ -16189,6 +16409,8 @@ function jsonBanner(config, version) {
16189
16409
  async ready(message) {
16190
16410
  if (failed)
16191
16411
  return;
16412
+ if (!modelLogged)
16413
+ log45.info(TAG41, `Model: ${localModelLine(config)}`);
16192
16414
  log45.info(TAG41, message);
16193
16415
  }
16194
16416
  };
@@ -16200,6 +16422,7 @@ function renderPretty(input) {
16200
16422
  projectName,
16201
16423
  gitProvider,
16202
16424
  httpPort,
16425
+ model,
16203
16426
  checks,
16204
16427
  readyMessage
16205
16428
  } = input;
@@ -16207,7 +16430,7 @@ function renderPretty(input) {
16207
16430
  lines.push("");
16208
16431
  lines.push(titleRule(`Harmony Agent Daemon v${version}`));
16209
16432
  lines.push("");
16210
- for (const row of configRows(config, projectName, gitProvider, httpPort)) {
16433
+ for (const row of configRows(config, projectName, gitProvider, httpPort, model)) {
16211
16434
  lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
16212
16435
  }
16213
16436
  lines.push("");
@@ -16221,7 +16444,7 @@ function renderPretty(input) {
16221
16444
  return lines.join(`
16222
16445
  `);
16223
16446
  }
16224
- function configRows(config, projectName, gitProvider, httpPort) {
16447
+ function configRows(config, projectName, gitProvider, httpPort, model) {
16225
16448
  const rows = [];
16226
16449
  const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
16227
16450
  rows.push({ label: "Project", value: projectLabel });
@@ -16230,8 +16453,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
16230
16453
  const reviewEnabled = config.agent.review.enabled;
16231
16454
  const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
16232
16455
  const flowDesc = reviewEnabled ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}` : `Pickup ${config.agent.pickupColumns.join(", ")}`;
16233
- const { model, escalateModel, reviewModel } = config.agent.claude;
16234
- const modelDesc = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
16456
+ const modelDesc = model ? startupModelLine(model) : localModelLine(config);
16235
16457
  rows.push({
16236
16458
  label: "Model",
16237
16459
  value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
@@ -16265,6 +16487,11 @@ function titleRule(title) {
16265
16487
  const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
16266
16488
  return dim(`${prefix}${title}${surround}${suffix}`);
16267
16489
  }
16490
+ function localModelLine(config) {
16491
+ const { model, escalateModel, reviewModel } = config.agent.claude;
16492
+ const models = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
16493
+ return `${models} (local config only; database model settings not read)`;
16494
+ }
16268
16495
  function runnerDesc(runner) {
16269
16496
  return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
16270
16497
  }
@@ -16285,6 +16512,7 @@ function yellow(s) {
16285
16512
  var TAG41 = "daemon", RULE_WIDTH = 70, ANSI;
16286
16513
  var init_startup_banner = __esm(() => {
16287
16514
  init_port_registry();
16515
+ init_startup_model();
16288
16516
  ANSI = {
16289
16517
  reset: "\x1B[0m",
16290
16518
  dim: "\x1B[2m",
@@ -17642,6 +17870,11 @@ async function main() {
17642
17870
  } catch (err) {
17643
17871
  log50.warn(TAG46, `Could not report model config: ${err instanceof Error ? err.message : String(err)}`);
17644
17872
  }
17873
+ const startupModel = await resolveStartupModel(config, client);
17874
+ banner.setModel(startupModel);
17875
+ for (const warning of startupModel.warnings) {
17876
+ banner.warn(warning);
17877
+ }
17645
17878
  migrateConfigDir();
17646
17879
  try {
17647
17880
  migrateLegacyStateFile({
@@ -17964,6 +18197,7 @@ var init_src2 = __esm(() => {
17964
18197
  init_recovery();
17965
18198
  init_review_worktree();
17966
18199
  init_startup_banner();
18200
+ init_startup_model();
17967
18201
  init_state_migration();
17968
18202
  init_state_store();
17969
18203
  init_stream_parser_selftest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.45.0",
3
+ "version": "1.47.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": {