@gethmy/agent 1.8.1 → 1.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +104 -32
  2. package/dist/index.js +104 -32
  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;
@@ -3759,9 +3779,10 @@ class ReviewWorker {
3759
3779
  runId = null;
3760
3780
  lastRunLogPath = null;
3761
3781
  sessionId = null;
3762
- constructor(id, config, client, _userEmail, onDone, stateStore, workspaceId, _projectId) {
3782
+ constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
3763
3783
  this.config = config;
3764
3784
  this.client = client;
3785
+ this.agentId = agentId;
3765
3786
  this.onDone = onDone;
3766
3787
  this.stateStore = stateStore;
3767
3788
  this.workspaceId = workspaceId;
@@ -3860,6 +3881,7 @@ class ReviewWorker {
3860
3881
  const { session: reviewSession } = await this.client.startAgentSession(card.id, {
3861
3882
  agentIdentifier: agentIdentifier(this.id),
3862
3883
  agentName: `${AGENT_NAME} (Review)`,
3884
+ agentId: this.agentId,
3863
3885
  status: "working",
3864
3886
  currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
3865
3887
  progressPercent: 5
@@ -3960,6 +3982,10 @@ class ReviewWorker {
3960
3982
  const sessionStats = this.lastSessionStats;
3961
3983
  if (this.aborted)
3962
3984
  return;
3985
+ if (this.timeoutTimer) {
3986
+ clearTimeout(this.timeoutTimer);
3987
+ this.timeoutTimer = null;
3988
+ }
3963
3989
  this.state = "completing";
3964
3990
  await this.recordPhase("completing");
3965
3991
  log.info(this.tag, `Claude review finished for #${card.short_id}`);
@@ -4311,14 +4337,21 @@ class SleepGuard {
4311
4337
  const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
4312
4338
  stdio: "ignore"
4313
4339
  });
4340
+ let spawned = false;
4341
+ child.on("spawn", () => {
4342
+ spawned = true;
4343
+ });
4314
4344
  child.on("error", (err) => {
4315
4345
  log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
4316
4346
  if (this.child === child)
4317
4347
  this.child = null;
4318
4348
  });
4319
4349
  child.on("exit", () => {
4320
- if (this.child === child)
4321
- this.child = null;
4350
+ if (this.child !== child)
4351
+ return;
4352
+ this.child = null;
4353
+ if (spawned && this.holds > 0)
4354
+ this.start();
4322
4355
  });
4323
4356
  child.unref();
4324
4357
  this.child = child;
@@ -4376,7 +4409,7 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
4376
4409
  const successorId = link.target_card.id;
4377
4410
  try {
4378
4411
  const { card } = await deps.client.getCard(successorId);
4379
- if (card.assignee_id !== deps.agentUserId) {
4412
+ if (card.assigned_agent_id !== deps.agentId) {
4380
4413
  log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4381
4414
  continue;
4382
4415
  }
@@ -4653,6 +4686,7 @@ var init_prompt = __esm(() => {
4653
4686
  class Worker {
4654
4687
  config;
4655
4688
  client;
4689
+ agentId;
4656
4690
  onDone;
4657
4691
  workspaceId;
4658
4692
  projectId;
@@ -4674,9 +4708,10 @@ class Worker {
4674
4708
  verificationFailed = false;
4675
4709
  sessionId = null;
4676
4710
  runId = null;
4677
- constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4711
+ constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4678
4712
  this.config = config;
4679
4713
  this.client = client;
4714
+ this.agentId = agentId;
4680
4715
  this.onDone = onDone;
4681
4716
  this.workspaceId = workspaceId;
4682
4717
  this.projectId = projectId;
@@ -4757,6 +4792,7 @@ class Worker {
4757
4792
  const { session } = await this.client.startAgentSession(card.id, {
4758
4793
  agentIdentifier: agentIdentifier(this.id),
4759
4794
  agentName: AGENT_NAME,
4795
+ agentId: this.agentId,
4760
4796
  status: "working",
4761
4797
  currentTask: "Setting up worktree",
4762
4798
  progressPercent: 5
@@ -4801,6 +4837,10 @@ class Worker {
4801
4837
  await this.spawnClaude(prompt, card, subtasks);
4802
4838
  if (this.aborted)
4803
4839
  return;
4840
+ if (this.timeoutTimer) {
4841
+ clearTimeout(this.timeoutTimer);
4842
+ this.timeoutTimer = null;
4843
+ }
4804
4844
  this.state = "verifying";
4805
4845
  await this.recordPhase("verifying");
4806
4846
  log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
@@ -4843,6 +4883,14 @@ class Worker {
4843
4883
  } catch {}
4844
4884
  await this.recordOutcome(card.id, "success");
4845
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
+ }
4846
4894
  try {
4847
4895
  await runTransition(this.client, card, {
4848
4896
  move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
@@ -5098,24 +5146,30 @@ class Pool {
5098
5146
  client;
5099
5147
  projectId;
5100
5148
  stateStore;
5149
+ agentId;
5101
5150
  implWorkers = [];
5102
5151
  reviewWorkers = [];
5103
5152
  implQueue;
5104
5153
  reviewQueue;
5105
5154
  budget;
5106
5155
  sleepGuard = new SleepGuard;
5156
+ shuttingDown = false;
5107
5157
  onCardCompleted = null;
5108
- constructor(config, client, userEmail, workspaceId, projectId, stateStore) {
5158
+ constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
5109
5159
  this.client = client;
5110
5160
  this.projectId = projectId;
5111
5161
  this.stateStore = stateStore;
5162
+ this.agentId = agentId;
5112
5163
  this.implQueue = new PriorityQueue(config);
5113
5164
  this.reviewQueue = new PriorityQueue(config);
5114
5165
  this.budget = new BudgetGuard(config.budget, this.stateStore);
5115
5166
  for (let i = 0;i < config.poolSize; i++) {
5116
- this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
5117
- this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
5118
- this.sleepGuard.release();
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
+ }
5119
5173
  }, workspaceId, projectId, stateStore, async (completedCard) => {
5120
5174
  await this.onCardCompleted?.(completedCard);
5121
5175
  }));
@@ -5123,9 +5177,12 @@ class Pool {
5123
5177
  if (config.review.enabled) {
5124
5178
  for (let i = 0;i < config.review.poolSize; i++) {
5125
5179
  const reviewWorkerId = config.poolSize + i;
5126
- this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
5127
- this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
5128
- this.sleepGuard.release();
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
+ }
5129
5186
  }, stateStore, workspaceId, projectId));
5130
5187
  }
5131
5188
  }
@@ -5276,6 +5333,7 @@ class Pool {
5276
5333
  }
5277
5334
  async shutdown() {
5278
5335
  log.info(TAG24, "Shutting down pool...");
5336
+ this.shuttingDown = true;
5279
5337
  const active = [
5280
5338
  ...this.implWorkers.filter((w) => w.isActive),
5281
5339
  ...this.reviewWorkers.filter((w) => w.isActive)
@@ -5286,6 +5344,8 @@ class Pool {
5286
5344
  }
5287
5345
  cardDataCache = new Map;
5288
5346
  tryDispatchFor(workers, queue, label) {
5347
+ if (this.shuttingDown)
5348
+ return false;
5289
5349
  const idle = workers.find((w) => w.isIdle);
5290
5350
  if (!idle) {
5291
5351
  log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
@@ -5433,7 +5493,7 @@ class Reconciler {
5433
5493
  client;
5434
5494
  pool;
5435
5495
  projectId;
5436
- agentUserId;
5496
+ agentId;
5437
5497
  pickupColumns;
5438
5498
  reviewColumns;
5439
5499
  approvedLabel;
@@ -5448,11 +5508,11 @@ class Reconciler {
5448
5508
  get isRunning() {
5449
5509
  return this.timer !== null;
5450
5510
  }
5451
- 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) {
5452
5512
  this.client = client;
5453
5513
  this.pool = pool;
5454
5514
  this.projectId = projectId;
5455
- this.agentUserId = agentUserId;
5515
+ this.agentId = agentId;
5456
5516
  this.pickupColumns = pickupColumns;
5457
5517
  this.reviewColumns = reviewColumns;
5458
5518
  this.approvedLabel = approvedLabel;
@@ -5510,9 +5570,9 @@ class Reconciler {
5510
5570
  }
5511
5571
  const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5512
5572
  const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5513
- 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)));
5514
5574
  const knownCardIds = this.pool.knownCardIds();
5515
- 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));
5516
5576
  for (const card of assignedCards) {
5517
5577
  if (!knownCardIds.has(card.id)) {
5518
5578
  const column = columnMap.get(card.column_id);
@@ -5897,6 +5957,7 @@ class Watcher {
5897
5957
  daemonId: this.daemonId,
5898
5958
  startedAt: new Date().toISOString(),
5899
5959
  userId: this.identity.userId,
5960
+ agentId: this.identity.agentId,
5900
5961
  userEmail: this.identity.userEmail,
5901
5962
  agentIdentifier: this.identity.agentIdentifier,
5902
5963
  agentName: this.identity.agentName
@@ -6233,20 +6294,27 @@ async function main() {
6233
6294
  const errored = outcomes.filter((o) => o.errors.length).length;
6234
6295
  banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
6235
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})`);
6236
6304
  const realtimeCreds = await fetchRealtimeCredentials(client);
6237
6305
  banner.check("Realtime credentials");
6238
- 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);
6239
6307
  const promoteSuccessors = async (completedCard) => {
6240
6308
  await promoteUnblockedSuccessors(completedCard, {
6241
6309
  client,
6242
- agentUserId,
6243
- enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
6310
+ agentId,
6311
+ enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
6244
6312
  });
6245
6313
  };
6246
6314
  pool.onCardCompleted = promoteSuccessors;
6247
6315
  const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
6248
6316
  const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
6249
- 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);
6250
6318
  let mergeMonitor = null;
6251
6319
  if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
6252
6320
  mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
@@ -6305,11 +6373,12 @@ async function main() {
6305
6373
  }) : null;
6306
6374
  const watcher = new Watcher(realtimeCreds, config.projectId, {
6307
6375
  userId: agentUserId,
6376
+ agentId,
6308
6377
  userEmail: config.userEmail,
6309
- agentIdentifier: "harmony-daemon",
6310
- agentName: AGENT_NAME
6378
+ agentIdentifier: config.agentIdentifier,
6379
+ agentName: config.agentName
6311
6380
  }, async (event) => {
6312
- await handleBroadcast(event, client, pool, config, agentUserId);
6381
+ await handleBroadcast(event, client, pool, config, agentId);
6313
6382
  }, async (command) => {
6314
6383
  await pool.handleAgentCommand(command.cardId, command.command);
6315
6384
  });
@@ -6375,18 +6444,18 @@ async function main() {
6375
6444
  await banner.ready("watching for card assignments");
6376
6445
  watcher.allowStartupLogs();
6377
6446
  }
6378
- async function handleBroadcast(event, client, pool, config, agentUserId) {
6447
+ async function handleBroadcast(event, client, pool, config, agentId) {
6379
6448
  const payload = event.payload;
6380
6449
  const cardId = payload.card_id;
6381
6450
  if (!cardId)
6382
6451
  return;
6383
- const assigneeId = payload.assignee_id;
6384
- if (assigneeId === undefined)
6452
+ const assignedAgentId = payload.assigned_agent_id;
6453
+ if (assignedAgentId === undefined)
6385
6454
  return;
6386
- if (assigneeId === agentUserId) {
6455
+ if (assignedAgentId === agentId) {
6387
6456
  log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
6388
6457
  try {
6389
- await tryEnqueueCard(cardId, client, pool, config);
6458
+ await tryEnqueueCard(cardId, client, pool, config, agentId);
6390
6459
  } catch (err) {
6391
6460
  log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6392
6461
  }
@@ -6395,8 +6464,12 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
6395
6464
  await pool.removeCard(cardId);
6396
6465
  }
6397
6466
  }
6398
- async function tryEnqueueCard(cardId, client, pool, config) {
6467
+ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6399
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
+ }
6400
6473
  const board = await client.getBoard(config.projectId, { summary: true });
6401
6474
  const columns = board.columns;
6402
6475
  const column = columns.find((c) => c.id === card.column_id);
@@ -6440,7 +6513,6 @@ var init_src = __esm(() => {
6440
6513
  init_startup_banner();
6441
6514
  init_state_store();
6442
6515
  init_stream_parser_selftest();
6443
- init_types();
6444
6516
  init_unblock();
6445
6517
  init_watcher();
6446
6518
  init_worktree_gc();
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;
@@ -3758,9 +3778,10 @@ class ReviewWorker {
3758
3778
  runId = null;
3759
3779
  lastRunLogPath = null;
3760
3780
  sessionId = null;
3761
- constructor(id, config, client, _userEmail, onDone, stateStore, workspaceId, _projectId) {
3781
+ constructor(id, config, client, agentId, onDone, stateStore, workspaceId, _projectId) {
3762
3782
  this.config = config;
3763
3783
  this.client = client;
3784
+ this.agentId = agentId;
3764
3785
  this.onDone = onDone;
3765
3786
  this.stateStore = stateStore;
3766
3787
  this.workspaceId = workspaceId;
@@ -3859,6 +3880,7 @@ class ReviewWorker {
3859
3880
  const { session: reviewSession } = await this.client.startAgentSession(card.id, {
3860
3881
  agentIdentifier: agentIdentifier(this.id),
3861
3882
  agentName: `${AGENT_NAME} (Review)`,
3883
+ agentId: this.agentId,
3862
3884
  status: "working",
3863
3885
  currentTask: localMode ? "Reviewing local changes" : "Setting up review worktree",
3864
3886
  progressPercent: 5
@@ -3959,6 +3981,10 @@ class ReviewWorker {
3959
3981
  const sessionStats = this.lastSessionStats;
3960
3982
  if (this.aborted)
3961
3983
  return;
3984
+ if (this.timeoutTimer) {
3985
+ clearTimeout(this.timeoutTimer);
3986
+ this.timeoutTimer = null;
3987
+ }
3962
3988
  this.state = "completing";
3963
3989
  await this.recordPhase("completing");
3964
3990
  log.info(this.tag, `Claude review finished for #${card.short_id}`);
@@ -4310,14 +4336,21 @@ class SleepGuard {
4310
4336
  const child = spawn3("caffeinate", ["-i", "-w", String(process.pid)], {
4311
4337
  stdio: "ignore"
4312
4338
  });
4339
+ let spawned = false;
4340
+ child.on("spawn", () => {
4341
+ spawned = true;
4342
+ });
4313
4343
  child.on("error", (err) => {
4314
4344
  log.warn(TAG20, `caffeinate unavailable: ${err.message}`);
4315
4345
  if (this.child === child)
4316
4346
  this.child = null;
4317
4347
  });
4318
4348
  child.on("exit", () => {
4319
- if (this.child === child)
4320
- this.child = null;
4349
+ if (this.child !== child)
4350
+ return;
4351
+ this.child = null;
4352
+ if (spawned && this.holds > 0)
4353
+ this.start();
4321
4354
  });
4322
4355
  child.unref();
4323
4356
  this.child = child;
@@ -4375,7 +4408,7 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
4375
4408
  const successorId = link.target_card.id;
4376
4409
  try {
4377
4410
  const { card } = await deps.client.getCard(successorId);
4378
- if (card.assignee_id !== deps.agentUserId) {
4411
+ if (card.assigned_agent_id !== deps.agentId) {
4379
4412
  log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4380
4413
  continue;
4381
4414
  }
@@ -4652,6 +4685,7 @@ var init_prompt = __esm(() => {
4652
4685
  class Worker {
4653
4686
  config;
4654
4687
  client;
4688
+ agentId;
4655
4689
  onDone;
4656
4690
  workspaceId;
4657
4691
  projectId;
@@ -4673,9 +4707,10 @@ class Worker {
4673
4707
  verificationFailed = false;
4674
4708
  sessionId = null;
4675
4709
  runId = null;
4676
- constructor(id, config, client, _userEmail, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4710
+ constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
4677
4711
  this.config = config;
4678
4712
  this.client = client;
4713
+ this.agentId = agentId;
4679
4714
  this.onDone = onDone;
4680
4715
  this.workspaceId = workspaceId;
4681
4716
  this.projectId = projectId;
@@ -4756,6 +4791,7 @@ class Worker {
4756
4791
  const { session } = await this.client.startAgentSession(card.id, {
4757
4792
  agentIdentifier: agentIdentifier(this.id),
4758
4793
  agentName: AGENT_NAME,
4794
+ agentId: this.agentId,
4759
4795
  status: "working",
4760
4796
  currentTask: "Setting up worktree",
4761
4797
  progressPercent: 5
@@ -4800,6 +4836,10 @@ class Worker {
4800
4836
  await this.spawnClaude(prompt, card, subtasks);
4801
4837
  if (this.aborted)
4802
4838
  return;
4839
+ if (this.timeoutTimer) {
4840
+ clearTimeout(this.timeoutTimer);
4841
+ this.timeoutTimer = null;
4842
+ }
4803
4843
  this.state = "verifying";
4804
4844
  await this.recordPhase("verifying");
4805
4845
  log.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
@@ -4842,6 +4882,14 @@ class Worker {
4842
4882
  } catch {}
4843
4883
  await this.recordOutcome(card.id, "success");
4844
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
+ }
4845
4893
  try {
4846
4894
  await runTransition(this.client, card, {
4847
4895
  move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
@@ -5097,24 +5145,30 @@ class Pool {
5097
5145
  client;
5098
5146
  projectId;
5099
5147
  stateStore;
5148
+ agentId;
5100
5149
  implWorkers = [];
5101
5150
  reviewWorkers = [];
5102
5151
  implQueue;
5103
5152
  reviewQueue;
5104
5153
  budget;
5105
5154
  sleepGuard = new SleepGuard;
5155
+ shuttingDown = false;
5106
5156
  onCardCompleted = null;
5107
- constructor(config, client, userEmail, workspaceId, projectId, stateStore) {
5157
+ constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
5108
5158
  this.client = client;
5109
5159
  this.projectId = projectId;
5110
5160
  this.stateStore = stateStore;
5161
+ this.agentId = agentId;
5111
5162
  this.implQueue = new PriorityQueue(config);
5112
5163
  this.reviewQueue = new PriorityQueue(config);
5113
5164
  this.budget = new BudgetGuard(config.budget, this.stateStore);
5114
5165
  for (let i = 0;i < config.poolSize; i++) {
5115
- this.implWorkers.push(new Worker(i, config, client, userEmail, () => {
5116
- this.tryDispatchFor(this.implWorkers, this.implQueue, "impl");
5117
- this.sleepGuard.release();
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
+ }
5118
5172
  }, workspaceId, projectId, stateStore, async (completedCard) => {
5119
5173
  await this.onCardCompleted?.(completedCard);
5120
5174
  }));
@@ -5122,9 +5176,12 @@ class Pool {
5122
5176
  if (config.review.enabled) {
5123
5177
  for (let i = 0;i < config.review.poolSize; i++) {
5124
5178
  const reviewWorkerId = config.poolSize + i;
5125
- this.reviewWorkers.push(new ReviewWorker(reviewWorkerId, config, client, userEmail, () => {
5126
- this.tryDispatchFor(this.reviewWorkers, this.reviewQueue, "review");
5127
- this.sleepGuard.release();
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
+ }
5128
5185
  }, stateStore, workspaceId, projectId));
5129
5186
  }
5130
5187
  }
@@ -5275,6 +5332,7 @@ class Pool {
5275
5332
  }
5276
5333
  async shutdown() {
5277
5334
  log.info(TAG24, "Shutting down pool...");
5335
+ this.shuttingDown = true;
5278
5336
  const active = [
5279
5337
  ...this.implWorkers.filter((w) => w.isActive),
5280
5338
  ...this.reviewWorkers.filter((w) => w.isActive)
@@ -5285,6 +5343,8 @@ class Pool {
5285
5343
  }
5286
5344
  cardDataCache = new Map;
5287
5345
  tryDispatchFor(workers, queue, label) {
5346
+ if (this.shuttingDown)
5347
+ return false;
5288
5348
  const idle = workers.find((w) => w.isIdle);
5289
5349
  if (!idle) {
5290
5350
  log.debug(TAG24, `No idle ${label} workers (queue: ${queue.length})`);
@@ -5432,7 +5492,7 @@ class Reconciler {
5432
5492
  client;
5433
5493
  pool;
5434
5494
  projectId;
5435
- agentUserId;
5495
+ agentId;
5436
5496
  pickupColumns;
5437
5497
  reviewColumns;
5438
5498
  approvedLabel;
@@ -5447,11 +5507,11 @@ class Reconciler {
5447
5507
  get isRunning() {
5448
5508
  return this.timer !== null;
5449
5509
  }
5450
- 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) {
5451
5511
  this.client = client;
5452
5512
  this.pool = pool;
5453
5513
  this.projectId = projectId;
5454
- this.agentUserId = agentUserId;
5514
+ this.agentId = agentId;
5455
5515
  this.pickupColumns = pickupColumns;
5456
5516
  this.reviewColumns = reviewColumns;
5457
5517
  this.approvedLabel = approvedLabel;
@@ -5509,9 +5569,9 @@ class Reconciler {
5509
5569
  }
5510
5570
  const pickupColumnIds = new Set(columns.filter((c) => this.pickupColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5511
5571
  const reviewColumnIds = new Set(columns.filter((c) => this.reviewColumns.some((name) => name.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
5512
- 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)));
5513
5573
  const knownCardIds = this.pool.knownCardIds();
5514
- 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));
5515
5575
  for (const card of assignedCards) {
5516
5576
  if (!knownCardIds.has(card.id)) {
5517
5577
  const column = columnMap.get(card.column_id);
@@ -5896,6 +5956,7 @@ class Watcher {
5896
5956
  daemonId: this.daemonId,
5897
5957
  startedAt: new Date().toISOString(),
5898
5958
  userId: this.identity.userId,
5959
+ agentId: this.identity.agentId,
5899
5960
  userEmail: this.identity.userEmail,
5900
5961
  agentIdentifier: this.identity.agentIdentifier,
5901
5962
  agentName: this.identity.agentName
@@ -6232,20 +6293,27 @@ async function main() {
6232
6293
  const errored = outcomes.filter((o) => o.errors.length).length;
6233
6294
  banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
6234
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})`);
6235
6303
  const realtimeCreds = await fetchRealtimeCredentials(client);
6236
6304
  banner.check("Realtime credentials");
6237
- 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);
6238
6306
  const promoteSuccessors = async (completedCard) => {
6239
6307
  await promoteUnblockedSuccessors(completedCard, {
6240
6308
  client,
6241
- agentUserId,
6242
- enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config)
6309
+ agentId,
6310
+ enqueue: (cardId) => tryEnqueueCard(cardId, client, pool, config, agentId)
6243
6311
  });
6244
6312
  };
6245
6313
  pool.onCardCompleted = promoteSuccessors;
6246
6314
  const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
6247
6315
  const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
6248
- 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);
6249
6317
  let mergeMonitor = null;
6250
6318
  if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
6251
6319
  mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
@@ -6304,11 +6372,12 @@ async function main() {
6304
6372
  }) : null;
6305
6373
  const watcher = new Watcher(realtimeCreds, config.projectId, {
6306
6374
  userId: agentUserId,
6375
+ agentId,
6307
6376
  userEmail: config.userEmail,
6308
- agentIdentifier: "harmony-daemon",
6309
- agentName: AGENT_NAME
6377
+ agentIdentifier: config.agentIdentifier,
6378
+ agentName: config.agentName
6310
6379
  }, async (event) => {
6311
- await handleBroadcast(event, client, pool, config, agentUserId);
6380
+ await handleBroadcast(event, client, pool, config, agentId);
6312
6381
  }, async (command) => {
6313
6382
  await pool.handleAgentCommand(command.cardId, command.command);
6314
6383
  });
@@ -6374,18 +6443,18 @@ async function main() {
6374
6443
  await banner.ready("watching for card assignments");
6375
6444
  watcher.allowStartupLogs();
6376
6445
  }
6377
- async function handleBroadcast(event, client, pool, config, agentUserId) {
6446
+ async function handleBroadcast(event, client, pool, config, agentId) {
6378
6447
  const payload = event.payload;
6379
6448
  const cardId = payload.card_id;
6380
6449
  if (!cardId)
6381
6450
  return;
6382
- const assigneeId = payload.assignee_id;
6383
- if (assigneeId === undefined)
6451
+ const assignedAgentId = payload.assigned_agent_id;
6452
+ if (assignedAgentId === undefined)
6384
6453
  return;
6385
- if (assigneeId === agentUserId) {
6454
+ if (assignedAgentId === agentId) {
6386
6455
  log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
6387
6456
  try {
6388
- await tryEnqueueCard(cardId, client, pool, config);
6457
+ await tryEnqueueCard(cardId, client, pool, config, agentId);
6389
6458
  } catch (err) {
6390
6459
  log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6391
6460
  }
@@ -6394,8 +6463,12 @@ async function handleBroadcast(event, client, pool, config, agentUserId) {
6394
6463
  await pool.removeCard(cardId);
6395
6464
  }
6396
6465
  }
6397
- async function tryEnqueueCard(cardId, client, pool, config) {
6466
+ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6398
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
+ }
6399
6472
  const board = await client.getBoard(config.projectId, { summary: true });
6400
6473
  const columns = board.columns;
6401
6474
  const column = columns.find((c) => c.id === card.column_id);
@@ -6439,7 +6512,6 @@ var init_src = __esm(() => {
6439
6512
  init_startup_banner();
6440
6513
  init_state_store();
6441
6514
  init_stream_parser_selftest();
6442
- init_types();
6443
6515
  init_unblock();
6444
6516
  init_watcher();
6445
6517
  init_worktree_gc();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",