@gethmy/agent 1.17.0 → 1.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +145 -84
  2. package/dist/index.js +145 -84
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -105,6 +105,7 @@ var init_log = __esm(() => {
105
105
  var exports_board_helpers = {};
106
106
  __export(exports_board_helpers, {
107
107
  resolveCardLabels: () => resolveCardLabels,
108
+ releaseAssignedAgent: () => releaseAssignedAgent,
108
109
  moveCardToColumn: () => moveCardToColumn,
109
110
  moveCardAndAddLabel: () => moveCardAndAddLabel,
110
111
  hasLabel: () => hasLabel,
@@ -126,6 +127,9 @@ function resolveCardLabels(card, labelMap) {
126
127
  function hasLabel(cardLabels, labelName) {
127
128
  return cardLabels.some((l) => l.name.toLowerCase() === labelName.toLowerCase());
128
129
  }
130
+ async function releaseAssignedAgent(client, cardId) {
131
+ await client.updateCard(cardId, { assignedAgentId: null });
132
+ }
129
133
  async function moveCardToColumn(client, card, targetColumnName) {
130
134
  try {
131
135
  const board = await client.getBoard(card.project_id);
@@ -3862,6 +3866,11 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3862
3866
  }
3863
3867
  if (config.completion.moveToColumn) {
3864
3868
  await moveCardToColumn(client, card, config.completion.moveToColumn);
3869
+ try {
3870
+ await releaseAssignedAgent(client, card.id);
3871
+ } catch (err) {
3872
+ log.warn(TAG15, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3873
+ }
3865
3874
  if (onMovedToCompletion) {
3866
3875
  try {
3867
3876
  await onMovedToCompletion(card);
@@ -6190,6 +6199,7 @@ async function runTransition(client, card, plan, opts = {}) {
6190
6199
  const board = await withRetry("move", shortId, () => client.getBoard(card.project_id), attempts, backoffMs);
6191
6200
  const columns = board.columns;
6192
6201
  const labels = board.labels ?? [];
6202
+ let moveLanded = false;
6193
6203
  if (plan.move) {
6194
6204
  const target = columns.find((c) => c.name.toLowerCase() === plan.move.columnName.toLowerCase());
6195
6205
  if (!target) {
@@ -6202,8 +6212,14 @@ async function runTransition(client, card, plan, opts = {}) {
6202
6212
  await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
6203
6213
  log.info(TAG23, `#${shortId} → "${target.name}"`);
6204
6214
  card.column_id = target.id;
6215
+ moveLanded = true;
6216
+ } else {
6217
+ moveLanded = true;
6205
6218
  }
6206
6219
  }
6220
+ if (moveLanded && plan.onMoved) {
6221
+ await plan.onMoved();
6222
+ }
6207
6223
  if (plan.addLabels?.length) {
6208
6224
  const existing = new Set(card.labelIds ?? []);
6209
6225
  for (const { name, color } of plan.addLabels) {
@@ -6439,7 +6455,15 @@ class ReviewWorker {
6439
6455
  const cwd = this.worktreePath;
6440
6456
  if (!localMode) {
6441
6457
  log.info(this.tag, `Starting dev server on port ${port}...`);
6442
- this.devServerProcess = spawnInGroup("bun", ["run", "dev", "--", "--port", String(port)], { cwd, stdio: ["ignore", "pipe", "pipe"] });
6458
+ const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
6459
+ this.devServerProcess = spawnInGroup(devCmd, devArgs, {
6460
+ cwd,
6461
+ stdio: ["ignore", "pipe", "pipe"]
6462
+ });
6463
+ let devServerSpawnError = null;
6464
+ this.devServerProcess.once("error", (err) => {
6465
+ devServerSpawnError = err;
6466
+ });
6443
6467
  await this.client.updateAgentProgress(card.id, {
6444
6468
  agentIdentifier: agentIdentifier(this.id),
6445
6469
  agentName: `${AGENT_NAME} (Review)`,
@@ -6447,6 +6471,9 @@ class ReviewWorker {
6447
6471
  currentTask: `Starting dev server on port ${port}…`,
6448
6472
  progressPercent: 10
6449
6473
  });
6474
+ if (devServerSpawnError) {
6475
+ throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
6476
+ }
6450
6477
  await waitForDevServer(this.devServerProcess, 30000);
6451
6478
  await probeDevServer(port);
6452
6479
  log.info(this.tag, `Dev server ready on port ${port}`);
@@ -6701,7 +6728,7 @@ class ReviewWorker {
6701
6728
  [parse_error] ${msg}
6702
6729
  `);
6703
6730
  });
6704
- if (this.process.stdout) {
6731
+ if (this.process?.stdout) {
6705
6732
  parser.attach(this.process.stdout);
6706
6733
  if (runLog) {
6707
6734
  this.process.stdout.on("data", (chunk) => {
@@ -6710,14 +6737,14 @@ class ReviewWorker {
6710
6737
  }
6711
6738
  }
6712
6739
  let stderr = "";
6713
- this.process.stderr?.on("data", (data) => {
6740
+ this.process?.stderr?.on("data", (data) => {
6714
6741
  stderr += data.toString();
6715
6742
  runLog?.stream.write(`[stderr] ${data.toString()}`);
6716
6743
  });
6717
- this.process.on("error", (err) => {
6744
+ this.process?.on("error", (err) => {
6718
6745
  reject(new Error(`Failed to spawn claude: ${err.message}`));
6719
6746
  });
6720
- this.process.on("close", (code) => {
6747
+ this.process?.on("close", (code) => {
6721
6748
  this.process = null;
6722
6749
  const stdout = textChunks.join("");
6723
6750
  const stats = tracker.stats;
@@ -6847,6 +6874,7 @@ var init_review_worker = __esm(() => {
6847
6874
  init_gate_collectors();
6848
6875
  init_git_diff_stat();
6849
6876
  init_log();
6877
+ init_pm();
6850
6878
  init_process_group();
6851
6879
  init_progress_tracker();
6852
6880
  init_review_completion();
@@ -7502,16 +7530,16 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
7502
7530
  currentStage: next.stage.id,
7503
7531
  done: false
7504
7532
  });
7505
- deps.sink?.recordPlaybookAdvanced({
7506
- fromStageId: stage.id,
7507
- fromStageName: stage.name,
7508
- toStageId: next.stage.id,
7509
- toStageName: next.stage.name,
7510
- advancedBy: "system",
7511
- reason: summary
7512
- });
7513
7533
  await runTransition(deps.client, card, {
7514
7534
  move: { columnName: toColumn },
7535
+ onMoved: () => deps.sink?.recordPlaybookAdvanced({
7536
+ fromStageId: stage.id,
7537
+ fromStageName: stage.name,
7538
+ toStageId: next.stage.id,
7539
+ toStageName: next.stage.name,
7540
+ advancedBy: "system",
7541
+ reason: summary
7542
+ }),
7515
7543
  addLabels: [{ name: AGENT_LABEL }],
7516
7544
  ...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
7517
7545
  }, { store: deps.stateStore, runId: deps.runId });
@@ -9220,6 +9248,22 @@ var init_recovery = __esm(() => {
9220
9248
  init_worktree();
9221
9249
  });
9222
9250
 
9251
+ // src/claim.ts
9252
+ async function claimReviewCard(client, cardId, agentId) {
9253
+ try {
9254
+ const { claimed } = await client.claimCard(cardId, agentId);
9255
+ log.debug(TAG34, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
9256
+ return claimed;
9257
+ } catch (err) {
9258
+ log.error(TAG34, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
9259
+ return false;
9260
+ }
9261
+ }
9262
+ var TAG34 = "claim";
9263
+ var init_claim = __esm(() => {
9264
+ init_log();
9265
+ });
9266
+
9223
9267
  // src/strand-recovery.ts
9224
9268
  var exports_strand_recovery = {};
9225
9269
  __export(exports_strand_recovery, {
@@ -9266,19 +9310,27 @@ async function reclaimPreReviewStrands(opts) {
9266
9310
  const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
9267
9311
  if (prUrl)
9268
9312
  continue;
9269
- log.warn(TAG34, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
9270
- try {
9271
- await client.updateCard(card.id, { assignedAgentId: agentId });
9272
- reclaimed.push(card.id);
9273
- } catch (err) {
9274
- log.error(TAG34, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9313
+ const won = await claimReviewCard(client, card.id, agentId);
9314
+ if (!won) {
9315
+ log.debug(TAG35, `#${card.short_id} lost the review claim race, skipping`);
9316
+ continue;
9317
+ }
9318
+ log.warn(TAG35, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
9319
+ reclaimed.push(card.id);
9320
+ if (opts.onClaimed) {
9321
+ try {
9322
+ await opts.onClaimed(card);
9323
+ } catch (err) {
9324
+ log.error(TAG35, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
9325
+ }
9275
9326
  }
9276
9327
  }
9277
9328
  return reclaimed;
9278
9329
  }
9279
- var TAG34 = "strand-recovery";
9330
+ var TAG35 = "strand-recovery";
9280
9331
  var init_strand_recovery = __esm(() => {
9281
9332
  init_board_helpers();
9333
+ init_claim();
9282
9334
  init_git_pr();
9283
9335
  init_log();
9284
9336
  init_review_worktree();
@@ -9327,7 +9379,7 @@ class Reconciler {
9327
9379
  clearInterval(this.timer);
9328
9380
  this.timer = null;
9329
9381
  }
9330
- log.info(TAG35, "Heartbeat stopped");
9382
+ log.info(TAG36, "Heartbeat stopped");
9331
9383
  }
9332
9384
  async recoverStaleRuns() {
9333
9385
  if (!this.stateStore || !this.agentConfig)
@@ -9344,7 +9396,7 @@ class Reconciler {
9344
9396
  if (!daemonDead && !(heartbeatStale && ourZombie))
9345
9397
  continue;
9346
9398
  const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
9347
- log.warn(TAG35, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9399
+ log.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9348
9400
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
9349
9401
  runId: run.runId,
9350
9402
  cardId: run.cardId,
@@ -9371,11 +9423,11 @@ class Reconciler {
9371
9423
  const stalledAt = Date.parse(card.updated_at ?? "");
9372
9424
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
9373
9425
  continue;
9374
- log.warn(TAG35, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9426
+ log.warn(TAG36, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9375
9427
  try {
9376
9428
  await this.client.moveCard(card.id, pickupCol.id);
9377
9429
  } catch (err) {
9378
- log.error(TAG35, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9430
+ log.error(TAG36, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9379
9431
  }
9380
9432
  }
9381
9433
  }
@@ -9397,10 +9449,19 @@ class Reconciler {
9397
9449
  labelMap,
9398
9450
  reviewColumns: this.reviewColumns,
9399
9451
  approvedLabel: this.approvedLabel,
9400
- graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
9452
+ graceMs: 0,
9401
9453
  knownCardIds,
9402
9454
  cwd: process.cwd(),
9403
- provider: this.gitProvider
9455
+ provider: this.gitProvider,
9456
+ onClaimed: async (card) => {
9457
+ const column = columns.find((c) => c.id === card.column_id);
9458
+ if (!column)
9459
+ return;
9460
+ const cardLabels = resolveCardLabels(card, labelMap);
9461
+ const subtasks = card.subtasks ?? [];
9462
+ log.info(TAG36, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
9463
+ await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
9464
+ }
9404
9465
  });
9405
9466
  }
9406
9467
  async releaseStalledApprovals(cards, columns, knownCardIds) {
@@ -9422,11 +9483,11 @@ class Reconciler {
9422
9483
  const parkedAt = Date.parse(card.updated_at ?? "");
9423
9484
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
9424
9485
  continue;
9425
- log.warn(TAG35, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
9486
+ log.warn(TAG36, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
9426
9487
  try {
9427
9488
  await this.client.moveCard(card.id, pickupCol.id);
9428
9489
  } catch (err) {
9429
- log.error(TAG35, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9490
+ log.error(TAG36, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9430
9491
  }
9431
9492
  }
9432
9493
  }
@@ -9469,21 +9530,21 @@ class Reconciler {
9469
9530
  const subtasks = card.subtasks ?? [];
9470
9531
  const mode = route.mode;
9471
9532
  if (route.stage) {
9472
- log.info(TAG35, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
9533
+ log.info(TAG36, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
9473
9534
  }
9474
9535
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
9475
- log.debug(TAG35, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
9536
+ log.debug(TAG36, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
9476
9537
  continue;
9477
9538
  }
9478
9539
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
9479
- log.debug(TAG35, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
9540
+ log.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
9480
9541
  continue;
9481
9542
  }
9482
9543
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
9483
- log.debug(TAG35, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
9544
+ log.debug(TAG36, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
9484
9545
  continue;
9485
9546
  }
9486
- log.info(TAG35, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
9547
+ log.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
9487
9548
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
9488
9549
  }
9489
9550
  }
@@ -9494,18 +9555,18 @@ class Reconciler {
9494
9555
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
9495
9556
  for (const knownId of knownCardIds) {
9496
9557
  if (!allAgentCardIds.has(knownId)) {
9497
- log.info(TAG35, `Missed unassign: ${knownId} — removing`);
9558
+ log.info(TAG36, `Missed unassign: ${knownId} — removing`);
9498
9559
  await this.pool.removeCard(knownId);
9499
9560
  }
9500
9561
  }
9501
9562
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
9502
- log.debug(TAG35, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9563
+ log.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9503
9564
  } catch (err) {
9504
- log.error(TAG35, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9565
+ log.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9505
9566
  }
9506
9567
  }
9507
9568
  }
9508
- var TAG35 = "reconcile";
9569
+ var TAG36 = "reconcile";
9509
9570
  var init_reconcile = __esm(() => {
9510
9571
  init_board_helpers();
9511
9572
  init_git_pr();
@@ -9545,7 +9606,7 @@ function prettyBanner(config, version) {
9545
9606
  checks.push({ kind: "ok", message });
9546
9607
  },
9547
9608
  warn(message) {
9548
- log.warn(TAG36, message);
9609
+ log.warn(TAG37, message);
9549
9610
  checks.push({ kind: "warn", message: message.split(`
9550
9611
  `, 1)[0] });
9551
9612
  },
@@ -9570,25 +9631,25 @@ function prettyBanner(config, version) {
9570
9631
  };
9571
9632
  }
9572
9633
  function jsonBanner(config, version) {
9573
- log.info(TAG36, `Harmony Agent Daemon v${version} starting...`);
9574
- log.info(TAG36, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
9634
+ log.info(TAG37, `Harmony Agent Daemon v${version} starting...`);
9635
+ log.info(TAG37, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
9575
9636
  if (config.agent.review.enabled) {
9576
- log.info(TAG36, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
9637
+ log.info(TAG37, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
9577
9638
  }
9578
9639
  let failed = false;
9579
9640
  return {
9580
9641
  setProjectName(_name) {},
9581
9642
  setGitProvider(provider) {
9582
- log.info(TAG36, `Git provider: ${provider}`);
9643
+ log.info(TAG37, `Git provider: ${provider}`);
9583
9644
  },
9584
9645
  setHttpPort(port) {
9585
- log.info(TAG36, `HTTP server on port ${port}`);
9646
+ log.info(TAG37, `HTTP server on port ${port}`);
9586
9647
  },
9587
9648
  check(message) {
9588
- log.info(TAG36, message);
9649
+ log.info(TAG37, message);
9589
9650
  },
9590
9651
  warn(message) {
9591
- log.warn(TAG36, message);
9652
+ log.warn(TAG37, message);
9592
9653
  },
9593
9654
  fail() {
9594
9655
  failed = true;
@@ -9596,7 +9657,7 @@ function jsonBanner(config, version) {
9596
9657
  async ready(message) {
9597
9658
  if (failed)
9598
9659
  return;
9599
- log.info(TAG36, message);
9660
+ log.info(TAG37, message);
9600
9661
  }
9601
9662
  };
9602
9663
  }
@@ -9677,7 +9738,7 @@ function cyan(s) {
9677
9738
  function yellow(s) {
9678
9739
  return `${ANSI.yellow}${s}${ANSI.reset}`;
9679
9740
  }
9680
- var TAG36 = "daemon", RULE_WIDTH = 70, ANSI;
9741
+ var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
9681
9742
  var init_startup_banner = __esm(() => {
9682
9743
  init_log();
9683
9744
  ANSI = {
@@ -9828,13 +9889,13 @@ class Watcher {
9828
9889
  }
9829
9890
  async start() {
9830
9891
  if (!isPretty()) {
9831
- log.info(TAG37, "Connecting to Supabase realtime (broadcast)...");
9892
+ log.info(TAG38, "Connecting to Supabase realtime (broadcast)...");
9832
9893
  }
9833
9894
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
9834
9895
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
9835
9896
  this.subscribeBroadcast();
9836
9897
  presenceChannel.on("presence", { event: "sync" }, () => {
9837
- log.debug(TAG37, "Presence sync");
9898
+ log.debug(TAG38, "Presence sync");
9838
9899
  }).subscribe(async (status) => {
9839
9900
  if (status === "SUBSCRIBED") {
9840
9901
  await presenceChannel.track({
@@ -9847,7 +9908,7 @@ class Watcher {
9847
9908
  agentName: this.identity.agentName
9848
9909
  });
9849
9910
  if (!isPretty() || !this.suppressStartupLogs) {
9850
- log.info(TAG37, "Presence tracked on board-presence channel");
9911
+ log.info(TAG38, "Presence tracked on board-presence channel");
9851
9912
  }
9852
9913
  this.presenceTracked = true;
9853
9914
  this.maybeResolveReady();
@@ -9860,13 +9921,13 @@ class Watcher {
9860
9921
  return;
9861
9922
  const gen = ++this.broadcastGen;
9862
9923
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
9863
- log.debug(TAG37, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9924
+ log.debug(TAG38, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9864
9925
  this.onCardBroadcast({
9865
9926
  event: "card_update",
9866
9927
  payload: msg.payload ?? {}
9867
9928
  });
9868
9929
  }).on("broadcast", { event: "card_created" }, (msg) => {
9869
- log.debug(TAG37, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9930
+ log.debug(TAG38, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9870
9931
  this.onCardBroadcast({
9871
9932
  event: "card_created",
9872
9933
  payload: msg.payload ?? {}
@@ -9876,7 +9937,7 @@ class Watcher {
9876
9937
  const cardId = payload.card_id;
9877
9938
  const command = payload.command;
9878
9939
  if (cardId && command) {
9879
- log.info(TAG37, `Broadcast: agent_command ${command} for ${cardId}`);
9940
+ log.info(TAG38, `Broadcast: agent_command ${command} for ${cardId}`);
9880
9941
  this.onAgentCommand?.({ cardId, command });
9881
9942
  }
9882
9943
  }).subscribe((status) => {
@@ -9886,13 +9947,13 @@ class Watcher {
9886
9947
  this.connected = true;
9887
9948
  this.reconnectAttempts = 0;
9888
9949
  if (!isPretty() || !this.suppressStartupLogs) {
9889
- log.info(TAG37, "Broadcast subscription active");
9950
+ log.info(TAG38, "Broadcast subscription active");
9890
9951
  }
9891
9952
  this.maybeResolveReady();
9892
9953
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9893
9954
  this.connected = false;
9894
9955
  if (!this.stopping) {
9895
- log.warn(TAG37, `Broadcast subscription ${status} — scheduling reconnect`);
9956
+ log.warn(TAG38, `Broadcast subscription ${status} — scheduling reconnect`);
9896
9957
  this.scheduleReconnect();
9897
9958
  }
9898
9959
  }
@@ -9911,7 +9972,7 @@ class Watcher {
9911
9972
  async reconnectBroadcast() {
9912
9973
  if (this.stopping || !this.supabase)
9913
9974
  return;
9914
- log.warn(TAG37, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9975
+ log.warn(TAG38, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9915
9976
  if (this.channel) {
9916
9977
  const old = this.channel;
9917
9978
  this.channel = null;
@@ -9941,10 +10002,10 @@ class Watcher {
9941
10002
  this.supabase = null;
9942
10003
  }
9943
10004
  this.connected = false;
9944
- log.info(TAG37, "Broadcast subscription stopped");
10005
+ log.info(TAG38, "Broadcast subscription stopped");
9945
10006
  }
9946
10007
  }
9947
- var TAG37 = "watcher";
10008
+ var TAG38 = "watcher";
9948
10009
  var init_watcher = __esm(() => {
9949
10010
  init_log();
9950
10011
  });
@@ -10031,10 +10092,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
10031
10092
  });
10032
10093
  } catch {}
10033
10094
  if (result.removed.length > 0) {
10034
- log.info(TAG38, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
10095
+ log.info(TAG39, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
10035
10096
  }
10036
10097
  if (result.errors.length > 0) {
10037
- log.warn(TAG38, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
10098
+ log.warn(TAG39, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
10038
10099
  }
10039
10100
  return result;
10040
10101
  }
@@ -10064,7 +10125,7 @@ function pruneFailedRemoteBranches(opts) {
10064
10125
  } catch (err) {
10065
10126
  const detail = gitErrorDetail2(err);
10066
10127
  if (isTransientGitNetworkError(detail)) {
10067
- log.debug(TAG38, `Remote branch GC skipped — remote unreachable: ${detail}`);
10128
+ log.debug(TAG39, `Remote branch GC skipped — remote unreachable: ${detail}`);
10068
10129
  return result;
10069
10130
  }
10070
10131
  result.errors.push({ ref: "fetch", error: detail });
@@ -10103,7 +10164,7 @@ function pruneFailedRemoteBranches(opts) {
10103
10164
  continue;
10104
10165
  }
10105
10166
  if (clock() > sweepDeadline) {
10106
- log.debug(TAG38, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
10167
+ log.debug(TAG39, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
10107
10168
  break;
10108
10169
  }
10109
10170
  try {
@@ -10116,17 +10177,17 @@ function pruneFailedRemoteBranches(opts) {
10116
10177
  } catch (err) {
10117
10178
  const detail = gitErrorDetail2(err);
10118
10179
  if (isTransientGitNetworkError(detail)) {
10119
- log.debug(TAG38, `Remote branch GC interrupted — remote unreachable: ${detail}`);
10180
+ log.debug(TAG39, `Remote branch GC interrupted — remote unreachable: ${detail}`);
10120
10181
  break;
10121
10182
  }
10122
10183
  result.errors.push({ ref, error: detail });
10123
10184
  }
10124
10185
  }
10125
10186
  if (result.removed.length > 0) {
10126
- log.info(TAG38, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
10187
+ log.info(TAG39, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
10127
10188
  }
10128
10189
  if (result.errors.length > 0) {
10129
- log.warn(TAG38, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
10190
+ log.warn(TAG39, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
10130
10191
  }
10131
10192
  return result;
10132
10193
  }
@@ -10157,13 +10218,13 @@ class WorktreeGc {
10157
10218
  try {
10158
10219
  runWorktreeGc(this.basePath, this.store);
10159
10220
  } catch (err) {
10160
- log.warn(TAG38, `GC tick failed: ${err instanceof Error ? err.message : err}`);
10221
+ log.warn(TAG39, `GC tick failed: ${err instanceof Error ? err.message : err}`);
10161
10222
  }
10162
10223
  if (this.remoteOpts) {
10163
10224
  try {
10164
10225
  pruneFailedRemoteBranches(this.remoteOpts);
10165
10226
  } catch (err) {
10166
- log.warn(TAG38, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
10227
+ log.warn(TAG39, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
10167
10228
  }
10168
10229
  }
10169
10230
  }
@@ -10177,7 +10238,7 @@ function getRepoRoot2() {
10177
10238
  return null;
10178
10239
  }
10179
10240
  }
10180
- var TAG38 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
10241
+ var TAG39 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
10181
10242
  var init_worktree_gc = __esm(() => {
10182
10243
  init_log();
10183
10244
  init_worktree();
@@ -10281,7 +10342,7 @@ async function main() {
10281
10342
  } catch (err) {
10282
10343
  if (err instanceof ConfigValidationError) {
10283
10344
  banner.fail();
10284
- log.error(TAG39, err.message);
10345
+ log.error(TAG40, err.message);
10285
10346
  process.exit(1);
10286
10347
  }
10287
10348
  throw err;
@@ -10291,7 +10352,7 @@ async function main() {
10291
10352
  } catch (err) {
10292
10353
  if (err instanceof ConfigValidationError) {
10293
10354
  banner.fail();
10294
- log.error(TAG39, err.message);
10355
+ log.error(TAG40, err.message);
10295
10356
  process.exit(1);
10296
10357
  }
10297
10358
  throw err;
@@ -10401,7 +10462,7 @@ async function main() {
10401
10462
  if (shuttingDown)
10402
10463
  return;
10403
10464
  shuttingDown = true;
10404
- log.info(TAG39, `Received ${signal}, shutting down gracefully...`);
10465
+ log.info(TAG40, `Received ${signal}, shutting down gracefully...`);
10405
10466
  reconciler.stop();
10406
10467
  mergeMonitor?.stop();
10407
10468
  worktreeGc.stop();
@@ -10411,18 +10472,18 @@ async function main() {
10411
10472
  }
10412
10473
  await watcher.stop();
10413
10474
  await pool.shutdown();
10414
- log.info(TAG39, "Daemon stopped.");
10475
+ log.info(TAG40, "Daemon stopped.");
10415
10476
  process.exit(exitCode);
10416
10477
  };
10417
10478
  process.on("SIGINT", () => shutdown("SIGINT"));
10418
10479
  process.on("SIGTERM", () => shutdown("SIGTERM"));
10419
10480
  process.on("uncaughtException", (err) => {
10420
- log.error(TAG39, `Uncaught exception: ${err.message}`);
10481
+ log.error(TAG40, `Uncaught exception: ${err.message}`);
10421
10482
  exitCode = 1;
10422
10483
  shutdown("uncaughtException");
10423
10484
  });
10424
10485
  process.on("unhandledRejection", (reason) => {
10425
- log.error(TAG39, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
10486
+ log.error(TAG40, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
10426
10487
  exitCode = 1;
10427
10488
  shutdown("unhandledRejection");
10428
10489
  });
@@ -10475,29 +10536,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
10475
10536
  if (assignedAgentId === undefined)
10476
10537
  return;
10477
10538
  if (assignedAgentId === agentId) {
10478
- log.info(TAG39, `Broadcast: card ${cardId} assigned to agent`);
10539
+ log.info(TAG40, `Broadcast: card ${cardId} assigned to agent`);
10479
10540
  try {
10480
10541
  await pool.resetAttemptsForReassign(cardId);
10481
10542
  await tryEnqueueCard(cardId, client, pool, config, agentId);
10482
10543
  } catch (err) {
10483
- log.error(TAG39, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
10544
+ log.error(TAG40, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
10484
10545
  }
10485
10546
  } else if (pool.isCardKnown(cardId)) {
10486
- log.info(TAG39, `Broadcast: card ${cardId} unassigned from agent`);
10547
+ log.info(TAG40, `Broadcast: card ${cardId} unassigned from agent`);
10487
10548
  await pool.removeCard(cardId);
10488
10549
  }
10489
10550
  }
10490
10551
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
10491
10552
  const { card } = await client.getCard(cardId);
10492
10553
  if (card.assigned_agent_id !== agentId) {
10493
- log.debug(TAG39, `Card ${cardId} no longer assigned to agent — skipping`);
10554
+ log.debug(TAG40, `Card ${cardId} no longer assigned to agent — skipping`);
10494
10555
  return;
10495
10556
  }
10496
10557
  const board = await client.getBoard(config.projectId, { summary: true });
10497
10558
  const columns = board.columns;
10498
10559
  const column = columns.find((c) => c.id === card.column_id);
10499
10560
  if (!column) {
10500
- log.warn(TAG39, `Column not found for card ${cardId}`);
10561
+ log.warn(TAG40, `Column not found for card ${cardId}`);
10501
10562
  return;
10502
10563
  }
10503
10564
  const route = classifyPickup(card, column.name, {
@@ -10506,27 +10567,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
10506
10567
  playbooks: config.agent.playbooks
10507
10568
  });
10508
10569
  if (!route) {
10509
- log.info(TAG39, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
10570
+ log.info(TAG40, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
10510
10571
  return;
10511
10572
  }
10512
10573
  if (route.stage) {
10513
- log.info(TAG39, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
10574
+ log.info(TAG40, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
10514
10575
  }
10515
10576
  const mode = route.mode;
10516
10577
  const labelMap = buildLabelMap(board.labels ?? []);
10517
10578
  const cardLabels = resolveCardLabels(card, labelMap);
10518
10579
  const subtasks = card.subtasks ?? [];
10519
10580
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
10520
- log.debug(TAG39, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
10581
+ log.debug(TAG40, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
10521
10582
  return;
10522
10583
  }
10523
10584
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
10524
- log.info(TAG39, `Card #${card.short_id} has no branch reference — skipping auto-review`);
10585
+ log.info(TAG40, `Card #${card.short_id} has no branch reference — skipping auto-review`);
10525
10586
  return;
10526
10587
  }
10527
10588
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
10528
10589
  }
10529
- var TAG39 = "daemon", PKG_VERSION;
10590
+ var TAG40 = "daemon", PKG_VERSION;
10530
10591
  var init_src = __esm(() => {
10531
10592
  init_board_helpers();
10532
10593
  init_config();
package/dist/index.js CHANGED
@@ -104,6 +104,7 @@ var init_log = __esm(() => {
104
104
  var exports_board_helpers = {};
105
105
  __export(exports_board_helpers, {
106
106
  resolveCardLabels: () => resolveCardLabels,
107
+ releaseAssignedAgent: () => releaseAssignedAgent,
107
108
  moveCardToColumn: () => moveCardToColumn,
108
109
  moveCardAndAddLabel: () => moveCardAndAddLabel,
109
110
  hasLabel: () => hasLabel,
@@ -125,6 +126,9 @@ function resolveCardLabels(card, labelMap) {
125
126
  function hasLabel(cardLabels, labelName) {
126
127
  return cardLabels.some((l) => l.name.toLowerCase() === labelName.toLowerCase());
127
128
  }
129
+ async function releaseAssignedAgent(client, cardId) {
130
+ await client.updateCard(cardId, { assignedAgentId: null });
131
+ }
128
132
  async function moveCardToColumn(client, card, targetColumnName) {
129
133
  try {
130
134
  const board = await client.getBoard(card.project_id);
@@ -3861,6 +3865,11 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3861
3865
  }
3862
3866
  if (config.completion.moveToColumn) {
3863
3867
  await moveCardToColumn(client, card, config.completion.moveToColumn);
3868
+ try {
3869
+ await releaseAssignedAgent(client, card.id);
3870
+ } catch (err) {
3871
+ log.warn(TAG15, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3872
+ }
3864
3873
  if (onMovedToCompletion) {
3865
3874
  try {
3866
3875
  await onMovedToCompletion(card);
@@ -6189,6 +6198,7 @@ async function runTransition(client, card, plan, opts = {}) {
6189
6198
  const board = await withRetry("move", shortId, () => client.getBoard(card.project_id), attempts, backoffMs);
6190
6199
  const columns = board.columns;
6191
6200
  const labels = board.labels ?? [];
6201
+ let moveLanded = false;
6192
6202
  if (plan.move) {
6193
6203
  const target = columns.find((c) => c.name.toLowerCase() === plan.move.columnName.toLowerCase());
6194
6204
  if (!target) {
@@ -6201,8 +6211,14 @@ async function runTransition(client, card, plan, opts = {}) {
6201
6211
  await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
6202
6212
  log.info(TAG23, `#${shortId} → "${target.name}"`);
6203
6213
  card.column_id = target.id;
6214
+ moveLanded = true;
6215
+ } else {
6216
+ moveLanded = true;
6204
6217
  }
6205
6218
  }
6219
+ if (moveLanded && plan.onMoved) {
6220
+ await plan.onMoved();
6221
+ }
6206
6222
  if (plan.addLabels?.length) {
6207
6223
  const existing = new Set(card.labelIds ?? []);
6208
6224
  for (const { name, color } of plan.addLabels) {
@@ -6438,7 +6454,15 @@ class ReviewWorker {
6438
6454
  const cwd = this.worktreePath;
6439
6455
  if (!localMode) {
6440
6456
  log.info(this.tag, `Starting dev server on port ${port}...`);
6441
- this.devServerProcess = spawnInGroup("bun", ["run", "dev", "--", "--port", String(port)], { cwd, stdio: ["ignore", "pipe", "pipe"] });
6457
+ const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
6458
+ this.devServerProcess = spawnInGroup(devCmd, devArgs, {
6459
+ cwd,
6460
+ stdio: ["ignore", "pipe", "pipe"]
6461
+ });
6462
+ let devServerSpawnError = null;
6463
+ this.devServerProcess.once("error", (err) => {
6464
+ devServerSpawnError = err;
6465
+ });
6442
6466
  await this.client.updateAgentProgress(card.id, {
6443
6467
  agentIdentifier: agentIdentifier(this.id),
6444
6468
  agentName: `${AGENT_NAME} (Review)`,
@@ -6446,6 +6470,9 @@ class ReviewWorker {
6446
6470
  currentTask: `Starting dev server on port ${port}…`,
6447
6471
  progressPercent: 10
6448
6472
  });
6473
+ if (devServerSpawnError) {
6474
+ throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
6475
+ }
6449
6476
  await waitForDevServer(this.devServerProcess, 30000);
6450
6477
  await probeDevServer(port);
6451
6478
  log.info(this.tag, `Dev server ready on port ${port}`);
@@ -6700,7 +6727,7 @@ class ReviewWorker {
6700
6727
  [parse_error] ${msg}
6701
6728
  `);
6702
6729
  });
6703
- if (this.process.stdout) {
6730
+ if (this.process?.stdout) {
6704
6731
  parser.attach(this.process.stdout);
6705
6732
  if (runLog) {
6706
6733
  this.process.stdout.on("data", (chunk) => {
@@ -6709,14 +6736,14 @@ class ReviewWorker {
6709
6736
  }
6710
6737
  }
6711
6738
  let stderr = "";
6712
- this.process.stderr?.on("data", (data) => {
6739
+ this.process?.stderr?.on("data", (data) => {
6713
6740
  stderr += data.toString();
6714
6741
  runLog?.stream.write(`[stderr] ${data.toString()}`);
6715
6742
  });
6716
- this.process.on("error", (err) => {
6743
+ this.process?.on("error", (err) => {
6717
6744
  reject(new Error(`Failed to spawn claude: ${err.message}`));
6718
6745
  });
6719
- this.process.on("close", (code) => {
6746
+ this.process?.on("close", (code) => {
6720
6747
  this.process = null;
6721
6748
  const stdout = textChunks.join("");
6722
6749
  const stats = tracker.stats;
@@ -6846,6 +6873,7 @@ var init_review_worker = __esm(() => {
6846
6873
  init_gate_collectors();
6847
6874
  init_git_diff_stat();
6848
6875
  init_log();
6876
+ init_pm();
6849
6877
  init_process_group();
6850
6878
  init_progress_tracker();
6851
6879
  init_review_completion();
@@ -7501,16 +7529,16 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
7501
7529
  currentStage: next.stage.id,
7502
7530
  done: false
7503
7531
  });
7504
- deps.sink?.recordPlaybookAdvanced({
7505
- fromStageId: stage.id,
7506
- fromStageName: stage.name,
7507
- toStageId: next.stage.id,
7508
- toStageName: next.stage.name,
7509
- advancedBy: "system",
7510
- reason: summary
7511
- });
7512
7532
  await runTransition(deps.client, card, {
7513
7533
  move: { columnName: toColumn },
7534
+ onMoved: () => deps.sink?.recordPlaybookAdvanced({
7535
+ fromStageId: stage.id,
7536
+ fromStageName: stage.name,
7537
+ toStageId: next.stage.id,
7538
+ toStageName: next.stage.name,
7539
+ advancedBy: "system",
7540
+ reason: summary
7541
+ }),
7514
7542
  addLabels: [{ name: AGENT_LABEL }],
7515
7543
  ...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
7516
7544
  }, { store: deps.stateStore, runId: deps.runId });
@@ -9219,6 +9247,22 @@ var init_recovery = __esm(() => {
9219
9247
  init_worktree();
9220
9248
  });
9221
9249
 
9250
+ // src/claim.ts
9251
+ async function claimReviewCard(client, cardId, agentId) {
9252
+ try {
9253
+ const { claimed } = await client.claimCard(cardId, agentId);
9254
+ log.debug(TAG34, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
9255
+ return claimed;
9256
+ } catch (err) {
9257
+ log.error(TAG34, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
9258
+ return false;
9259
+ }
9260
+ }
9261
+ var TAG34 = "claim";
9262
+ var init_claim = __esm(() => {
9263
+ init_log();
9264
+ });
9265
+
9222
9266
  // src/strand-recovery.ts
9223
9267
  var exports_strand_recovery = {};
9224
9268
  __export(exports_strand_recovery, {
@@ -9265,19 +9309,27 @@ async function reclaimPreReviewStrands(opts) {
9265
9309
  const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
9266
9310
  if (prUrl)
9267
9311
  continue;
9268
- log.warn(TAG34, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
9269
- try {
9270
- await client.updateCard(card.id, { assignedAgentId: agentId });
9271
- reclaimed.push(card.id);
9272
- } catch (err) {
9273
- log.error(TAG34, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9312
+ const won = await claimReviewCard(client, card.id, agentId);
9313
+ if (!won) {
9314
+ log.debug(TAG35, `#${card.short_id} lost the review claim race, skipping`);
9315
+ continue;
9316
+ }
9317
+ log.warn(TAG35, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
9318
+ reclaimed.push(card.id);
9319
+ if (opts.onClaimed) {
9320
+ try {
9321
+ await opts.onClaimed(card);
9322
+ } catch (err) {
9323
+ log.error(TAG35, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
9324
+ }
9274
9325
  }
9275
9326
  }
9276
9327
  return reclaimed;
9277
9328
  }
9278
- var TAG34 = "strand-recovery";
9329
+ var TAG35 = "strand-recovery";
9279
9330
  var init_strand_recovery = __esm(() => {
9280
9331
  init_board_helpers();
9332
+ init_claim();
9281
9333
  init_git_pr();
9282
9334
  init_log();
9283
9335
  init_review_worktree();
@@ -9326,7 +9378,7 @@ class Reconciler {
9326
9378
  clearInterval(this.timer);
9327
9379
  this.timer = null;
9328
9380
  }
9329
- log.info(TAG35, "Heartbeat stopped");
9381
+ log.info(TAG36, "Heartbeat stopped");
9330
9382
  }
9331
9383
  async recoverStaleRuns() {
9332
9384
  if (!this.stateStore || !this.agentConfig)
@@ -9343,7 +9395,7 @@ class Reconciler {
9343
9395
  if (!daemonDead && !(heartbeatStale && ourZombie))
9344
9396
  continue;
9345
9397
  const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
9346
- log.warn(TAG35, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9398
+ log.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9347
9399
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
9348
9400
  runId: run.runId,
9349
9401
  cardId: run.cardId,
@@ -9370,11 +9422,11 @@ class Reconciler {
9370
9422
  const stalledAt = Date.parse(card.updated_at ?? "");
9371
9423
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
9372
9424
  continue;
9373
- log.warn(TAG35, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9425
+ log.warn(TAG36, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9374
9426
  try {
9375
9427
  await this.client.moveCard(card.id, pickupCol.id);
9376
9428
  } catch (err) {
9377
- log.error(TAG35, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9429
+ log.error(TAG36, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9378
9430
  }
9379
9431
  }
9380
9432
  }
@@ -9396,10 +9448,19 @@ class Reconciler {
9396
9448
  labelMap,
9397
9449
  reviewColumns: this.reviewColumns,
9398
9450
  approvedLabel: this.approvedLabel,
9399
- graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
9451
+ graceMs: 0,
9400
9452
  knownCardIds,
9401
9453
  cwd: process.cwd(),
9402
- provider: this.gitProvider
9454
+ provider: this.gitProvider,
9455
+ onClaimed: async (card) => {
9456
+ const column = columns.find((c) => c.id === card.column_id);
9457
+ if (!column)
9458
+ return;
9459
+ const cardLabels = resolveCardLabels(card, labelMap);
9460
+ const subtasks = card.subtasks ?? [];
9461
+ log.info(TAG36, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
9462
+ await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
9463
+ }
9403
9464
  });
9404
9465
  }
9405
9466
  async releaseStalledApprovals(cards, columns, knownCardIds) {
@@ -9421,11 +9482,11 @@ class Reconciler {
9421
9482
  const parkedAt = Date.parse(card.updated_at ?? "");
9422
9483
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
9423
9484
  continue;
9424
- log.warn(TAG35, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
9485
+ log.warn(TAG36, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
9425
9486
  try {
9426
9487
  await this.client.moveCard(card.id, pickupCol.id);
9427
9488
  } catch (err) {
9428
- log.error(TAG35, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9489
+ log.error(TAG36, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9429
9490
  }
9430
9491
  }
9431
9492
  }
@@ -9468,21 +9529,21 @@ class Reconciler {
9468
9529
  const subtasks = card.subtasks ?? [];
9469
9530
  const mode = route.mode;
9470
9531
  if (route.stage) {
9471
- log.info(TAG35, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
9532
+ log.info(TAG36, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
9472
9533
  }
9473
9534
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
9474
- log.debug(TAG35, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
9535
+ log.debug(TAG36, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
9475
9536
  continue;
9476
9537
  }
9477
9538
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
9478
- log.debug(TAG35, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
9539
+ log.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
9479
9540
  continue;
9480
9541
  }
9481
9542
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
9482
- log.debug(TAG35, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
9543
+ log.debug(TAG36, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
9483
9544
  continue;
9484
9545
  }
9485
- log.info(TAG35, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
9546
+ log.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
9486
9547
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
9487
9548
  }
9488
9549
  }
@@ -9493,18 +9554,18 @@ class Reconciler {
9493
9554
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
9494
9555
  for (const knownId of knownCardIds) {
9495
9556
  if (!allAgentCardIds.has(knownId)) {
9496
- log.info(TAG35, `Missed unassign: ${knownId} — removing`);
9557
+ log.info(TAG36, `Missed unassign: ${knownId} — removing`);
9497
9558
  await this.pool.removeCard(knownId);
9498
9559
  }
9499
9560
  }
9500
9561
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
9501
- log.debug(TAG35, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9562
+ log.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9502
9563
  } catch (err) {
9503
- log.error(TAG35, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9564
+ log.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9504
9565
  }
9505
9566
  }
9506
9567
  }
9507
- var TAG35 = "reconcile";
9568
+ var TAG36 = "reconcile";
9508
9569
  var init_reconcile = __esm(() => {
9509
9570
  init_board_helpers();
9510
9571
  init_git_pr();
@@ -9544,7 +9605,7 @@ function prettyBanner(config, version) {
9544
9605
  checks.push({ kind: "ok", message });
9545
9606
  },
9546
9607
  warn(message) {
9547
- log.warn(TAG36, message);
9608
+ log.warn(TAG37, message);
9548
9609
  checks.push({ kind: "warn", message: message.split(`
9549
9610
  `, 1)[0] });
9550
9611
  },
@@ -9569,25 +9630,25 @@ function prettyBanner(config, version) {
9569
9630
  };
9570
9631
  }
9571
9632
  function jsonBanner(config, version) {
9572
- log.info(TAG36, `Harmony Agent Daemon v${version} starting...`);
9573
- log.info(TAG36, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
9633
+ log.info(TAG37, `Harmony Agent Daemon v${version} starting...`);
9634
+ log.info(TAG37, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
9574
9635
  if (config.agent.review.enabled) {
9575
- log.info(TAG36, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
9636
+ log.info(TAG37, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
9576
9637
  }
9577
9638
  let failed = false;
9578
9639
  return {
9579
9640
  setProjectName(_name) {},
9580
9641
  setGitProvider(provider) {
9581
- log.info(TAG36, `Git provider: ${provider}`);
9642
+ log.info(TAG37, `Git provider: ${provider}`);
9582
9643
  },
9583
9644
  setHttpPort(port) {
9584
- log.info(TAG36, `HTTP server on port ${port}`);
9645
+ log.info(TAG37, `HTTP server on port ${port}`);
9585
9646
  },
9586
9647
  check(message) {
9587
- log.info(TAG36, message);
9648
+ log.info(TAG37, message);
9588
9649
  },
9589
9650
  warn(message) {
9590
- log.warn(TAG36, message);
9651
+ log.warn(TAG37, message);
9591
9652
  },
9592
9653
  fail() {
9593
9654
  failed = true;
@@ -9595,7 +9656,7 @@ function jsonBanner(config, version) {
9595
9656
  async ready(message) {
9596
9657
  if (failed)
9597
9658
  return;
9598
- log.info(TAG36, message);
9659
+ log.info(TAG37, message);
9599
9660
  }
9600
9661
  };
9601
9662
  }
@@ -9676,7 +9737,7 @@ function cyan(s) {
9676
9737
  function yellow(s) {
9677
9738
  return `${ANSI.yellow}${s}${ANSI.reset}`;
9678
9739
  }
9679
- var TAG36 = "daemon", RULE_WIDTH = 70, ANSI;
9740
+ var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
9680
9741
  var init_startup_banner = __esm(() => {
9681
9742
  init_log();
9682
9743
  ANSI = {
@@ -9827,13 +9888,13 @@ class Watcher {
9827
9888
  }
9828
9889
  async start() {
9829
9890
  if (!isPretty()) {
9830
- log.info(TAG37, "Connecting to Supabase realtime (broadcast)...");
9891
+ log.info(TAG38, "Connecting to Supabase realtime (broadcast)...");
9831
9892
  }
9832
9893
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
9833
9894
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
9834
9895
  this.subscribeBroadcast();
9835
9896
  presenceChannel.on("presence", { event: "sync" }, () => {
9836
- log.debug(TAG37, "Presence sync");
9897
+ log.debug(TAG38, "Presence sync");
9837
9898
  }).subscribe(async (status) => {
9838
9899
  if (status === "SUBSCRIBED") {
9839
9900
  await presenceChannel.track({
@@ -9846,7 +9907,7 @@ class Watcher {
9846
9907
  agentName: this.identity.agentName
9847
9908
  });
9848
9909
  if (!isPretty() || !this.suppressStartupLogs) {
9849
- log.info(TAG37, "Presence tracked on board-presence channel");
9910
+ log.info(TAG38, "Presence tracked on board-presence channel");
9850
9911
  }
9851
9912
  this.presenceTracked = true;
9852
9913
  this.maybeResolveReady();
@@ -9859,13 +9920,13 @@ class Watcher {
9859
9920
  return;
9860
9921
  const gen = ++this.broadcastGen;
9861
9922
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
9862
- log.debug(TAG37, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9923
+ log.debug(TAG38, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9863
9924
  this.onCardBroadcast({
9864
9925
  event: "card_update",
9865
9926
  payload: msg.payload ?? {}
9866
9927
  });
9867
9928
  }).on("broadcast", { event: "card_created" }, (msg) => {
9868
- log.debug(TAG37, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9929
+ log.debug(TAG38, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9869
9930
  this.onCardBroadcast({
9870
9931
  event: "card_created",
9871
9932
  payload: msg.payload ?? {}
@@ -9875,7 +9936,7 @@ class Watcher {
9875
9936
  const cardId = payload.card_id;
9876
9937
  const command = payload.command;
9877
9938
  if (cardId && command) {
9878
- log.info(TAG37, `Broadcast: agent_command ${command} for ${cardId}`);
9939
+ log.info(TAG38, `Broadcast: agent_command ${command} for ${cardId}`);
9879
9940
  this.onAgentCommand?.({ cardId, command });
9880
9941
  }
9881
9942
  }).subscribe((status) => {
@@ -9885,13 +9946,13 @@ class Watcher {
9885
9946
  this.connected = true;
9886
9947
  this.reconnectAttempts = 0;
9887
9948
  if (!isPretty() || !this.suppressStartupLogs) {
9888
- log.info(TAG37, "Broadcast subscription active");
9949
+ log.info(TAG38, "Broadcast subscription active");
9889
9950
  }
9890
9951
  this.maybeResolveReady();
9891
9952
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9892
9953
  this.connected = false;
9893
9954
  if (!this.stopping) {
9894
- log.warn(TAG37, `Broadcast subscription ${status} — scheduling reconnect`);
9955
+ log.warn(TAG38, `Broadcast subscription ${status} — scheduling reconnect`);
9895
9956
  this.scheduleReconnect();
9896
9957
  }
9897
9958
  }
@@ -9910,7 +9971,7 @@ class Watcher {
9910
9971
  async reconnectBroadcast() {
9911
9972
  if (this.stopping || !this.supabase)
9912
9973
  return;
9913
- log.warn(TAG37, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9974
+ log.warn(TAG38, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9914
9975
  if (this.channel) {
9915
9976
  const old = this.channel;
9916
9977
  this.channel = null;
@@ -9940,10 +10001,10 @@ class Watcher {
9940
10001
  this.supabase = null;
9941
10002
  }
9942
10003
  this.connected = false;
9943
- log.info(TAG37, "Broadcast subscription stopped");
10004
+ log.info(TAG38, "Broadcast subscription stopped");
9944
10005
  }
9945
10006
  }
9946
- var TAG37 = "watcher";
10007
+ var TAG38 = "watcher";
9947
10008
  var init_watcher = __esm(() => {
9948
10009
  init_log();
9949
10010
  });
@@ -10030,10 +10091,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
10030
10091
  });
10031
10092
  } catch {}
10032
10093
  if (result.removed.length > 0) {
10033
- log.info(TAG38, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
10094
+ log.info(TAG39, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
10034
10095
  }
10035
10096
  if (result.errors.length > 0) {
10036
- log.warn(TAG38, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
10097
+ log.warn(TAG39, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
10037
10098
  }
10038
10099
  return result;
10039
10100
  }
@@ -10063,7 +10124,7 @@ function pruneFailedRemoteBranches(opts) {
10063
10124
  } catch (err) {
10064
10125
  const detail = gitErrorDetail2(err);
10065
10126
  if (isTransientGitNetworkError(detail)) {
10066
- log.debug(TAG38, `Remote branch GC skipped — remote unreachable: ${detail}`);
10127
+ log.debug(TAG39, `Remote branch GC skipped — remote unreachable: ${detail}`);
10067
10128
  return result;
10068
10129
  }
10069
10130
  result.errors.push({ ref: "fetch", error: detail });
@@ -10102,7 +10163,7 @@ function pruneFailedRemoteBranches(opts) {
10102
10163
  continue;
10103
10164
  }
10104
10165
  if (clock() > sweepDeadline) {
10105
- log.debug(TAG38, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
10166
+ log.debug(TAG39, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
10106
10167
  break;
10107
10168
  }
10108
10169
  try {
@@ -10115,17 +10176,17 @@ function pruneFailedRemoteBranches(opts) {
10115
10176
  } catch (err) {
10116
10177
  const detail = gitErrorDetail2(err);
10117
10178
  if (isTransientGitNetworkError(detail)) {
10118
- log.debug(TAG38, `Remote branch GC interrupted — remote unreachable: ${detail}`);
10179
+ log.debug(TAG39, `Remote branch GC interrupted — remote unreachable: ${detail}`);
10119
10180
  break;
10120
10181
  }
10121
10182
  result.errors.push({ ref, error: detail });
10122
10183
  }
10123
10184
  }
10124
10185
  if (result.removed.length > 0) {
10125
- log.info(TAG38, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
10186
+ log.info(TAG39, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
10126
10187
  }
10127
10188
  if (result.errors.length > 0) {
10128
- log.warn(TAG38, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
10189
+ log.warn(TAG39, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
10129
10190
  }
10130
10191
  return result;
10131
10192
  }
@@ -10156,13 +10217,13 @@ class WorktreeGc {
10156
10217
  try {
10157
10218
  runWorktreeGc(this.basePath, this.store);
10158
10219
  } catch (err) {
10159
- log.warn(TAG38, `GC tick failed: ${err instanceof Error ? err.message : err}`);
10220
+ log.warn(TAG39, `GC tick failed: ${err instanceof Error ? err.message : err}`);
10160
10221
  }
10161
10222
  if (this.remoteOpts) {
10162
10223
  try {
10163
10224
  pruneFailedRemoteBranches(this.remoteOpts);
10164
10225
  } catch (err) {
10165
- log.warn(TAG38, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
10226
+ log.warn(TAG39, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
10166
10227
  }
10167
10228
  }
10168
10229
  }
@@ -10176,7 +10237,7 @@ function getRepoRoot2() {
10176
10237
  return null;
10177
10238
  }
10178
10239
  }
10179
- var TAG38 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
10240
+ var TAG39 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
10180
10241
  var init_worktree_gc = __esm(() => {
10181
10242
  init_log();
10182
10243
  init_worktree();
@@ -10280,7 +10341,7 @@ async function main() {
10280
10341
  } catch (err) {
10281
10342
  if (err instanceof ConfigValidationError) {
10282
10343
  banner.fail();
10283
- log.error(TAG39, err.message);
10344
+ log.error(TAG40, err.message);
10284
10345
  process.exit(1);
10285
10346
  }
10286
10347
  throw err;
@@ -10290,7 +10351,7 @@ async function main() {
10290
10351
  } catch (err) {
10291
10352
  if (err instanceof ConfigValidationError) {
10292
10353
  banner.fail();
10293
- log.error(TAG39, err.message);
10354
+ log.error(TAG40, err.message);
10294
10355
  process.exit(1);
10295
10356
  }
10296
10357
  throw err;
@@ -10400,7 +10461,7 @@ async function main() {
10400
10461
  if (shuttingDown)
10401
10462
  return;
10402
10463
  shuttingDown = true;
10403
- log.info(TAG39, `Received ${signal}, shutting down gracefully...`);
10464
+ log.info(TAG40, `Received ${signal}, shutting down gracefully...`);
10404
10465
  reconciler.stop();
10405
10466
  mergeMonitor?.stop();
10406
10467
  worktreeGc.stop();
@@ -10410,18 +10471,18 @@ async function main() {
10410
10471
  }
10411
10472
  await watcher.stop();
10412
10473
  await pool.shutdown();
10413
- log.info(TAG39, "Daemon stopped.");
10474
+ log.info(TAG40, "Daemon stopped.");
10414
10475
  process.exit(exitCode);
10415
10476
  };
10416
10477
  process.on("SIGINT", () => shutdown("SIGINT"));
10417
10478
  process.on("SIGTERM", () => shutdown("SIGTERM"));
10418
10479
  process.on("uncaughtException", (err) => {
10419
- log.error(TAG39, `Uncaught exception: ${err.message}`);
10480
+ log.error(TAG40, `Uncaught exception: ${err.message}`);
10420
10481
  exitCode = 1;
10421
10482
  shutdown("uncaughtException");
10422
10483
  });
10423
10484
  process.on("unhandledRejection", (reason) => {
10424
- log.error(TAG39, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
10485
+ log.error(TAG40, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
10425
10486
  exitCode = 1;
10426
10487
  shutdown("unhandledRejection");
10427
10488
  });
@@ -10474,29 +10535,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
10474
10535
  if (assignedAgentId === undefined)
10475
10536
  return;
10476
10537
  if (assignedAgentId === agentId) {
10477
- log.info(TAG39, `Broadcast: card ${cardId} assigned to agent`);
10538
+ log.info(TAG40, `Broadcast: card ${cardId} assigned to agent`);
10478
10539
  try {
10479
10540
  await pool.resetAttemptsForReassign(cardId);
10480
10541
  await tryEnqueueCard(cardId, client, pool, config, agentId);
10481
10542
  } catch (err) {
10482
- log.error(TAG39, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
10543
+ log.error(TAG40, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
10483
10544
  }
10484
10545
  } else if (pool.isCardKnown(cardId)) {
10485
- log.info(TAG39, `Broadcast: card ${cardId} unassigned from agent`);
10546
+ log.info(TAG40, `Broadcast: card ${cardId} unassigned from agent`);
10486
10547
  await pool.removeCard(cardId);
10487
10548
  }
10488
10549
  }
10489
10550
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
10490
10551
  const { card } = await client.getCard(cardId);
10491
10552
  if (card.assigned_agent_id !== agentId) {
10492
- log.debug(TAG39, `Card ${cardId} no longer assigned to agent — skipping`);
10553
+ log.debug(TAG40, `Card ${cardId} no longer assigned to agent — skipping`);
10493
10554
  return;
10494
10555
  }
10495
10556
  const board = await client.getBoard(config.projectId, { summary: true });
10496
10557
  const columns = board.columns;
10497
10558
  const column = columns.find((c) => c.id === card.column_id);
10498
10559
  if (!column) {
10499
- log.warn(TAG39, `Column not found for card ${cardId}`);
10560
+ log.warn(TAG40, `Column not found for card ${cardId}`);
10500
10561
  return;
10501
10562
  }
10502
10563
  const route = classifyPickup(card, column.name, {
@@ -10505,27 +10566,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
10505
10566
  playbooks: config.agent.playbooks
10506
10567
  });
10507
10568
  if (!route) {
10508
- log.info(TAG39, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
10569
+ log.info(TAG40, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
10509
10570
  return;
10510
10571
  }
10511
10572
  if (route.stage) {
10512
- log.info(TAG39, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
10573
+ log.info(TAG40, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
10513
10574
  }
10514
10575
  const mode = route.mode;
10515
10576
  const labelMap = buildLabelMap(board.labels ?? []);
10516
10577
  const cardLabels = resolveCardLabels(card, labelMap);
10517
10578
  const subtasks = card.subtasks ?? [];
10518
10579
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
10519
- log.debug(TAG39, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
10580
+ log.debug(TAG40, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
10520
10581
  return;
10521
10582
  }
10522
10583
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
10523
- log.info(TAG39, `Card #${card.short_id} has no branch reference — skipping auto-review`);
10584
+ log.info(TAG40, `Card #${card.short_id} has no branch reference — skipping auto-review`);
10524
10585
  return;
10525
10586
  }
10526
10587
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
10527
10588
  }
10528
- var TAG39 = "daemon", PKG_VERSION;
10589
+ var TAG40 = "daemon", PKG_VERSION;
10529
10590
  var init_src = __esm(() => {
10530
10591
  init_board_helpers();
10531
10592
  init_config();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",