@gethmy/agent 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +287 -118
  2. package/dist/index.js +287 -118
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -336,6 +336,9 @@ function loadDaemonConfig() {
336
336
  throw new Error("No user email configured. Run `npx @gethmy/mcp setup` first.");
337
337
  }
338
338
  let agentOverrides = {};
339
+ let agentName = "Harmony Agent";
340
+ let agentIdentifier2 = "harmony-daemon";
341
+ let agentColor = "#57b8a5";
339
342
  try {
340
343
  const configPath = join(homedir(), ".harmony-mcp", "config.json");
341
344
  const raw = readFileSync(configPath, "utf-8");
@@ -343,6 +346,12 @@ function loadDaemonConfig() {
343
346
  if (parsed.agent) {
344
347
  agentOverrides = parsed.agent;
345
348
  }
349
+ if (typeof parsed.agentName === "string" && parsed.agentName.trim())
350
+ agentName = parsed.agentName.trim();
351
+ if (typeof parsed.agentIdentifier === "string" && parsed.agentIdentifier.trim())
352
+ agentIdentifier2 = parsed.agentIdentifier.trim();
353
+ if (typeof parsed.agentColor === "string" && parsed.agentColor.trim())
354
+ agentColor = parsed.agentColor.trim();
346
355
  } catch {}
347
356
  const agent = {
348
357
  ...DEFAULT_AGENT_CONFIG,
@@ -380,7 +389,17 @@ function loadDaemonConfig() {
380
389
  ...agentOverrides.timing ?? {}
381
390
  }
382
391
  };
383
- return { apiKey, apiUrl, workspaceId, projectId, userEmail, agent };
392
+ return {
393
+ apiKey,
394
+ apiUrl,
395
+ workspaceId,
396
+ projectId,
397
+ userEmail,
398
+ agentName,
399
+ agentIdentifier: agentIdentifier2,
400
+ agentColor,
401
+ agent
402
+ };
384
403
  }
385
404
  async function fetchRealtimeCredentials(client) {
386
405
  const result = await client.request("GET", "/config/realtime");
@@ -3738,6 +3757,7 @@ import { createHash } from "node:crypto";
3738
3757
  class ReviewWorker {
3739
3758
  config;
3740
3759
  client;
3760
+ agentId;
3741
3761
  onDone;
3742
3762
  stateStore;
3743
3763
  workspaceId;
@@ -3754,12 +3774,14 @@ class ReviewWorker {
3754
3774
  progressTracker = null;
3755
3775
  lastSessionStats = null;
3756
3776
  aborted = false;
3777
+ timedOut = false;
3757
3778
  runId = null;
3758
3779
  lastRunLogPath = null;
3759
3780
  sessionId = null;
3760
- constructor(id, config, client, _userEmail, onDone, stateStore, workspaceId, _projectId) {
3781
+ constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
3761
3782
  this.config = config;
3762
3783
  this.client = client;
3784
+ this.agentId = agentId;
3763
3785
  this.onDone = onDone;
3764
3786
  this.stateStore = stateStore;
3765
3787
  this.workspaceId = workspaceId;
@@ -3809,6 +3831,7 @@ class ReviewWorker {
3809
3831
  }
3810
3832
  async run(card, column, labels, subtasks) {
3811
3833
  this.aborted = false;
3834
+ this.timedOut = false;
3812
3835
  this.cardId = card.id;
3813
3836
  this.startedAt = Date.now();
3814
3837
  this.runId = newRunId();
@@ -3857,6 +3880,7 @@ class ReviewWorker {
3857
3880
  const { session: reviewSession } = await this.client.startAgentSession(card.id, {
3858
3881
  agentIdentifier: agentIdentifier(this.id),
3859
3882
  agentName: `${AGENT_NAME} (Review)`,
3883
+ agentId: this.agentId,
3860
3884
  status: "working",
3861
3885
  currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
3862
3886
  progressPercent: 5
@@ -3946,6 +3970,7 @@ class ReviewWorker {
3946
3970
  });
3947
3971
  this.timeoutTimer = setTimeout(() => {
3948
3972
  log.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
3973
+ this.timedOut = true;
3949
3974
  this.cancel();
3950
3975
  }, this.config.review.maxTimeout);
3951
3976
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
@@ -3956,6 +3981,10 @@ class ReviewWorker {
3956
3981
  const sessionStats = this.lastSessionStats;
3957
3982
  if (this.aborted)
3958
3983
  return;
3984
+ if (this.timeoutTimer) {
3985
+ clearTimeout(this.timeoutTimer);
3986
+ this.timeoutTimer = null;
3987
+ }
3959
3988
  this.state = "completing";
3960
3989
  await this.recordPhase("completing");
3961
3990
  log.info(this.tag, `Claude review finished for #${card.short_id}`);
@@ -4012,7 +4041,7 @@ class ReviewWorker {
4012
4041
  try {
4013
4042
  const run = this.stateStore.getRun(this.runId);
4014
4043
  if (run && run.endedAt === null) {
4015
- const status = this.state === "error" || this.aborted ? "paused" : "completed";
4044
+ const status = this.timedOut ? "failed" : this.state === "error" || this.aborted ? "paused" : "completed";
4016
4045
  await this.stateStore.endRun(this.runId, status);
4017
4046
  }
4018
4047
  } catch {}
@@ -4050,6 +4079,7 @@ class ReviewWorker {
4050
4079
  signalGroup(this.process, "SIGCONT");
4051
4080
  this.timeoutTimer = setTimeout(() => {
4052
4081
  log.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
4082
+ this.timedOut = true;
4053
4083
  this.cancel();
4054
4084
  }, this.config.review.maxTimeout);
4055
4085
  if (this.cardId) {
@@ -4085,7 +4115,11 @@ class ReviewWorker {
4085
4115
  if (this.cardId) {
4086
4116
  try {
4087
4117
  await this.client.endAgentSession(this.cardId, {
4088
- status: "paused",
4118
+ status: this.timedOut ? "failed" : "paused",
4119
+ ...this.timedOut ? {
4120
+ failureReason: "timeout",
4121
+ failureSummary: `Review exceeded the ${Math.round(this.config.review.maxTimeout / 60000)} min timeout`
4122
+ } : {},
4089
4123
  ...buildTokenPayload(snapshotStats)
4090
4124
  });
4091
4125
  } catch {}
@@ -4263,13 +4297,81 @@ var init_review_worker = __esm(() => {
4263
4297
  init_worktree();
4264
4298
  });
4265
4299
 
4300
+ // src/sleep-guard.ts
4301
+ import { spawn as spawn3 } from "node:child_process";
4302
+
4303
+ class SleepGuard {
4304
+ platform;
4305
+ child = null;
4306
+ holds = 0;
4307
+ constructor(platform = process.platform) {
4308
+ this.platform = platform;
4309
+ }
4310
+ get active() {
4311
+ return this.child !== null;
4312
+ }
4313
+ acquire() {
4314
+ this.holds++;
4315
+ if (this.holds === 1)
4316
+ this.start();
4317
+ }
4318
+ release() {
4319
+ this.holds = Math.max(0, this.holds - 1);
4320
+ if (this.holds === 0)
4321
+ this.stop();
4322
+ }
4323
+ stop() {
4324
+ this.holds = 0;
4325
+ if (this.child) {
4326
+ if (!this.child.killed)
4327
+ this.child.kill("SIGTERM");
4328
+ this.child = null;
4329
+ log.info(TAG20, "sleep assertion released");
4330
+ }
4331
+ }
4332
+ start() {
4333
+ if (this.platform !== "darwin" || this.child)
4334
+ return;
4335
+ try {
4336
+ const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
4337
+ stdio: "ignore"
4338
+ });
4339
+ let spawned = false;
4340
+ child.on("spawn", () => {
4341
+ spawned = true;
4342
+ });
4343
+ child.on("error", (err) => {
4344
+ log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
4345
+ if (this.child === child)
4346
+ this.child = null;
4347
+ });
4348
+ child.on("exit", () => {
4349
+ if (this.child !== child)
4350
+ return;
4351
+ this.child = null;
4352
+ if (spawned && this.holds > 0)
4353
+ this.start();
4354
+ });
4355
+ child.unref();
4356
+ this.child = child;
4357
+ log.info(TAG20, "sleep assertion acquired (caffeinate -i)");
4358
+ } catch (err) {
4359
+ log.warn(TAG20, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
4360
+ }
4361
+ }
4362
+ }
4363
+ var TAG20 = "sleep-guard";
4364
+ var init_sleep_guard = __esm(() => {
4365
+ init_log();
4366
+ });
4367
+
4266
4368
  // src/unblock.ts
4267
4369
  async function fetchBlocksLinks(client, cardId) {
4268
4370
  try {
4269
4371
  const { links } = await client.getCardLinks(cardId);
4270
4372
  return links.filter((l) => l.link_type === "blocks");
4271
4373
  } catch (err) {
4272
- log.warn(TAG20, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
4374
+ log.warn(TAG21, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
4273
4375
  return null;
4274
4376
  }
4275
4377
  }
@@ -4301,22 +4403,22 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
4301
4403
  const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
4302
4404
  if (successors.length === 0)
4303
4405
  return;
4304
- log.info(TAG20, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
4406
+ log.info(TAG21, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
4305
4407
  for (const link of successors) {
4306
4408
  const successorId = link.target_card.id;
4307
4409
  try {
4308
4410
  const { card } = await deps.client.getCard(successorId);
4309
- if (card.assignee_id !== deps.agentUserId) {
4310
- log.debug(TAG20, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4411
+ if (card.assigned_agent_id !== deps.agentId) {
4412
+ log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4311
4413
  continue;
4312
4414
  }
4313
4415
  await deps.enqueue(successorId);
4314
4416
  } catch (err) {
4315
- log.warn(TAG20, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
4417
+ log.warn(TAG21, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
4316
4418
  }
4317
4419
  }
4318
4420
  }
4319
- var TAG20 = "unblock";
4421
+ var TAG21 = "unblock";
4320
4422
  var init_unblock = __esm(() => {
4321
4423
  init_log();
4322
4424
  });
@@ -4460,11 +4562,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
4460
4562
  Do NOT push to main. All your work stays on \`${branchName}\`.
4461
4563
  When finished, call harmony_end_agent_session with status="completed".`
4462
4564
  });
4463
- log.info(TAG21, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4565
+ log.info(TAG22, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4464
4566
  return result.prompt + pastEpisodesSection;
4465
4567
  } catch (err) {
4466
4568
  const msg = err instanceof Error ? err.message : String(err);
4467
- log.warn(TAG21, `Failed to generate prompt via API, using fallback: ${msg}`);
4569
+ log.warn(TAG22, `Failed to generate prompt via API, using fallback: ${msg}`);
4468
4570
  const commentsSection = await renderCommentsSection(client, card.id);
4469
4571
  return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
4470
4572
  }
@@ -4482,7 +4584,7 @@ async function renderCommentsSection(client, cardId) {
4482
4584
 
4483
4585
  ${section}` : "";
4484
4586
  } catch (err) {
4485
- log.warn(TAG21, "comment-thread fetch failed", {
4587
+ log.warn(TAG22, "comment-thread fetch failed", {
4486
4588
  event: "comment_fetch_failed",
4487
4589
  error: err instanceof Error ? err.message : String(err)
4488
4590
  });
@@ -4532,7 +4634,7 @@ ${description}`.trim();
4532
4634
  ## Similar past tasks
4533
4635
  ${bullets}`;
4534
4636
  } catch (err) {
4535
- log.warn(TAG21, "past-episodes recall failed", {
4637
+ log.warn(TAG22, "past-episodes recall failed", {
4536
4638
  event: "episode_recall_failed",
4537
4639
  error: err instanceof Error ? err.message : String(err)
4538
4640
  });
@@ -4573,7 +4675,7 @@ ${subtaskStr}
4573
4675
  You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
4574
4676
  Do NOT push to main. All your work stays on \`${branchName}\`.`;
4575
4677
  }
4576
- var TAG21 = "prompt";
4678
+ var TAG22 = "prompt";
4577
4679
  var init_prompt = __esm(() => {
4578
4680
  init_dist();
4579
4681
  init_log();
@@ -4583,6 +4685,7 @@ var init_prompt = __esm(() => {
4583
4685
  class Worker {
4584
4686
  config;
4585
4687
  client;
4688
+ agentId;
4586
4689
  onDone;
4587
4690
  workspaceId;
4588
4691
  projectId;
@@ -4600,12 +4703,14 @@ class Worker {
4600
4703
  progressTracker = null;
4601
4704
  lastSessionStats;
4602
4705
  aborted = false;
4706
+ timedOut = false;
4603
4707
  verificationFailed = false;
4604
4708
  sessionId = null;
4605
4709
  runId = null;
4606
- constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4710
+ constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4607
4711
  this.config = config;
4608
4712
  this.client = client;
4713
+ this.agentId = agentId;
4609
4714
  this.onDone = onDone;
4610
4715
  this.workspaceId = workspaceId;
4611
4716
  this.projectId = projectId;
@@ -4645,7 +4750,7 @@ class Worker {
4645
4750
  }
4646
4751
  }
4647
4752
  get tag() {
4648
- return `${TAG22}:${this.id}`;
4753
+ return `${TAG23}:${this.id}`;
4649
4754
  }
4650
4755
  get isIdle() {
4651
4756
  return this.state === "idle";
@@ -4655,6 +4760,7 @@ class Worker {
4655
4760
  }
4656
4761
  async run(card, column, labels, subtasks) {
4657
4762
  this.aborted = false;
4763
+ this.timedOut = false;
4658
4764
  this.verificationFailed = false;
4659
4765
  this.cardId = card.id;
4660
4766
  this.startedAt = Date.now();
@@ -4685,13 +4791,14 @@ class Worker {
4685
4791
  const { session } = await this.client.startAgentSession(card.id, {
4686
4792
  agentIdentifier: agentIdentifier(this.id),
4687
4793
  agentName: AGENT_NAME,
4794
+ agentId: this.agentId,
4688
4795
  status: "working",
4689
4796
  currentTask: "Setting up worktree",
4690
4797
  progressPercent: 5
4691
4798
  });
4692
4799
  const sid = session && typeof session === "object" && "id" in session ? session.id : null;
4693
4800
  if (!sid) {
4694
- log.warn(TAG22, "startAgentSession returned no session id");
4801
+ log.warn(TAG23, "startAgentSession returned no session id");
4695
4802
  }
4696
4803
  this.sessionId = sid;
4697
4804
  await this.recordPhase("preparing");
@@ -4723,11 +4830,16 @@ class Worker {
4723
4830
  });
4724
4831
  this.timeoutTimer = setTimeout(() => {
4725
4832
  log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
4833
+ this.timedOut = true;
4726
4834
  this.cancel();
4727
4835
  }, this.config.maxTimeout);
4728
4836
  await this.spawnClaude(prompt, card, subtasks);
4729
4837
  if (this.aborted)
4730
4838
  return;
4839
+ if (this.timeoutTimer) {
4840
+ clearTimeout(this.timeoutTimer);
4841
+ this.timeoutTimer = null;
4842
+ }
4731
4843
  this.state = "verifying";
4732
4844
  await this.recordPhase("verifying");
4733
4845
  log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
@@ -4769,6 +4881,32 @@ class Worker {
4769
4881
  await this.stateStore.endRun(this.runId, "completed");
4770
4882
  } catch {}
4771
4883
  await this.recordOutcome(card.id, "success");
4884
+ } else if (this.runId && this.timedOut) {
4885
+ if (this.worktreePath) {
4886
+ try {
4887
+ cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
4888
+ } catch {
4889
+ log.warn(this.tag, "Failed to cleanup worktree before requeue");
4890
+ }
4891
+ this.worktreePath = null;
4892
+ }
4893
+ try {
4894
+ await runTransition(this.client, card, {
4895
+ move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
4896
+ endSession: {
4897
+ status: "failed",
4898
+ failureReason: "timeout",
4899
+ failureSummary: `Run exceeded the ${Math.round(this.config.maxTimeout / 60000)} min timeout`,
4900
+ ...buildTokenPayload(this.lastSessionStats)
4901
+ }
4902
+ });
4903
+ } catch (tErr) {
4904
+ log.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
4905
+ }
4906
+ try {
4907
+ await this.stateStore.endRun(this.runId, "failed", "timeout");
4908
+ } catch {}
4909
+ await this.recordOutcome(card.id, "failure");
4772
4910
  } else if (this.runId && this.aborted) {
4773
4911
  try {
4774
4912
  await this.stateStore.endRun(this.runId, "paused", "cancelled");
@@ -4839,6 +4977,7 @@ class Worker {
4839
4977
  signalGroup(this.process, "SIGCONT");
4840
4978
  this.timeoutTimer = setTimeout(() => {
4841
4979
  log.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
4980
+ this.timedOut = true;
4842
4981
  this.cancel();
4843
4982
  }, this.config.maxTimeout);
4844
4983
  if (this.cardId) {
@@ -4865,7 +5004,7 @@ class Worker {
4865
5004
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
4866
5005
  });
4867
5006
  }
4868
- if (this.cardId) {
5007
+ if (this.cardId && !this.timedOut) {
4869
5008
  try {
4870
5009
  const stats = this.lastSessionStats ?? this.progressTracker?.stats;
4871
5010
  await this.client.endAgentSession(this.cardId, {
@@ -4969,7 +5108,7 @@ class Worker {
4969
5108
  clearTimeout(this.timeoutTimer);
4970
5109
  this.timeoutTimer = null;
4971
5110
  }
4972
- if (this.worktreePath && this.state === "error") {
5111
+ if (this.worktreePath && (this.state === "error" || this.timedOut)) {
4973
5112
  try {
4974
5113
  cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
4975
5114
  } catch {
@@ -4985,7 +5124,7 @@ class Worker {
4985
5124
  this.sessionId = null;
4986
5125
  }
4987
5126
  }
4988
- var TAG22 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
5127
+ var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
4989
5128
  var init_worker = __esm(() => {
4990
5129
  init_board_helpers();
4991
5130
  init_completion();
@@ -5006,22 +5145,30 @@ class Pool {
5006
5145
  client;
5007
5146
  projectId;
5008
5147
  stateStore;
5148
+ agentId;
5009
5149
  implWorkers = [];
5010
5150
  reviewWorkers = [];
5011
5151
  implQueue;
5012
5152
  reviewQueue;
5013
5153
  budget;
5154
+ sleepGuard = new SleepGuard;
5155
+ shuttingDown = false;
5014
5156
  onCardCompleted = null;
5015
- constructor(config, client, userEmail, workspaceId, projectId, stateStore) {
5157
+ constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
5016
5158
  this.client = client;
5017
5159
  this.projectId = projectId;
5018
5160
  this.stateStore = stateStore;
5161
+ this.agentId = agentId;
5019
5162
  this.implQueue = new PriorityQueue(config);
5020
5163
  this.reviewQueue = new PriorityQueue(config);
5021
5164
  this.budget = new BudgetGuard(config.budget, this.stateStore);
5022
5165
  for (let i = 0;i < config.poolSize; i++) {
5023
- this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
5024
- this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
5166
+ this.implWorkers.push(new Worker(i, config, client, this.agentId, () => {
5167
+ try {
5168
+ this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
5169
+ } finally {
5170
+ this.sleepGuard.release();
5171
+ }
5025
5172
  }, workspaceId, projectId, stateStore, async (completedCard) => {
5026
5173
  await this.onCardCompleted?.(completedCard);
5027
5174
  }));
@@ -5029,36 +5176,40 @@ class Pool {
5029
5176
  if (config.review.enabled) {
5030
5177
  for (let i = 0;i < config.review.poolSize; i++) {
5031
5178
  const reviewWorkerId = config.poolSize + i;
5032
- this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
5033
- this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
5179
+ this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, this.agentId, () => {
5180
+ try {
5181
+ this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
5182
+ } finally {
5183
+ this.sleepGuard.release();
5184
+ }
5034
5185
  }, stateStore, workspaceId, projectId));
5035
5186
  }
5036
5187
  }
5037
5188
  }
5038
5189
  async enqueue(card, column, labels, subtasks, mode = "implement") {
5039
5190
  if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
5040
- log.debug(TAG23, `Card ${card.id} already queued or active, skipping`);
5191
+ log.debug(TAG24, `Card ${card.id} already queued or active, skipping`);
5041
5192
  return;
5042
5193
  }
5043
5194
  if (mode === "implement") {
5044
5195
  const decision = this.budget.check(card.id);
5045
5196
  if (!decision.allow) {
5046
5197
  if (decision.reason === "daily_budget") {
5047
- log.warn(TAG23, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
5198
+ log.warn(TAG24, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
5048
5199
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
5049
5200
  } else {
5050
- log.debug(TAG23, `#${card.short_id} gave up: ${decision.detail}`);
5201
+ log.debug(TAG24, `#${card.short_id} gave up: ${decision.detail}`);
5051
5202
  }
5052
5203
  return;
5053
5204
  }
5054
5205
  const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
5055
5206
  if (blockers === null) {
5056
- log.warn(TAG23, `#${card.short_id} blocker check failed — deferring to next tick`);
5207
+ log.warn(TAG24, `#${card.short_id} blocker check failed — deferring to next tick`);
5057
5208
  return;
5058
5209
  }
5059
5210
  if (blockers.length > 0) {
5060
5211
  const list = blockers.map((b) => `#${b.shortId}`).join(", ");
5061
- log.info(TAG23, `#${card.short_id} blocked by ${list} — waiting`);
5212
+ log.info(TAG24, `#${card.short_id} blocked by ${list} — waiting`);
5062
5213
  await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
5063
5214
  return;
5064
5215
  }
@@ -5087,7 +5238,7 @@ class Pool {
5087
5238
  });
5088
5239
  this.lastWaitingEmit.set(cardId, currentTask);
5089
5240
  } catch (err) {
5090
- log.debug(TAG23, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5241
+ log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5091
5242
  }
5092
5243
  }
5093
5244
  async removeCard(cardId) {
@@ -5097,13 +5248,13 @@ class Pool {
5097
5248
  const removed = queue.remove(cardId);
5098
5249
  if (removed) {
5099
5250
  this.cardDataCache.delete(cardId);
5100
- log.info(TAG23, `Removed #${removed.shortId} from ${removed.mode} queue`);
5251
+ log.info(TAG24, `Removed #${removed.shortId} from ${removed.mode} queue`);
5101
5252
  return;
5102
5253
  }
5103
5254
  }
5104
5255
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
5105
5256
  if (worker) {
5106
- log.info(TAG23, `Cancelling worker ${worker.id} for card ${cardId}`);
5257
+ log.info(TAG24, `Cancelling worker ${worker.id} for card ${cardId}`);
5107
5258
  await worker.cancel();
5108
5259
  }
5109
5260
  }
@@ -5131,10 +5282,10 @@ class Pool {
5131
5282
  async handleAgentCommand(cardId, command) {
5132
5283
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
5133
5284
  if (!worker) {
5134
- log.debug(TAG23, `No active worker for card ${cardId}, ignoring ${command}`);
5285
+ log.debug(TAG24, `No active worker for card ${cardId}, ignoring ${command}`);
5135
5286
  return;
5136
5287
  }
5137
- log.info(TAG23, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
5288
+ log.info(TAG24, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
5138
5289
  switch (command) {
5139
5290
  case "pause":
5140
5291
  await worker.pause();
@@ -5180,19 +5331,23 @@ class Pool {
5180
5331
  };
5181
5332
  }
5182
5333
  async shutdown() {
5183
- log.info(TAG23, "Shutting down pool...");
5334
+ log.info(TAG24, "Shutting down pool...");
5335
+ this.shuttingDown = true;
5184
5336
  const active = [
5185
5337
  ...this.implWorkers.filter((w) => w.isActive),
5186
5338
  ...this.reviewWorkers.filter((w) => w.isActive)
5187
5339
  ];
5188
5340
  await Promise.all(active.map((w) => w.cancel()));
5189
- log.info(TAG23, "Pool shutdown complete");
5341
+ this.sleepGuard.stop();
5342
+ log.info(TAG24, "Pool shutdown complete");
5190
5343
  }
5191
5344
  cardDataCache = new Map;
5192
5345
  tryDispatchFor(workers, queue, label) {
5346
+ if (this.shuttingDown)
5347
+ return false;
5193
5348
  const idle = workers.find((w) => w.isIdle);
5194
5349
  if (!idle) {
5195
- log.debug(TAG23, `No idle ${label} workers (queue: ${queue.length})`);
5350
+ log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
5196
5351
  return false;
5197
5352
  }
5198
5353
  const next = queue.dequeue();
@@ -5200,21 +5355,23 @@ class Pool {
5200
5355
  return false;
5201
5356
  const data = this.cardDataCache.get(next.cardId);
5202
5357
  if (!data) {
5203
- log.warn(TAG23, `No cached data for card ${next.cardId}, skipping`);
5358
+ log.warn(TAG24, `No cached data for card ${next.cardId}, skipping`);
5204
5359
  return false;
5205
5360
  }
5206
5361
  this.cardDataCache.delete(next.cardId);
5207
5362
  this.lastWaitingEmit.delete(next.cardId);
5208
- log.info(TAG23, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
5363
+ log.info(TAG24, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
5364
+ this.sleepGuard.acquire();
5209
5365
  idle.run(data.card, data.column, data.labels, data.subtasks);
5210
5366
  return true;
5211
5367
  }
5212
5368
  }
5213
- var TAG23 = "pool";
5369
+ var TAG24 = "pool";
5214
5370
  var init_pool = __esm(() => {
5215
5371
  init_log();
5216
5372
  init_queue();
5217
5373
  init_review_worker();
5374
+ init_sleep_guard();
5218
5375
  init_types();
5219
5376
  init_unblock();
5220
5377
  init_worker();
@@ -5236,7 +5393,7 @@ async function fetchCardSafely(client, cardId) {
5236
5393
  const { card } = await client.getCard(cardId);
5237
5394
  return card;
5238
5395
  } catch (err) {
5239
- log.warn(TAG24, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5396
+ log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5240
5397
  return null;
5241
5398
  }
5242
5399
  }
@@ -5246,7 +5403,7 @@ async function recoverOrphans(store, client, config) {
5246
5403
  return [];
5247
5404
  }
5248
5405
  const outcomes = [];
5249
- log.info(TAG24, `recovering ${active.length} orphan run(s) from prior daemon`);
5406
+ log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
5250
5407
  for (const run of active) {
5251
5408
  const outcome = {
5252
5409
  runId: run.runId,
@@ -5258,11 +5415,11 @@ async function recoverOrphans(store, client, config) {
5258
5415
  };
5259
5416
  outcomes.push(outcome);
5260
5417
  if (isProcessAlive(run.daemonPid, process.pid)) {
5261
- log.warn(TAG24, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5418
+ log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5262
5419
  outcome.actions.push("skipped: daemon pid still alive");
5263
5420
  continue;
5264
5421
  }
5265
- log.info(TAG24, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5422
+ log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5266
5423
  await recoverRun(run, store, client, config, outcome);
5267
5424
  }
5268
5425
  return outcomes;
@@ -5280,7 +5437,7 @@ async function recoverRun(run, store, client, config, outcome) {
5280
5437
  } catch (err) {
5281
5438
  const msg = err instanceof Error ? err.message : String(err);
5282
5439
  outcome.errors.push(`endAgentSession: ${msg}`);
5283
- log.warn(TAG24, `endAgentSession failed for ${run.cardId}: ${msg}`);
5440
+ log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
5284
5441
  }
5285
5442
  const card = await fetchCardSafely(client, run.cardId);
5286
5443
  if (card) {
@@ -5321,9 +5478,9 @@ async function recoverRun(run, store, client, config, outcome) {
5321
5478
  const msg = err instanceof Error ? err.message : String(err);
5322
5479
  outcome.errors.push(`endRun: ${msg}`);
5323
5480
  }
5324
- log.info(TAG24, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5481
+ log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5325
5482
  }
5326
- var TAG24 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5483
+ var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5327
5484
  var init_recovery = __esm(() => {
5328
5485
  init_board_helpers();
5329
5486
  init_log();
@@ -5335,7 +5492,7 @@ class Reconciler {
5335
5492
  client;
5336
5493
  pool;
5337
5494
  projectId;
5338
- agentUserId;
5495
+ agentId;
5339
5496
  pickupColumns;
5340
5497
  reviewColumns;
5341
5498
  approvedLabel;
@@ -5350,11 +5507,11 @@ class Reconciler {
5350
5507
  get isRunning() {
5351
5508
  return this.timer !== null;
5352
5509
  }
5353
- constructor(client, pool, projectId, agentUserId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
5510
+ constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
5354
5511
  this.client = client;
5355
5512
  this.pool = pool;
5356
5513
  this.projectId = projectId;
5357
- this.agentUserId = agentUserId;
5514
+ this.agentId = agentId;
5358
5515
  this.pickupColumns = pickupColumns;
5359
5516
  this.reviewColumns = reviewColumns;
5360
5517
  this.approvedLabel = approvedLabel;
@@ -5371,7 +5528,7 @@ class Reconciler {
5371
5528
  clearInterval(this.timer);
5372
5529
  this.timer = null;
5373
5530
  }
5374
- log.info(TAG25, "Heartbeat stopped");
5531
+ log.info(TAG26, "Heartbeat stopped");
5375
5532
  }
5376
5533
  async recoverStaleRuns() {
5377
5534
  if (!this.stateStore || !this.agentConfig)
@@ -5388,7 +5545,7 @@ class Reconciler {
5388
5545
  if (!daemonDead && !(heartbeatStale && ourZombie))
5389
5546
  continue;
5390
5547
  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`;
5391
- log.warn(TAG25, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5548
+ log.warn(TAG26, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5392
5549
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
5393
5550
  runId: run.runId,
5394
5551
  cardId: run.cardId,
@@ -5412,9 +5569,9 @@ class Reconciler {
5412
5569
  }
5413
5570
  const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5414
5571
  const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5415
- const assignedCards = cards.filter((c) => c.assignee_id === this.agentUserId && !c.archived_at && (pickupColumnIds.has(c.column_id) || reviewColumnIds.has(c.column_id)));
5572
+ const assignedCards = cards.filter((c) => c.assigned_agent_id === this.agentId && !c.archived_at && (pickupColumnIds.has(c.column_id) || reviewColumnIds.has(c.column_id)));
5416
5573
  const knownCardIds = this.pool.knownCardIds();
5417
- const allAgentCardIds = new Set(cards.filter((c) => c.assignee_id === this.agentUserId && !c.archived_at).map((c) => c.id));
5574
+ const allAgentCardIds = new Set(cards.filter((c) => c.assigned_agent_id === this.agentId && !c.archived_at).map((c) => c.id));
5418
5575
  for (const card of assignedCards) {
5419
5576
  if (!knownCardIds.has(card.id)) {
5420
5577
  const column = columnMap.get(card.column_id);
@@ -5424,18 +5581,18 @@ class Reconciler {
5424
5581
  const subtasks = card.subtasks ?? [];
5425
5582
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
5426
5583
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
5427
- log.debug(TAG25, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5584
+ log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5428
5585
  continue;
5429
5586
  }
5430
5587
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
5431
- log.debug(TAG25, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5588
+ log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5432
5589
  continue;
5433
5590
  }
5434
5591
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
5435
- log.debug(TAG25, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5592
+ log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5436
5593
  continue;
5437
5594
  }
5438
- log.info(TAG25, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5595
+ log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5439
5596
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
5440
5597
  }
5441
5598
  }
@@ -5444,17 +5601,17 @@ class Reconciler {
5444
5601
  }
5445
5602
  for (const knownId of knownCardIds) {
5446
5603
  if (!allAgentCardIds.has(knownId)) {
5447
- log.info(TAG25, `Missed unassign: ${knownId} — removing`);
5604
+ log.info(TAG26, `Missed unassign: ${knownId} — removing`);
5448
5605
  await this.pool.removeCard(knownId);
5449
5606
  }
5450
5607
  }
5451
- log.debug(TAG25, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5608
+ log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5452
5609
  } catch (err) {
5453
- log.error(TAG25, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5610
+ log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5454
5611
  }
5455
5612
  }
5456
5613
  }
5457
- var TAG25 = "reconcile";
5614
+ var TAG26 = "reconcile";
5458
5615
  var init_reconcile = __esm(() => {
5459
5616
  init_board_helpers();
5460
5617
  init_log();
@@ -5488,7 +5645,7 @@ function prettyBanner(config, version) {
5488
5645
  checks.push({ kind: "ok", message });
5489
5646
  },
5490
5647
  warn(message) {
5491
- log.warn(TAG26, message);
5648
+ log.warn(TAG27, message);
5492
5649
  checks.push({ kind: "warn", message: message.split(`
5493
5650
  `, 1)[0] });
5494
5651
  },
@@ -5512,22 +5669,22 @@ function prettyBanner(config, version) {
5512
5669
  };
5513
5670
  }
5514
5671
  function jsonBanner(config, version) {
5515
- log.info(TAG26, `Harmony Agent Daemon v${version} starting...`);
5516
- log.info(TAG26, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5672
+ log.info(TAG27, `Harmony Agent Daemon v${version} starting...`);
5673
+ log.info(TAG27, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5517
5674
  if (config.agent.review.enabled) {
5518
- log.info(TAG26, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5675
+ log.info(TAG27, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5519
5676
  }
5520
5677
  let failed = false;
5521
5678
  return {
5522
5679
  setProjectName(_name) {},
5523
5680
  setGitProvider(provider) {
5524
- log.info(TAG26, `Git provider: ${provider}`);
5681
+ log.info(TAG27, `Git provider: ${provider}`);
5525
5682
  },
5526
5683
  check(message) {
5527
- log.info(TAG26, message);
5684
+ log.info(TAG27, message);
5528
5685
  },
5529
5686
  warn(message) {
5530
- log.warn(TAG26, message);
5687
+ log.warn(TAG27, message);
5531
5688
  },
5532
5689
  fail() {
5533
5690
  failed = true;
@@ -5535,7 +5692,7 @@ function jsonBanner(config, version) {
5535
5692
  async ready(message) {
5536
5693
  if (failed)
5537
5694
  return;
5538
- log.info(TAG26, message);
5695
+ log.info(TAG27, message);
5539
5696
  }
5540
5697
  };
5541
5698
  }
@@ -5602,7 +5759,7 @@ function cyan(s) {
5602
5759
  function yellow(s) {
5603
5760
  return `${ANSI.yellow}${s}${ANSI.reset}`;
5604
5761
  }
5605
- var TAG26 = "daemon", RULE_WIDTH = 70, ANSI;
5762
+ var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
5606
5763
  var init_startup_banner = __esm(() => {
5607
5764
  init_log();
5608
5765
  ANSI = {
@@ -5749,18 +5906,18 @@ class Watcher {
5749
5906
  }
5750
5907
  async start() {
5751
5908
  if (!isPretty()) {
5752
- log.info(TAG27, "Connecting to Supabase realtime (broadcast)...");
5909
+ log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
5753
5910
  }
5754
5911
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
5755
5912
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
5756
5913
  const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
5757
- log.debug(TAG27, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5914
+ log.debug(TAG28, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5758
5915
  this.onCardBroadcast({
5759
5916
  event: "card_update",
5760
5917
  payload: msg.payload ?? {}
5761
5918
  });
5762
5919
  }).on("broadcast", { event: "card_created" }, (msg) => {
5763
- log.debug(TAG27, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5920
+ log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5764
5921
  this.onCardBroadcast({
5765
5922
  event: "card_created",
5766
5923
  payload: msg.payload ?? {}
@@ -5770,41 +5927,42 @@ class Watcher {
5770
5927
  const cardId = payload.card_id;
5771
5928
  const command = payload.command;
5772
5929
  if (cardId && command) {
5773
- log.info(TAG27, `Broadcast: agent_command ${command} for ${cardId}`);
5930
+ log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
5774
5931
  this.onAgentCommand?.({ cardId, command });
5775
5932
  }
5776
5933
  }).subscribe((status) => {
5777
5934
  if (status === "SUBSCRIBED") {
5778
5935
  this.connected = true;
5779
5936
  if (!isPretty() || !this.suppressStartupLogs) {
5780
- log.info(TAG27, "Broadcast subscription active");
5937
+ log.info(TAG28, "Broadcast subscription active");
5781
5938
  }
5782
5939
  this.maybeResolveReady();
5783
5940
  } else if (status === "CHANNEL_ERROR") {
5784
5941
  this.connected = false;
5785
- log.error(TAG27, "Broadcast channel error — will rely on reconciliation");
5942
+ log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
5786
5943
  } else if (status === "TIMED_OUT") {
5787
5944
  this.connected = false;
5788
- log.warn(TAG27, "Broadcast subscription timed out — retrying...");
5945
+ log.warn(TAG28, "Broadcast subscription timed out — retrying...");
5789
5946
  } else if (status === "CLOSED") {
5790
5947
  this.connected = false;
5791
5948
  }
5792
5949
  });
5793
5950
  this.channel = channel;
5794
5951
  presenceChannel.on("presence", { event: "sync" }, () => {
5795
- log.debug(TAG27, "Presence sync");
5952
+ log.debug(TAG28, "Presence sync");
5796
5953
  }).subscribe(async (status) => {
5797
5954
  if (status === "SUBSCRIBED") {
5798
5955
  await presenceChannel.track({
5799
5956
  daemonId: this.daemonId,
5800
5957
  startedAt: new Date().toISOString(),
5801
5958
  userId: this.identity.userId,
5959
+ agentId: this.identity.agentId,
5802
5960
  userEmail: this.identity.userEmail,
5803
5961
  agentIdentifier: this.identity.agentIdentifier,
5804
5962
  agentName: this.identity.agentName
5805
5963
  });
5806
5964
  if (!isPretty() || !this.suppressStartupLogs) {
5807
- log.info(TAG27, "Presence tracked on board-presence channel");
5965
+ log.info(TAG28, "Presence tracked on board-presence channel");
5808
5966
  }
5809
5967
  this.presenceTracked = true;
5810
5968
  this.maybeResolveReady();
@@ -5826,10 +5984,10 @@ class Watcher {
5826
5984
  this.supabase = null;
5827
5985
  }
5828
5986
  this.connected = false;
5829
- log.info(TAG27, "Broadcast subscription stopped");
5987
+ log.info(TAG28, "Broadcast subscription stopped");
5830
5988
  }
5831
5989
  }
5832
- var TAG27 = "watcher";
5990
+ var TAG28 = "watcher";
5833
5991
  var init_watcher = __esm(() => {
5834
5992
  init_log();
5835
5993
  });
@@ -5904,10 +6062,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
5904
6062
  });
5905
6063
  } catch {}
5906
6064
  if (result.removed.length > 0) {
5907
- log.info(TAG28, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
6065
+ log.info(TAG29, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
5908
6066
  }
5909
6067
  if (result.errors.length > 0) {
5910
- log.warn(TAG28, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
6068
+ log.warn(TAG29, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
5911
6069
  }
5912
6070
  return result;
5913
6071
  }
@@ -5984,10 +6142,10 @@ function pruneFailedRemoteBranches(opts) {
5984
6142
  }
5985
6143
  }
5986
6144
  if (result.removed.length > 0) {
5987
- log.info(TAG28, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6145
+ log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
5988
6146
  }
5989
6147
  if (result.errors.length > 0) {
5990
- log.warn(TAG28, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
6148
+ log.warn(TAG29, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
5991
6149
  }
5992
6150
  return result;
5993
6151
  }
@@ -6018,13 +6176,13 @@ class WorktreeGc {
6018
6176
  try {
6019
6177
  runWorktreeGc(this.basePath, this.store);
6020
6178
  } catch (err) {
6021
- log.warn(TAG28, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6179
+ log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6022
6180
  }
6023
6181
  if (this.remoteOpts) {
6024
6182
  try {
6025
6183
  pruneFailedRemoteBranches(this.remoteOpts);
6026
6184
  } catch (err) {
6027
- log.warn(TAG28, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6185
+ log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6028
6186
  }
6029
6187
  }
6030
6188
  }
@@ -6038,7 +6196,7 @@ function getRepoRoot2() {
6038
6196
  return null;
6039
6197
  }
6040
6198
  }
6041
- var TAG28 = "worktree-gc";
6199
+ var TAG29 = "worktree-gc";
6042
6200
  var init_worktree_gc = __esm(() => {
6043
6201
  init_log();
6044
6202
  init_worktree();
@@ -6120,7 +6278,7 @@ async function main() {
6120
6278
  } catch (err) {
6121
6279
  if (err instanceof ConfigValidationError) {
6122
6280
  banner.fail();
6123
- log.error(TAG29, err.message);
6281
+ log.error(TAG30, err.message);
6124
6282
  process.exit(1);
6125
6283
  }
6126
6284
  throw err;
@@ -6135,20 +6293,27 @@ async function main() {
6135
6293
  const errored = outcomes.filter((o) => o.errors.length).length;
6136
6294
  banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
6137
6295
  }
6296
+ const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
6297
+ identifier: config.agentIdentifier,
6298
+ name: config.agentName,
6299
+ color: config.agentColor
6300
+ });
6301
+ const agentId = registeredAgent.id;
6302
+ banner.check(`Agent registered (${config.agentName})`);
6138
6303
  const realtimeCreds = await fetchRealtimeCredentials(client);
6139
6304
  banner.check("Realtime credentials");
6140
- const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore);
6305
+ const pool = new Pool(config.agent, client, config.userEmail, config.workspaceId, config.projectId, stateStore, agentId);
6141
6306
  const promoteSuccessors = async (completedCard) => {
6142
6307
  await promoteUnblockedSuccessors(completedCard, {
6143
6308
  client,
6144
- agentUserId,
6145
- enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
6309
+ agentId,
6310
+ enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
6146
6311
  });
6147
6312
  };
6148
6313
  pool.onCardCompleted = promoteSuccessors;
6149
6314
  const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
6150
6315
  const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
6151
- const reconciler = new Reconciler(client, pool, config.projectId, agentUserId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
6316
+ const reconciler = new Reconciler(client, pool, config.projectId, agentId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
6152
6317
  let mergeMonitor = null;
6153
6318
  if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
6154
6319
  mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
@@ -6207,11 +6372,12 @@ async function main() {
6207
6372
  }) : null;
6208
6373
  const watcher = new Watcher(realtimeCreds, config.projectId, {
6209
6374
  userId: agentUserId,
6375
+ agentId,
6210
6376
  userEmail: config.userEmail,
6211
- agentIdentifier: "harmony-daemon",
6212
- agentName: AGENT_NAME
6377
+ agentIdentifier: config.agentIdentifier,
6378
+ agentName: config.agentName
6213
6379
  }, async (event) => {
6214
- await handleBroadcast(event, client, pool, config, agentUserId);
6380
+ await handleBroadcast(event, client, pool, config, agentId);
6215
6381
  }, async (command) => {
6216
6382
  await pool.handleAgentCommand(command.cardId, command.command);
6217
6383
  });
@@ -6221,25 +6387,25 @@ async function main() {
6221
6387
  if (shuttingDown)
6222
6388
  return;
6223
6389
  shuttingDown = true;
6224
- log.info(TAG29, `Received ${signal}, shutting down gracefully...`);
6390
+ log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
6225
6391
  reconciler.stop();
6226
6392
  mergeMonitor?.stop();
6227
6393
  worktreeGc.stop();
6228
6394
  await httpServer?.stop();
6229
6395
  await watcher.stop();
6230
6396
  await pool.shutdown();
6231
- log.info(TAG29, "Daemon stopped.");
6397
+ log.info(TAG30, "Daemon stopped.");
6232
6398
  process.exit(exitCode);
6233
6399
  };
6234
6400
  process.on("SIGINT", () => shutdown("SIGINT"));
6235
6401
  process.on("SIGTERM", () => shutdown("SIGTERM"));
6236
6402
  process.on("uncaughtException", (err) => {
6237
- log.error(TAG29, `Uncaught exception: ${err.message}`);
6403
+ log.error(TAG30, `Uncaught exception: ${err.message}`);
6238
6404
  exitCode = 1;
6239
6405
  shutdown("uncaughtException");
6240
6406
  });
6241
6407
  process.on("unhandledRejection", (reason) => {
6242
- log.error(TAG29, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6408
+ log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6243
6409
  exitCode = 1;
6244
6410
  shutdown("unhandledRejection");
6245
6411
  });
@@ -6277,39 +6443,43 @@ async function main() {
6277
6443
  await banner.ready("watching for card assignments");
6278
6444
  watcher.allowStartupLogs();
6279
6445
  }
6280
- async function handleBroadcast(event, client, pool, config, agentUserId) {
6446
+ async function handleBroadcast(event, client, pool, config, agentId) {
6281
6447
  const payload = event.payload;
6282
6448
  const cardId = payload.card_id;
6283
6449
  if (!cardId)
6284
6450
  return;
6285
- const assigneeId = payload.assignee_id;
6286
- if (assigneeId === undefined)
6451
+ const assignedAgentId = payload.assigned_agent_id;
6452
+ if (assignedAgentId === undefined)
6287
6453
  return;
6288
- if (assigneeId === agentUserId) {
6289
- log.info(TAG29, `Broadcast: card ${cardId} assigned to agent`);
6454
+ if (assignedAgentId === agentId) {
6455
+ log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
6290
6456
  try {
6291
- await tryEnqueueCard(cardId, client, pool, config);
6457
+ await tryEnqueueCard(cardId, client, pool, config, agentId);
6292
6458
  } catch (err) {
6293
- log.error(TAG29, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6459
+ log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6294
6460
  }
6295
6461
  } else if (pool.isCardKnown(cardId)) {
6296
- log.info(TAG29, `Broadcast: card ${cardId} unassigned from agent`);
6462
+ log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
6297
6463
  await pool.removeCard(cardId);
6298
6464
  }
6299
6465
  }
6300
- async function tryEnqueueCard(cardId, client, pool, config) {
6466
+ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6301
6467
  const { card } = await client.getCard(cardId);
6468
+ if (card.assigned_agent_id !== agentId) {
6469
+ log.debug(TAG30, `Card ${cardId} no longer assigned to agent — skipping`);
6470
+ return;
6471
+ }
6302
6472
  const board = await client.getBoard(config.projectId, { summary: true });
6303
6473
  const columns = board.columns;
6304
6474
  const column = columns.find((c) => c.id === card.column_id);
6305
6475
  if (!column) {
6306
- log.warn(TAG29, `Column not found for card ${cardId}`);
6476
+ log.warn(TAG30, `Column not found for card ${cardId}`);
6307
6477
  return;
6308
6478
  }
6309
6479
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6310
6480
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6311
6481
  if (!isPickupColumn && !isReviewColumn) {
6312
- log.info(TAG29, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6482
+ log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6313
6483
  return;
6314
6484
  }
6315
6485
  const mode = isReviewColumn ? "review" : "implement";
@@ -6317,16 +6487,16 @@ async function tryEnqueueCard(cardId, client, pool, config) {
6317
6487
  const cardLabels = resolveCardLabels(card, labelMap);
6318
6488
  const subtasks = card.subtasks ?? [];
6319
6489
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
6320
- log.debug(TAG29, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6490
+ log.debug(TAG30, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6321
6491
  return;
6322
6492
  }
6323
6493
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6324
- log.info(TAG29, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6494
+ log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6325
6495
  return;
6326
6496
  }
6327
6497
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
6328
6498
  }
6329
- var TAG29 = "daemon", PKG_VERSION;
6499
+ var TAG30 = "daemon", PKG_VERSION;
6330
6500
  var init_src = __esm(() => {
6331
6501
  init_board_helpers();
6332
6502
  init_config();
@@ -6342,7 +6512,6 @@ var init_src = __esm(() => {
6342
6512
  init_startup_banner();
6343
6513
  init_state_store();
6344
6514
  init_stream_parser_selftest();
6345
- init_types();
6346
6515
  init_unblock();
6347
6516
  init_watcher();
6348
6517
  init_worktree_gc();