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