@gethmy/agent 1.7.3 → 1.8.1

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 +292 -88
  2. package/dist/index.js +292 -88
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1162,6 +1162,7 @@ class MergeMonitor {
1162
1162
  running = false;
1163
1163
  provider;
1164
1164
  cwd;
1165
+ onCardCompleted = null;
1165
1166
  constructor(client, projectId, config, intervalMs = 60000) {
1166
1167
  this.client = client;
1167
1168
  this.projectId = projectId;
@@ -1282,6 +1283,13 @@ class MergeMonitor {
1282
1283
  log.info(TAG7, `Deleted local branch ${branchName}`);
1283
1284
  } catch {}
1284
1285
  }
1286
+ if (this.onCardCompleted) {
1287
+ try {
1288
+ await this.onCardCompleted(card);
1289
+ } catch (err) {
1290
+ log.warn(TAG7, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
1291
+ }
1292
+ }
1285
1293
  log.info(TAG7, `#${card.short_id} completed (merged)`);
1286
1294
  }
1287
1295
  }
@@ -2028,7 +2036,7 @@ function buildTokenPayload(stats) {
2028
2036
  modelName: stats.cost.modelName
2029
2037
  };
2030
2038
  }
2031
- async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore) {
2039
+ async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion) {
2032
2040
  let verificationResult = {
2033
2041
  passed: true,
2034
2042
  buildErrors: [],
@@ -2137,6 +2145,13 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2137
2145
  }
2138
2146
  if (config.completion.moveToColumn) {
2139
2147
  await moveCardToColumn(client, card, config.completion.moveToColumn);
2148
+ if (onMovedToCompletion) {
2149
+ try {
2150
+ await onMovedToCompletion(card);
2151
+ } catch (err) {
2152
+ log.warn(TAG12, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2153
+ }
2154
+ }
2140
2155
  }
2141
2156
  if (config.completion.postSummary) {
2142
2157
  await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
@@ -3739,6 +3754,7 @@ class ReviewWorker {
3739
3754
  progressTracker = null;
3740
3755
  lastSessionStats = null;
3741
3756
  aborted = false;
3757
+ timedOut = false;
3742
3758
  runId = null;
3743
3759
  lastRunLogPath = null;
3744
3760
  sessionId = null;
@@ -3794,6 +3810,7 @@ class ReviewWorker {
3794
3810
  }
3795
3811
  async run(card, column, labels, subtasks) {
3796
3812
  this.aborted = false;
3813
+ this.timedOut = false;
3797
3814
  this.cardId = card.id;
3798
3815
  this.startedAt = Date.now();
3799
3816
  this.runId = newRunId();
@@ -3931,6 +3948,7 @@ class ReviewWorker {
3931
3948
  });
3932
3949
  this.timeoutTimer = setTimeout(() => {
3933
3950
  log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
3951
+ this.timedOut = true;
3934
3952
  this.cancel();
3935
3953
  }, this.config.review.maxTimeout);
3936
3954
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
@@ -3997,7 +4015,7 @@ class ReviewWorker {
3997
4015
  try {
3998
4016
  const run = this.stateStore.getRun(this.runId);
3999
4017
  if (run && run.endedAt === null) {
4000
- const status = this.state === "error" || this.aborted ? "paused" : "completed";
4018
+ const status = this.timedOut ? "failed" : this.state === "error" || this.aborted ? "paused" : "completed";
4001
4019
  await this.stateStore.endRun(this.runId, status);
4002
4020
  }
4003
4021
  } catch {}
@@ -4035,6 +4053,7 @@ class ReviewWorker {
4035
4053
  signalGroup(this.process, "SIGCONT");
4036
4054
  this.timeoutTimer = setTimeout(() => {
4037
4055
  log.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
4056
+ this.timedOut = true;
4038
4057
  this.cancel();
4039
4058
  }, this.config.review.maxTimeout);
4040
4059
  if (this.cardId) {
@@ -4070,7 +4089,11 @@ class ReviewWorker {
4070
4089
  if (this.cardId) {
4071
4090
  try {
4072
4091
  await this.client.endAgentSession(this.cardId, {
4073
- status: "paused",
4092
+ status: this.timedOut ? "failed" : "paused",
4093
+ ...this.timedOut ? {
4094
+ failureReason: "timeout",
4095
+ failureSummary: `Review exceeded the ${Math.round(this.config.review.maxTimeout / 60000)} min timeout`
4096
+ } : {},
4074
4097
  ...buildTokenPayload(snapshotStats)
4075
4098
  });
4076
4099
  } catch {}
@@ -4248,9 +4271,127 @@ var init_review_worker = __esm(() => {
4248
4271
  init_worktree();
4249
4272
  });
4250
4273
 
4274
+ // src/sleep-guard.ts
4275
+ import { spawn as spawn3 } from "node:child_process";
4276
+
4277
+ class SleepGuard {
4278
+ platform;
4279
+ child = null;
4280
+ holds = 0;
4281
+ constructor(platform = process.platform) {
4282
+ this.platform = platform;
4283
+ }
4284
+ get active() {
4285
+ return this.child !== null;
4286
+ }
4287
+ acquire() {
4288
+ this.holds++;
4289
+ if (this.holds === 1)
4290
+ this.start();
4291
+ }
4292
+ release() {
4293
+ this.holds = Math.max(0, this.holds - 1);
4294
+ if (this.holds === 0)
4295
+ this.stop();
4296
+ }
4297
+ stop() {
4298
+ this.holds = 0;
4299
+ if (this.child) {
4300
+ if (!this.child.killed)
4301
+ this.child.kill("SIGTERM");
4302
+ this.child = null;
4303
+ log.info(TAG20, "sleep assertion released");
4304
+ }
4305
+ }
4306
+ start() {
4307
+ if (this.platform !== "darwin" || this.child)
4308
+ return;
4309
+ try {
4310
+ const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
4311
+ stdio: "ignore"
4312
+ });
4313
+ child.on("error", (err) => {
4314
+ log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
4315
+ if (this.child === child)
4316
+ this.child = null;
4317
+ });
4318
+ child.on("exit", () => {
4319
+ if (this.child === child)
4320
+ this.child = null;
4321
+ });
4322
+ child.unref();
4323
+ this.child = child;
4324
+ log.info(TAG20, "sleep assertion acquired (caffeinate -i)");
4325
+ } catch (err) {
4326
+ log.warn(TAG20, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
4327
+ }
4328
+ }
4329
+ }
4330
+ var TAG20 = "sleep-guard";
4331
+ var init_sleep_guard = __esm(() => {
4332
+ init_log();
4333
+ });
4334
+
4335
+ // src/unblock.ts
4336
+ async function fetchBlocksLinks(client, cardId) {
4337
+ try {
4338
+ const { links } = await client.getCardLinks(cardId);
4339
+ return links.filter((l) => l.link_type === "blocks");
4340
+ } catch (err) {
4341
+ log.warn(TAG21, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
4342
+ return null;
4343
+ }
4344
+ }
4345
+ function isBlockerResolved(blocker, columns) {
4346
+ if (blocker.done)
4347
+ return true;
4348
+ const blockerColumn = columns.find((c) => c.id === blocker.column_id);
4349
+ return blockerColumn?.mark_cards_done === true;
4350
+ }
4351
+ async function getUnresolvedBlockers(client, card, projectId) {
4352
+ const links = await fetchBlocksLinks(client, card.id);
4353
+ if (!links)
4354
+ return null;
4355
+ const incoming = links.filter((l) => l.direction === "incoming");
4356
+ if (incoming.length === 0)
4357
+ return [];
4358
+ const board = await client.getBoard(projectId, { summary: true });
4359
+ const columns = board.columns ?? [];
4360
+ return incoming.filter((l) => !isBlockerResolved(l.target_card, columns)).map((l) => ({
4361
+ cardId: l.target_card.id,
4362
+ shortId: l.target_card.short_id,
4363
+ title: l.target_card.title
4364
+ }));
4365
+ }
4366
+ async function promoteUnblockedSuccessors(completedCard, deps) {
4367
+ const links = await fetchBlocksLinks(deps.client, completedCard.id);
4368
+ if (!links)
4369
+ return;
4370
+ const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
4371
+ if (successors.length === 0)
4372
+ return;
4373
+ log.info(TAG21, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
4374
+ for (const link of successors) {
4375
+ const successorId = link.target_card.id;
4376
+ try {
4377
+ const { card } = await deps.client.getCard(successorId);
4378
+ if (card.assignee_id !== deps.agentUserId) {
4379
+ log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4380
+ continue;
4381
+ }
4382
+ await deps.enqueue(successorId);
4383
+ } catch (err) {
4384
+ log.warn(TAG21, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
4385
+ }
4386
+ }
4387
+ }
4388
+ var TAG21 = "unblock";
4389
+ var init_unblock = __esm(() => {
4390
+ init_log();
4391
+ });
4392
+
4251
4393
  // ../harmony-shared/dist/cardLinks.js
4252
4394
  var init_cardLinks = () => {};
4253
-
4254
4395
  // ../harmony-shared/dist/commentSerializer.js
4255
4396
  function sanitizeHeaderField(value) {
4256
4397
  return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
@@ -4388,11 +4529,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
4388
4529
  Do NOT push to main. All your work stays on \`${branchName}\`.
4389
4530
  When finished, call harmony_end_agent_session with status="completed".`
4390
4531
  });
4391
- log.info(TAG20, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4532
+ log.info(TAG22, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4392
4533
  return result.prompt + pastEpisodesSection;
4393
4534
  } catch (err) {
4394
4535
  const msg = err instanceof Error ? err.message : String(err);
4395
- log.warn(TAG20, `Failed to generate prompt via API, using fallback: ${msg}`);
4536
+ log.warn(TAG22, `Failed to generate prompt via API, using fallback: ${msg}`);
4396
4537
  const commentsSection = await renderCommentsSection(client, card.id);
4397
4538
  return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
4398
4539
  }
@@ -4410,7 +4551,7 @@ async function renderCommentsSection(client, cardId) {
4410
4551
 
4411
4552
  ${section}` : "";
4412
4553
  } catch (err) {
4413
- log.warn(TAG20, "comment-thread fetch failed", {
4554
+ log.warn(TAG22, "comment-thread fetch failed", {
4414
4555
  event: "comment_fetch_failed",
4415
4556
  error: err instanceof Error ? err.message : String(err)
4416
4557
  });
@@ -4460,7 +4601,7 @@ ${description}`.trim();
4460
4601
  ## Similar past tasks
4461
4602
  ${bullets}`;
4462
4603
  } catch (err) {
4463
- log.warn(TAG20, "past-episodes recall failed", {
4604
+ log.warn(TAG22, "past-episodes recall failed", {
4464
4605
  event: "episode_recall_failed",
4465
4606
  error: err instanceof Error ? err.message : String(err)
4466
4607
  });
@@ -4501,7 +4642,7 @@ ${subtaskStr}
4501
4642
  You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
4502
4643
  Do NOT push to main. All your work stays on \`${branchName}\`.`;
4503
4644
  }
4504
- var TAG20 = "prompt";
4645
+ var TAG22 = "prompt";
4505
4646
  var init_prompt = __esm(() => {
4506
4647
  init_dist();
4507
4648
  init_log();
@@ -4515,6 +4656,7 @@ class Worker {
4515
4656
  workspaceId;
4516
4657
  projectId;
4517
4658
  stateStore;
4659
+ onCardCompleted;
4518
4660
  id;
4519
4661
  state = "idle";
4520
4662
  cardId = null;
@@ -4527,16 +4669,18 @@ class Worker {
4527
4669
  progressTracker = null;
4528
4670
  lastSessionStats;
4529
4671
  aborted = false;
4672
+ timedOut = false;
4530
4673
  verificationFailed = false;
4531
4674
  sessionId = null;
4532
4675
  runId = null;
4533
- constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore) {
4676
+ constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4534
4677
  this.config = config;
4535
4678
  this.client = client;
4536
4679
  this.onDone = onDone;
4537
4680
  this.workspaceId = workspaceId;
4538
4681
  this.projectId = projectId;
4539
4682
  this.stateStore = stateStore;
4683
+ this.onCardCompleted = onCardCompleted;
4540
4684
  this.id = id;
4541
4685
  }
4542
4686
  startHeartbeat() {
@@ -4571,7 +4715,7 @@ class Worker {
4571
4715
  }
4572
4716
  }
4573
4717
  get tag() {
4574
- return `${TAG21}:${this.id}`;
4718
+ return `${TAG23}:${this.id}`;
4575
4719
  }
4576
4720
  get isIdle() {
4577
4721
  return this.state === "idle";
@@ -4581,6 +4725,7 @@ class Worker {
4581
4725
  }
4582
4726
  async run(card, column, labels, subtasks) {
4583
4727
  this.aborted = false;
4728
+ this.timedOut = false;
4584
4729
  this.verificationFailed = false;
4585
4730
  this.cardId = card.id;
4586
4731
  this.startedAt = Date.now();
@@ -4617,7 +4762,7 @@ class Worker {
4617
4762
  });
4618
4763
  const sid = session && typeof session === "object" && "id" in session ? session.id : null;
4619
4764
  if (!sid) {
4620
- log.warn(TAG21, "startAgentSession returned no session id");
4765
+ log.warn(TAG23, "startAgentSession returned no session id");
4621
4766
  }
4622
4767
  this.sessionId = sid;
4623
4768
  await this.recordPhase("preparing");
@@ -4649,6 +4794,7 @@ class Worker {
4649
4794
  });
4650
4795
  this.timeoutTimer = setTimeout(() => {
4651
4796
  log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
4797
+ this.timedOut = true;
4652
4798
  this.cancel();
4653
4799
  }, this.config.maxTimeout);
4654
4800
  await this.spawnClaude(prompt, card, subtasks);
@@ -4666,7 +4812,7 @@ class Worker {
4666
4812
  });
4667
4813
  this.state = "completing";
4668
4814
  await this.recordPhase("completing");
4669
- const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore);
4815
+ const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted);
4670
4816
  this.verificationFailed = !completed;
4671
4817
  } catch (err) {
4672
4818
  this.state = "error";
@@ -4695,6 +4841,24 @@ class Worker {
4695
4841
  await this.stateStore.endRun(this.runId, "completed");
4696
4842
  } catch {}
4697
4843
  await this.recordOutcome(card.id, "success");
4844
+ } else if (this.runId && this.timedOut) {
4845
+ try {
4846
+ await runTransition(this.client, card, {
4847
+ move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
4848
+ endSession: {
4849
+ status: "failed",
4850
+ failureReason: "timeout",
4851
+ failureSummary: `Run exceeded the ${Math.round(this.config.maxTimeout / 60000)} min timeout`,
4852
+ ...buildTokenPayload(this.lastSessionStats)
4853
+ }
4854
+ });
4855
+ } catch (tErr) {
4856
+ log.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
4857
+ }
4858
+ try {
4859
+ await this.stateStore.endRun(this.runId, "failed", "timeout");
4860
+ } catch {}
4861
+ await this.recordOutcome(card.id, "failure");
4698
4862
  } else if (this.runId && this.aborted) {
4699
4863
  try {
4700
4864
  await this.stateStore.endRun(this.runId, "paused", "cancelled");
@@ -4765,6 +4929,7 @@ class Worker {
4765
4929
  signalGroup(this.process, "SIGCONT");
4766
4930
  this.timeoutTimer = setTimeout(() => {
4767
4931
  log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
4932
+ this.timedOut = true;
4768
4933
  this.cancel();
4769
4934
  }, this.config.maxTimeout);
4770
4935
  if (this.cardId) {
@@ -4791,7 +4956,7 @@ class Worker {
4791
4956
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
4792
4957
  });
4793
4958
  }
4794
- if (this.cardId) {
4959
+ if (this.cardId && !this.timedOut) {
4795
4960
  try {
4796
4961
  const stats = this.lastSessionStats ?? this.progressTracker?.stats;
4797
4962
  await this.client.endAgentSession(this.cardId, {
@@ -4895,7 +5060,7 @@ class Worker {
4895
5060
  clearTimeout(this.timeoutTimer);
4896
5061
  this.timeoutTimer = null;
4897
5062
  }
4898
- if (this.worktreePath && this.state === "error") {
5063
+ if (this.worktreePath && (this.state === "error" || this.timedOut)) {
4899
5064
  try {
4900
5065
  cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
4901
5066
  } catch {
@@ -4911,7 +5076,7 @@ class Worker {
4911
5076
  this.sessionId = null;
4912
5077
  }
4913
5078
  }
4914
- var TAG21 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
5079
+ var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
4915
5080
  var init_worker = __esm(() => {
4916
5081
  init_board_helpers();
4917
5082
  init_completion();
@@ -4930,14 +5095,18 @@ var init_worker = __esm(() => {
4930
5095
  // src/pool.ts
4931
5096
  class Pool {
4932
5097
  client;
5098
+ projectId;
4933
5099
  stateStore;
4934
5100
  implWorkers = [];
4935
5101
  reviewWorkers = [];
4936
5102
  implQueue;
4937
5103
  reviewQueue;
4938
5104
  budget;
5105
+ sleepGuard = new SleepGuard;
5106
+ onCardCompleted = null;
4939
5107
  constructor(config, client, userEmail, workspaceId, projectId, stateStore) {
4940
5108
  this.client = client;
5109
+ this.projectId = projectId;
4941
5110
  this.stateStore = stateStore;
4942
5111
  this.implQueue = new PriorityQueue(config);
4943
5112
  this.reviewQueue = new PriorityQueue(config);
@@ -4945,33 +5114,48 @@ class Pool {
4945
5114
  for (let i = 0;i < config.poolSize; i++) {
4946
5115
  this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
4947
5116
  this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
4948
- }, workspaceId, projectId, stateStore));
5117
+ this.sleepGuard.release();
5118
+ }, workspaceId, projectId, stateStore, async (completedCard) => {
5119
+ await this.onCardCompleted?.(completedCard);
5120
+ }));
4949
5121
  }
4950
5122
  if (config.review.enabled) {
4951
5123
  for (let i = 0;i < config.review.poolSize; i++) {
4952
5124
  const reviewWorkerId = config.poolSize + i;
4953
5125
  this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
4954
5126
  this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
5127
+ this.sleepGuard.release();
4955
5128
  }, stateStore, workspaceId, projectId));
4956
5129
  }
4957
5130
  }
4958
5131
  }
4959
5132
  async enqueue(card, column, labels, subtasks, mode = "implement") {
4960
5133
  if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
4961
- log.debug(TAG22, `Card ${card.id} already queued or active, skipping`);
5134
+ log.debug(TAG24, `Card ${card.id} already queued or active, skipping`);
4962
5135
  return;
4963
5136
  }
4964
5137
  if (mode === "implement") {
4965
5138
  const decision = this.budget.check(card.id);
4966
5139
  if (!decision.allow) {
4967
5140
  if (decision.reason === "daily_budget") {
4968
- log.warn(TAG22, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
5141
+ log.warn(TAG24, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
4969
5142
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
4970
5143
  } else {
4971
- log.debug(TAG22, `#${card.short_id} gave up: ${decision.detail}`);
5144
+ log.debug(TAG24, `#${card.short_id} gave up: ${decision.detail}`);
4972
5145
  }
4973
5146
  return;
4974
5147
  }
5148
+ const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
5149
+ if (blockers === null) {
5150
+ log.warn(TAG24, `#${card.short_id} blocker check failed — deferring to next tick`);
5151
+ return;
5152
+ }
5153
+ if (blockers.length > 0) {
5154
+ const list = blockers.map((b) => `#${b.shortId}`).join(", ");
5155
+ log.info(TAG24, `#${card.short_id} blocked by ${list} — waiting`);
5156
+ await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
5157
+ return;
5158
+ }
4975
5159
  }
4976
5160
  const queue = mode === "review" ? this.reviewQueue : this.implQueue;
4977
5161
  queue.enqueue(card, column, labels, mode);
@@ -4984,7 +5168,10 @@ class Pool {
4984
5168
  await this.emitWaiting(card.id, position > 0 ? `Queued (${position}/${total}) — waiting for ${mode} worker` : `Queued — waiting for ${mode} worker`);
4985
5169
  }
4986
5170
  }
5171
+ lastWaitingEmit = new Map;
4987
5172
  async emitWaiting(cardId, currentTask) {
5173
+ if (this.lastWaitingEmit.get(cardId) === currentTask)
5174
+ return;
4988
5175
  try {
4989
5176
  await this.client.updateAgentProgress(cardId, {
4990
5177
  agentIdentifier: agentIdentifier(0),
@@ -4992,23 +5179,25 @@ class Pool {
4992
5179
  status: "waiting",
4993
5180
  currentTask
4994
5181
  });
5182
+ this.lastWaitingEmit.set(cardId, currentTask);
4995
5183
  } catch (err) {
4996
- log.debug(TAG22, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5184
+ log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
4997
5185
  }
4998
5186
  }
4999
5187
  async removeCard(cardId) {
5000
5188
  await this.stateStore.resetAttempts(cardId);
5189
+ this.lastWaitingEmit.delete(cardId);
5001
5190
  for (const queue of [this.implQueue, this.reviewQueue]) {
5002
5191
  const removed = queue.remove(cardId);
5003
5192
  if (removed) {
5004
5193
  this.cardDataCache.delete(cardId);
5005
- log.info(TAG22, `Removed #${removed.shortId} from ${removed.mode} queue`);
5194
+ log.info(TAG24, `Removed #${removed.shortId} from ${removed.mode} queue`);
5006
5195
  return;
5007
5196
  }
5008
5197
  }
5009
5198
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
5010
5199
  if (worker) {
5011
- log.info(TAG22, `Cancelling worker ${worker.id} for card ${cardId}`);
5200
+ log.info(TAG24, `Cancelling worker ${worker.id} for card ${cardId}`);
5012
5201
  await worker.cancel();
5013
5202
  }
5014
5203
  }
@@ -5036,10 +5225,10 @@ class Pool {
5036
5225
  async handleAgentCommand(cardId, command) {
5037
5226
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
5038
5227
  if (!worker) {
5039
- log.debug(TAG22, `No active worker for card ${cardId}, ignoring ${command}`);
5228
+ log.debug(TAG24, `No active worker for card ${cardId}, ignoring ${command}`);
5040
5229
  return;
5041
5230
  }
5042
- log.info(TAG22, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
5231
+ log.info(TAG24, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
5043
5232
  switch (command) {
5044
5233
  case "pause":
5045
5234
  await worker.pause();
@@ -5085,19 +5274,20 @@ class Pool {
5085
5274
  };
5086
5275
  }
5087
5276
  async shutdown() {
5088
- log.info(TAG22, "Shutting down pool...");
5277
+ log.info(TAG24, "Shutting down pool...");
5089
5278
  const active = [
5090
5279
  ...this.implWorkers.filter((w) => w.isActive),
5091
5280
  ...this.reviewWorkers.filter((w) => w.isActive)
5092
5281
  ];
5093
5282
  await Promise.all(active.map((w) => w.cancel()));
5094
- log.info(TAG22, "Pool shutdown complete");
5283
+ this.sleepGuard.stop();
5284
+ log.info(TAG24, "Pool shutdown complete");
5095
5285
  }
5096
5286
  cardDataCache = new Map;
5097
5287
  tryDispatchFor(workers, queue, label) {
5098
5288
  const idle = workers.find((w) => w.isIdle);
5099
5289
  if (!idle) {
5100
- log.debug(TAG22, `No idle ${label} workers (queue: ${queue.length})`);
5290
+ log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
5101
5291
  return false;
5102
5292
  }
5103
5293
  const next = queue.dequeue();
@@ -5105,21 +5295,25 @@ class Pool {
5105
5295
  return false;
5106
5296
  const data = this.cardDataCache.get(next.cardId);
5107
5297
  if (!data) {
5108
- log.warn(TAG22, `No cached data for card ${next.cardId}, skipping`);
5298
+ log.warn(TAG24, `No cached data for card ${next.cardId}, skipping`);
5109
5299
  return false;
5110
5300
  }
5111
5301
  this.cardDataCache.delete(next.cardId);
5112
- log.info(TAG22, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
5302
+ this.lastWaitingEmit.delete(next.cardId);
5303
+ log.info(TAG24, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
5304
+ this.sleepGuard.acquire();
5113
5305
  idle.run(data.card, data.column, data.labels, data.subtasks);
5114
5306
  return true;
5115
5307
  }
5116
5308
  }
5117
- var TAG22 = "pool";
5309
+ var TAG24 = "pool";
5118
5310
  var init_pool = __esm(() => {
5119
5311
  init_log();
5120
5312
  init_queue();
5121
5313
  init_review_worker();
5314
+ init_sleep_guard();
5122
5315
  init_types();
5316
+ init_unblock();
5123
5317
  init_worker();
5124
5318
  });
5125
5319
 
@@ -5139,7 +5333,7 @@ async function fetchCardSafely(client, cardId) {
5139
5333
  const { card } = await client.getCard(cardId);
5140
5334
  return card;
5141
5335
  } catch (err) {
5142
- log.warn(TAG23, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5336
+ log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5143
5337
  return null;
5144
5338
  }
5145
5339
  }
@@ -5149,7 +5343,7 @@ async function recoverOrphans(store, client, config) {
5149
5343
  return [];
5150
5344
  }
5151
5345
  const outcomes = [];
5152
- log.info(TAG23, `recovering ${active.length} orphan run(s) from prior daemon`);
5346
+ log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
5153
5347
  for (const run of active) {
5154
5348
  const outcome = {
5155
5349
  runId: run.runId,
@@ -5161,11 +5355,11 @@ async function recoverOrphans(store, client, config) {
5161
5355
  };
5162
5356
  outcomes.push(outcome);
5163
5357
  if (isProcessAlive(run.daemonPid, process.pid)) {
5164
- log.warn(TAG23, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5358
+ log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5165
5359
  outcome.actions.push("skipped: daemon pid still alive");
5166
5360
  continue;
5167
5361
  }
5168
- log.info(TAG23, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5362
+ log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5169
5363
  await recoverRun(run, store, client, config, outcome);
5170
5364
  }
5171
5365
  return outcomes;
@@ -5183,7 +5377,7 @@ async function recoverRun(run, store, client, config, outcome) {
5183
5377
  } catch (err) {
5184
5378
  const msg = err instanceof Error ? err.message : String(err);
5185
5379
  outcome.errors.push(`endAgentSession: ${msg}`);
5186
- log.warn(TAG23, `endAgentSession failed for ${run.cardId}: ${msg}`);
5380
+ log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
5187
5381
  }
5188
5382
  const card = await fetchCardSafely(client, run.cardId);
5189
5383
  if (card) {
@@ -5224,9 +5418,9 @@ async function recoverRun(run, store, client, config, outcome) {
5224
5418
  const msg = err instanceof Error ? err.message : String(err);
5225
5419
  outcome.errors.push(`endRun: ${msg}`);
5226
5420
  }
5227
- log.info(TAG23, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5421
+ log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5228
5422
  }
5229
- var TAG23 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5423
+ var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5230
5424
  var init_recovery = __esm(() => {
5231
5425
  init_board_helpers();
5232
5426
  init_log();
@@ -5274,7 +5468,7 @@ class Reconciler {
5274
5468
  clearInterval(this.timer);
5275
5469
  this.timer = null;
5276
5470
  }
5277
- log.info(TAG24, "Heartbeat stopped");
5471
+ log.info(TAG26, "Heartbeat stopped");
5278
5472
  }
5279
5473
  async recoverStaleRuns() {
5280
5474
  if (!this.stateStore || !this.agentConfig)
@@ -5291,7 +5485,7 @@ class Reconciler {
5291
5485
  if (!daemonDead && !(heartbeatStale && ourZombie))
5292
5486
  continue;
5293
5487
  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`;
5294
- log.warn(TAG24, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5488
+ log.warn(TAG26, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5295
5489
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
5296
5490
  runId: run.runId,
5297
5491
  cardId: run.cardId,
@@ -5327,18 +5521,18 @@ class Reconciler {
5327
5521
  const subtasks = card.subtasks ?? [];
5328
5522
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
5329
5523
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
5330
- log.debug(TAG24, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5524
+ log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5331
5525
  continue;
5332
5526
  }
5333
5527
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
5334
- log.debug(TAG24, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5528
+ log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5335
5529
  continue;
5336
5530
  }
5337
5531
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
5338
- log.debug(TAG24, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5532
+ log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5339
5533
  continue;
5340
5534
  }
5341
- log.info(TAG24, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5535
+ log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5342
5536
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
5343
5537
  }
5344
5538
  }
@@ -5347,17 +5541,17 @@ class Reconciler {
5347
5541
  }
5348
5542
  for (const knownId of knownCardIds) {
5349
5543
  if (!allAgentCardIds.has(knownId)) {
5350
- log.info(TAG24, `Missed unassign: ${knownId} — removing`);
5544
+ log.info(TAG26, `Missed unassign: ${knownId} — removing`);
5351
5545
  await this.pool.removeCard(knownId);
5352
5546
  }
5353
5547
  }
5354
- log.debug(TAG24, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5548
+ log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5355
5549
  } catch (err) {
5356
- log.error(TAG24, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5550
+ log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5357
5551
  }
5358
5552
  }
5359
5553
  }
5360
- var TAG24 = "reconcile";
5554
+ var TAG26 = "reconcile";
5361
5555
  var init_reconcile = __esm(() => {
5362
5556
  init_board_helpers();
5363
5557
  init_log();
@@ -5391,7 +5585,7 @@ function prettyBanner(config, version) {
5391
5585
  checks.push({ kind: "ok", message });
5392
5586
  },
5393
5587
  warn(message) {
5394
- log.warn(TAG25, message);
5588
+ log.warn(TAG27, message);
5395
5589
  checks.push({ kind: "warn", message: message.split(`
5396
5590
  `, 1)[0] });
5397
5591
  },
@@ -5415,22 +5609,22 @@ function prettyBanner(config, version) {
5415
5609
  };
5416
5610
  }
5417
5611
  function jsonBanner(config, version) {
5418
- log.info(TAG25, `Harmony Agent Daemon v${version} starting...`);
5419
- log.info(TAG25, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5612
+ log.info(TAG27, `Harmony Agent Daemon v${version} starting...`);
5613
+ log.info(TAG27, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5420
5614
  if (config.agent.review.enabled) {
5421
- log.info(TAG25, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5615
+ log.info(TAG27, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5422
5616
  }
5423
5617
  let failed = false;
5424
5618
  return {
5425
5619
  setProjectName(_name) {},
5426
5620
  setGitProvider(provider) {
5427
- log.info(TAG25, `Git provider: ${provider}`);
5621
+ log.info(TAG27, `Git provider: ${provider}`);
5428
5622
  },
5429
5623
  check(message) {
5430
- log.info(TAG25, message);
5624
+ log.info(TAG27, message);
5431
5625
  },
5432
5626
  warn(message) {
5433
- log.warn(TAG25, message);
5627
+ log.warn(TAG27, message);
5434
5628
  },
5435
5629
  fail() {
5436
5630
  failed = true;
@@ -5438,7 +5632,7 @@ function jsonBanner(config, version) {
5438
5632
  async ready(message) {
5439
5633
  if (failed)
5440
5634
  return;
5441
- log.info(TAG25, message);
5635
+ log.info(TAG27, message);
5442
5636
  }
5443
5637
  };
5444
5638
  }
@@ -5505,7 +5699,7 @@ function cyan(s) {
5505
5699
  function yellow(s) {
5506
5700
  return `${ANSI.yellow}${s}${ANSI.reset}`;
5507
5701
  }
5508
- var TAG25 = "daemon", RULE_WIDTH = 70, ANSI;
5702
+ var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
5509
5703
  var init_startup_banner = __esm(() => {
5510
5704
  init_log();
5511
5705
  ANSI = {
@@ -5652,18 +5846,18 @@ class Watcher {
5652
5846
  }
5653
5847
  async start() {
5654
5848
  if (!isPretty()) {
5655
- log.info(TAG26, "Connecting to Supabase realtime (broadcast)...");
5849
+ log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
5656
5850
  }
5657
5851
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
5658
5852
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
5659
5853
  const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
5660
- log.debug(TAG26, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5854
+ log.debug(TAG28, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5661
5855
  this.onCardBroadcast({
5662
5856
  event: "card_update",
5663
5857
  payload: msg.payload ?? {}
5664
5858
  });
5665
5859
  }).on("broadcast", { event: "card_created" }, (msg) => {
5666
- log.debug(TAG26, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5860
+ log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5667
5861
  this.onCardBroadcast({
5668
5862
  event: "card_created",
5669
5863
  payload: msg.payload ?? {}
@@ -5673,29 +5867,29 @@ class Watcher {
5673
5867
  const cardId = payload.card_id;
5674
5868
  const command = payload.command;
5675
5869
  if (cardId && command) {
5676
- log.info(TAG26, `Broadcast: agent_command ${command} for ${cardId}`);
5870
+ log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
5677
5871
  this.onAgentCommand?.({ cardId, command });
5678
5872
  }
5679
5873
  }).subscribe((status) => {
5680
5874
  if (status === "SUBSCRIBED") {
5681
5875
  this.connected = true;
5682
5876
  if (!isPretty() || !this.suppressStartupLogs) {
5683
- log.info(TAG26, "Broadcast subscription active");
5877
+ log.info(TAG28, "Broadcast subscription active");
5684
5878
  }
5685
5879
  this.maybeResolveReady();
5686
5880
  } else if (status === "CHANNEL_ERROR") {
5687
5881
  this.connected = false;
5688
- log.error(TAG26, "Broadcast channel error — will rely on reconciliation");
5882
+ log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
5689
5883
  } else if (status === "TIMED_OUT") {
5690
5884
  this.connected = false;
5691
- log.warn(TAG26, "Broadcast subscription timed out — retrying...");
5885
+ log.warn(TAG28, "Broadcast subscription timed out — retrying...");
5692
5886
  } else if (status === "CLOSED") {
5693
5887
  this.connected = false;
5694
5888
  }
5695
5889
  });
5696
5890
  this.channel = channel;
5697
5891
  presenceChannel.on("presence", { event: "sync" }, () => {
5698
- log.debug(TAG26, "Presence sync");
5892
+ log.debug(TAG28, "Presence sync");
5699
5893
  }).subscribe(async (status) => {
5700
5894
  if (status === "SUBSCRIBED") {
5701
5895
  await presenceChannel.track({
@@ -5707,7 +5901,7 @@ class Watcher {
5707
5901
  agentName: this.identity.agentName
5708
5902
  });
5709
5903
  if (!isPretty() || !this.suppressStartupLogs) {
5710
- log.info(TAG26, "Presence tracked on board-presence channel");
5904
+ log.info(TAG28, "Presence tracked on board-presence channel");
5711
5905
  }
5712
5906
  this.presenceTracked = true;
5713
5907
  this.maybeResolveReady();
@@ -5729,10 +5923,10 @@ class Watcher {
5729
5923
  this.supabase = null;
5730
5924
  }
5731
5925
  this.connected = false;
5732
- log.info(TAG26, "Broadcast subscription stopped");
5926
+ log.info(TAG28, "Broadcast subscription stopped");
5733
5927
  }
5734
5928
  }
5735
- var TAG26 = "watcher";
5929
+ var TAG28 = "watcher";
5736
5930
  var init_watcher = __esm(() => {
5737
5931
  init_log();
5738
5932
  });
@@ -5807,10 +6001,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
5807
6001
  });
5808
6002
  } catch {}
5809
6003
  if (result.removed.length > 0) {
5810
- log.info(TAG27, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
6004
+ log.info(TAG29, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
5811
6005
  }
5812
6006
  if (result.errors.length > 0) {
5813
- log.warn(TAG27, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
6007
+ log.warn(TAG29, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
5814
6008
  }
5815
6009
  return result;
5816
6010
  }
@@ -5887,10 +6081,10 @@ function pruneFailedRemoteBranches(opts) {
5887
6081
  }
5888
6082
  }
5889
6083
  if (result.removed.length > 0) {
5890
- log.info(TAG27, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6084
+ log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
5891
6085
  }
5892
6086
  if (result.errors.length > 0) {
5893
- log.warn(TAG27, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
6087
+ log.warn(TAG29, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
5894
6088
  }
5895
6089
  return result;
5896
6090
  }
@@ -5921,13 +6115,13 @@ class WorktreeGc {
5921
6115
  try {
5922
6116
  runWorktreeGc(this.basePath, this.store);
5923
6117
  } catch (err) {
5924
- log.warn(TAG27, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6118
+ log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
5925
6119
  }
5926
6120
  if (this.remoteOpts) {
5927
6121
  try {
5928
6122
  pruneFailedRemoteBranches(this.remoteOpts);
5929
6123
  } catch (err) {
5930
- log.warn(TAG27, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6124
+ log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
5931
6125
  }
5932
6126
  }
5933
6127
  }
@@ -5941,7 +6135,7 @@ function getRepoRoot2() {
5941
6135
  return null;
5942
6136
  }
5943
6137
  }
5944
- var TAG27 = "worktree-gc";
6138
+ var TAG29 = "worktree-gc";
5945
6139
  var init_worktree_gc = __esm(() => {
5946
6140
  init_log();
5947
6141
  init_worktree();
@@ -6023,7 +6217,7 @@ async function main() {
6023
6217
  } catch (err) {
6024
6218
  if (err instanceof ConfigValidationError) {
6025
6219
  banner.fail();
6026
- log.error(TAG28, err.message);
6220
+ log.error(TAG30, err.message);
6027
6221
  process.exit(1);
6028
6222
  }
6029
6223
  throw err;
@@ -6041,12 +6235,21 @@ async function main() {
6041
6235
  const realtimeCreds = await fetchRealtimeCredentials(client);
6042
6236
  banner.check("Realtime credentials");
6043
6237
  const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore);
6238
+ const promoteSuccessors = async (completedCard) => {
6239
+ await promoteUnblockedSuccessors(completedCard, {
6240
+ client,
6241
+ agentUserId,
6242
+ enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
6243
+ });
6244
+ };
6245
+ pool.onCardCompleted = promoteSuccessors;
6044
6246
  const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
6045
6247
  const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
6046
6248
  const reconciler = new Reconciler(client, pool, config.projectId, agentUserId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
6047
6249
  let mergeMonitor = null;
6048
6250
  if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
6049
6251
  mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
6252
+ mergeMonitor.onCardCompleted = promoteSuccessors;
6050
6253
  }
6051
6254
  const worktreeGc = new WorktreeGc(config.agent.worktree.basePath, stateStore, config.agent.timing.worktreeGcIntervalMs, config.agent.worktree.failedAttemptRetentionDays > 0 ? {
6052
6255
  prefix: config.agent.worktree.failedBranchPrefix,
@@ -6115,25 +6318,25 @@ async function main() {
6115
6318
  if (shuttingDown)
6116
6319
  return;
6117
6320
  shuttingDown = true;
6118
- log.info(TAG28, `Received ${signal}, shutting down gracefully...`);
6321
+ log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
6119
6322
  reconciler.stop();
6120
6323
  mergeMonitor?.stop();
6121
6324
  worktreeGc.stop();
6122
6325
  await httpServer?.stop();
6123
6326
  await watcher.stop();
6124
6327
  await pool.shutdown();
6125
- log.info(TAG28, "Daemon stopped.");
6328
+ log.info(TAG30, "Daemon stopped.");
6126
6329
  process.exit(exitCode);
6127
6330
  };
6128
6331
  process.on("SIGINT", () => shutdown("SIGINT"));
6129
6332
  process.on("SIGTERM", () => shutdown("SIGTERM"));
6130
6333
  process.on("uncaughtException", (err) => {
6131
- log.error(TAG28, `Uncaught exception: ${err.message}`);
6334
+ log.error(TAG30, `Uncaught exception: ${err.message}`);
6132
6335
  exitCode = 1;
6133
6336
  shutdown("uncaughtException");
6134
6337
  });
6135
6338
  process.on("unhandledRejection", (reason) => {
6136
- log.error(TAG28, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6339
+ log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6137
6340
  exitCode = 1;
6138
6341
  shutdown("unhandledRejection");
6139
6342
  });
@@ -6180,14 +6383,14 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
6180
6383
  if (assigneeId === undefined)
6181
6384
  return;
6182
6385
  if (assigneeId === agentUserId) {
6183
- log.info(TAG28, `Broadcast: card ${cardId} assigned to agent`);
6386
+ log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
6184
6387
  try {
6185
6388
  await tryEnqueueCard(cardId, client, pool, config);
6186
6389
  } catch (err) {
6187
- log.error(TAG28, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6390
+ log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6188
6391
  }
6189
6392
  } else if (pool.isCardKnown(cardId)) {
6190
- log.info(TAG28, `Broadcast: card ${cardId} unassigned from agent`);
6393
+ log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
6191
6394
  await pool.removeCard(cardId);
6192
6395
  }
6193
6396
  }
@@ -6197,13 +6400,13 @@ async function tryEnqueueCard(cardId, client, pool, config) {
6197
6400
  const columns = board.columns;
6198
6401
  const column = columns.find((c) => c.id === card.column_id);
6199
6402
  if (!column) {
6200
- log.warn(TAG28, `Column not found for card ${cardId}`);
6403
+ log.warn(TAG30, `Column not found for card ${cardId}`);
6201
6404
  return;
6202
6405
  }
6203
6406
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6204
6407
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6205
6408
  if (!isPickupColumn && !isReviewColumn) {
6206
- log.info(TAG28, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6409
+ log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6207
6410
  return;
6208
6411
  }
6209
6412
  const mode = isReviewColumn ? "review" : "implement";
@@ -6211,16 +6414,16 @@ async function tryEnqueueCard(cardId, client, pool, config) {
6211
6414
  const cardLabels = resolveCardLabels(card, labelMap);
6212
6415
  const subtasks = card.subtasks ?? [];
6213
6416
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
6214
- log.debug(TAG28, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6417
+ log.debug(TAG30, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6215
6418
  return;
6216
6419
  }
6217
6420
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6218
- log.info(TAG28, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6421
+ log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6219
6422
  return;
6220
6423
  }
6221
6424
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
6222
6425
  }
6223
- var TAG28 = "daemon", PKG_VERSION;
6426
+ var TAG30 = "daemon", PKG_VERSION;
6224
6427
  var init_src = __esm(() => {
6225
6428
  init_board_helpers();
6226
6429
  init_config();
@@ -6237,6 +6440,7 @@ var init_src = __esm(() => {
6237
6440
  init_state_store();
6238
6441
  init_stream_parser_selftest();
6239
6442
  init_types();
6443
+ init_unblock();
6240
6444
  init_watcher();
6241
6445
  init_worktree_gc();
6242
6446
  ({ version: PKG_VERSION } = createRequire2(import.meta.url)("../package.json"));