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