@gethmy/agent 1.9.1 → 1.10.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 +747 -109
  2. package/dist/index.js +742 -108
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -219,12 +219,170 @@ var init_board_helpers = __esm(() => {
219
219
  init_log();
220
220
  });
221
221
 
222
+ // src/plan-phase.ts
223
+ function scoreComplexity(enriched) {
224
+ const { card, labels, subtasks } = enriched;
225
+ let score = 0;
226
+ const desc = (card.description ?? "").trim();
227
+ if (desc.length > 600)
228
+ score += 3;
229
+ else if (desc.length > 200)
230
+ score += 2;
231
+ else if (desc.length > 0)
232
+ score += 1;
233
+ score += Math.min(subtasks.length, 4);
234
+ const names = labels.map((l) => l.name.toLowerCase());
235
+ if (names.some((n) => /feature|epic|refactor|architecture|migration/.test(n))) {
236
+ score += 2;
237
+ }
238
+ if (names.some((n) => /typo|chore|trivial|docs/.test(n))) {
239
+ score -= 2;
240
+ }
241
+ return Math.max(0, score);
242
+ }
243
+ function shouldPlan(enriched, config) {
244
+ if (!config.enabled)
245
+ return false;
246
+ const { card } = enriched;
247
+ const hasPlan = !!card.plan_id;
248
+ const needsRefresh = card.needs_plan_refresh === true;
249
+ if (hasPlan && !needsRefresh)
250
+ return false;
251
+ return scoreComplexity(enriched) >= config.minComplexityScore;
252
+ }
253
+ function buildPlanPrompt(enriched, worktreePath) {
254
+ const { card, column, labels, subtasks } = enriched;
255
+ const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
256
+ const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- ${s.title}`).join(`
257
+ `) : "No subtasks defined.";
258
+ const description = card.description?.trim() || "No description provided.";
259
+ return `You are a senior engineer producing an IMPLEMENTATION PLAN for a task on the Harmony board. You are in PLAN MODE: explore the codebase to ground the plan, but do NOT write, edit, or commit any code in this pass.
260
+
261
+ ## Card: #${card.short_id} - ${card.title}
262
+ **Labels**: ${labelStr}
263
+ **Column**: ${column.name}
264
+ **Priority**: ${card.priority}
265
+
266
+ ## Description
267
+ ${description}
268
+
269
+ ## Subtasks
270
+ ${subtaskStr}
271
+
272
+ ## Your job
273
+ 1. Read the parts of the codebase relevant to this task (use Read/Grep/Glob; do NOT edit).
274
+ 2. Decide the smallest correct approach. Note the exact files you expect to touch.
275
+ 3. Call out risks, unknowns, and anything that needs a human decision.
276
+ 4. Break the work into ordered, independently-verifiable tasks.
277
+
278
+ You are exploring the worktree at \`${worktreePath}\`. Read-only this pass — no Write/Edit/Bash-that-mutates, no commits.
279
+
280
+ ## Output contract
281
+ End your final message with EXACTLY ONE fenced block tagged \`plan\`, in this structure:
282
+
283
+ \`\`\`plan
284
+ # <one-line plan title>
285
+
286
+ ## Approach
287
+ <2-4 sentences: the chosen approach and why>
288
+
289
+ ## Files
290
+ - <path> — <what changes here>
291
+
292
+ ## Steps
293
+ 1. <ordered step>
294
+ 2. <ordered step>
295
+
296
+ ## Tasks
297
+ - [ ] <discrete, verifiable task>
298
+ - [ ] <discrete, verifiable task>
299
+
300
+ ## Risks
301
+ - <risk / unknown / decision needed, or "none">
302
+ \`\`\`
303
+
304
+ The \`## Tasks\` checklist is parsed into trackable tasks, so keep each line a single concrete action.`;
305
+ }
306
+ function extractPlanArtifact(assistantText, fallbackTitle) {
307
+ const text = assistantText ?? "";
308
+ const fenced = text.match(PLAN_FENCE);
309
+ const markdown = (fenced ? fenced[1] : text).trim();
310
+ const titleMatch = markdown.match(H1);
311
+ const title = (titleMatch?.[1] ?? fallbackTitle).trim() || fallbackTitle;
312
+ return {
313
+ title,
314
+ markdown,
315
+ tasks: parseTasksSection(markdown)
316
+ };
317
+ }
318
+ function parseTasksSection(markdown) {
319
+ const lines = markdown.split(`
320
+ `);
321
+ const tasks = [];
322
+ let inTasks = false;
323
+ for (const line of lines) {
324
+ const heading = line.match(/^#{1,6}\s+(.+?)\s*$/);
325
+ if (heading) {
326
+ inTasks = /^tasks\b/i.test(heading[1].trim());
327
+ continue;
328
+ }
329
+ if (!inTasks)
330
+ continue;
331
+ const item = line.match(TASK_LINE);
332
+ if (item) {
333
+ const content = item[1].trim();
334
+ if (content)
335
+ tasks.push({ content });
336
+ }
337
+ }
338
+ return tasks;
339
+ }
340
+ function buildPlanComment(artifact) {
341
+ const body = artifact.markdown.trim();
342
+ return [
343
+ "## \uD83E\uDDED Plan (agent, advisory)",
344
+ "",
345
+ "The daemon explored the worktree read-only and produced this plan before implementing. Implementation is starting now in the same run.",
346
+ "",
347
+ body
348
+ ].join(`
349
+ `);
350
+ }
351
+ function buildGatedPlanComment(artifact, pickupColumnName) {
352
+ const body = artifact.markdown.trim();
353
+ return [
354
+ "## \uD83E\uDDED Plan (agent, awaiting approval)",
355
+ "",
356
+ `The daemon explored the worktree read-only and produced this plan. Implementation is **gated on your approval** — review the plan below, then move this card to **${pickupColumnName}** to start implementation with it. Edit the linked plan first if the approach needs changes.`,
357
+ "",
358
+ body
359
+ ].join(`
360
+ `);
361
+ }
362
+ var DEFAULT_PLANNING_CONFIG, PLAN_FENCE, H1, TASK_LINE;
363
+ var init_plan_phase = __esm(() => {
364
+ DEFAULT_PLANNING_CONFIG = {
365
+ enabled: false,
366
+ mode: "advisory",
367
+ model: "sonnet",
368
+ maxTurns: 40,
369
+ postComment: true,
370
+ awaitingApprovalColumn: "To Do",
371
+ minComplexityScore: 3,
372
+ approvalTtlHours: 0
373
+ };
374
+ PLAN_FENCE = /```plan\s*\n([\s\S]*?)```/i;
375
+ H1 = /^#\s+(.+?)\s*$/m;
376
+ TASK_LINE = /^\s*(?:[-*]\s*\[[ xX]?\]|[-*]|\d+[.)])\s+(.+?)\s*$/;
377
+ });
378
+
222
379
  // src/types.ts
223
380
  function agentIdentifier(workerId) {
224
381
  return `harmony-daemon-${workerId}`;
225
382
  }
226
- var DEFAULT_AGENT_CONFIG, NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
383
+ var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent";
227
384
  var init_types = __esm(() => {
385
+ init_plan_phase();
228
386
  DEFAULT_AGENT_CONFIG = {
229
387
  poolSize: 3,
230
388
  maxTimeout: 1800000,
@@ -291,7 +449,8 @@ var init_types = __esm(() => {
291
449
  staleHeartbeatMs: 120000,
292
450
  reconcileIntervalMs: 60000,
293
451
  worktreeGcIntervalMs: 5 * 60000
294
- }
452
+ },
453
+ planning: DEFAULT_PLANNING_CONFIG
295
454
  };
296
455
  });
297
456
 
@@ -387,6 +546,10 @@ function loadDaemonConfig() {
387
546
  timing: {
388
547
  ...DEFAULT_AGENT_CONFIG.timing,
389
548
  ...agentOverrides.timing ?? {}
549
+ },
550
+ planning: {
551
+ ...DEFAULT_AGENT_CONFIG.planning,
552
+ ...agentOverrides.planning ?? {}
390
553
  }
391
554
  };
392
555
  return {
@@ -446,6 +609,20 @@ async function validateColumnReferences(client, projectId, config) {
446
609
  }
447
610
  required.push({ value: config.review.moveToColumn, where: "review.moveToColumn" }, { value: config.review.failColumn, where: "review.failColumn" });
448
611
  }
612
+ if (config.planning.enabled && config.planning.mode === "gated") {
613
+ required.push({
614
+ value: config.planning.awaitingApprovalColumn,
615
+ where: "planning.awaitingApprovalColumn"
616
+ });
617
+ const parkCol = config.planning.awaitingApprovalColumn?.toLowerCase();
618
+ const allPickups = [
619
+ ...config.pickupColumns,
620
+ ...config.review.enabled ? config.review.pickupColumns : []
621
+ ];
622
+ if (parkCol && allPickups.some((c) => c.toLowerCase() === parkCol)) {
623
+ issues.push(`planning.awaitingApprovalColumn: "${config.planning.awaitingApprovalColumn}" is also a pickup column (implement or review) — a gated card parked there is picked up immediately, bypassing approval. Use a column the daemon does not pick up from.`);
624
+ }
625
+ }
449
626
  for (const { value, where } of required) {
450
627
  if (!value)
451
628
  continue;
@@ -840,24 +1017,63 @@ import {
840
1017
  class HttpServer {
841
1018
  opts;
842
1019
  server = null;
1020
+ boundPort = null;
843
1021
  constructor(opts) {
844
1022
  this.opts = opts;
845
1023
  }
1024
+ get port() {
1025
+ return this.boundPort;
1026
+ }
846
1027
  async start() {
847
- return new Promise((resolve, reject) => {
848
- this.server = createServer((req, res) => {
849
- this.route(req, res).catch((err) => {
850
- log.error(TAG3, `unhandled: ${err instanceof Error ? err.message : err}`);
851
- if (!res.headersSent) {
852
- res.writeHead(500, { "content-type": "application/json" });
853
- res.end(JSON.stringify({ error: "internal_error" }));
854
- }
855
- });
1028
+ this.server = createServer((req, res) => {
1029
+ this.route(req, res).catch((err) => {
1030
+ log.error(TAG3, `unhandled: ${err instanceof Error ? err.message : err}`);
1031
+ if (!res.headersSent) {
1032
+ res.writeHead(500, { "content-type": "application/json" });
1033
+ res.end(JSON.stringify({ error: "internal_error" }));
1034
+ }
856
1035
  });
857
- this.server.once("error", reject);
858
- this.server.listen(this.opts.port, this.opts.bindAddr, () => {
1036
+ });
1037
+ const attempts = Math.max(1, this.opts.maxPortAttempts ?? 10);
1038
+ const startPort = this.opts.port;
1039
+ for (let i = 0;i < attempts; i++) {
1040
+ const port = startPort + i;
1041
+ try {
1042
+ await this.listenOnce(port);
1043
+ this.boundPort = port;
1044
+ if (port !== startPort) {
1045
+ log.info(TAG3, `port ${startPort} busy — bound to ${port} instead`);
1046
+ }
1047
+ return port;
1048
+ } catch (err) {
1049
+ const lastAttempt = i === attempts - 1;
1050
+ if (isAddrInUse(err) && !lastAttempt) {
1051
+ log.debug(TAG3, `port ${port} in use, trying ${port + 1}`);
1052
+ continue;
1053
+ }
1054
+ throw err;
1055
+ }
1056
+ }
1057
+ throw new Error("HTTP server failed to bind");
1058
+ }
1059
+ listenOnce(port) {
1060
+ return new Promise((resolve, reject) => {
1061
+ const server = this.server;
1062
+ if (!server) {
1063
+ reject(new Error("server not created"));
1064
+ return;
1065
+ }
1066
+ const onError = (err) => {
1067
+ server.removeListener("listening", onListening);
1068
+ reject(err);
1069
+ };
1070
+ const onListening = () => {
1071
+ server.removeListener("error", onError);
859
1072
  resolve();
860
- });
1073
+ };
1074
+ server.once("error", onError);
1075
+ server.once("listening", onListening);
1076
+ server.listen(port, this.opts.bindAddr);
861
1077
  });
862
1078
  }
863
1079
  async stop() {
@@ -867,6 +1083,7 @@ class HttpServer {
867
1083
  this.server?.close(() => resolve());
868
1084
  });
869
1085
  this.server = null;
1086
+ this.boundPort = null;
870
1087
  }
871
1088
  async route(req, res) {
872
1089
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -913,6 +1130,9 @@ class HttpServer {
913
1130
  }
914
1131
  }
915
1132
  }
1133
+ function isAddrInUse(err) {
1134
+ return typeof err === "object" && err !== null && err.code === "EADDRINUSE";
1135
+ }
916
1136
  function parseCommand(path) {
917
1137
  const match = path.match(/^\/(pause|resume|stop)\/([^/]+)$/);
918
1138
  if (!match)
@@ -1373,6 +1593,62 @@ function formatCents(cents) {
1373
1593
  return `$${(cents / 100).toFixed(2)}`;
1374
1594
  }
1375
1595
 
1596
+ // src/error-classifier.ts
1597
+ function parseRetryAfterMs(message) {
1598
+ const match = message.match(/retry[- ]?after["':\s]+(\d+)/i);
1599
+ if (!match)
1600
+ return;
1601
+ const seconds = Number(match[1]);
1602
+ if (!Number.isFinite(seconds) || seconds <= 0)
1603
+ return;
1604
+ return seconds * 1000;
1605
+ }
1606
+ function classifyRunError(message) {
1607
+ if (!message)
1608
+ return { kind: null };
1609
+ const retryAfterMs = parseRetryAfterMs(message);
1610
+ if (AUTH.test(message))
1611
+ return { kind: "auth", retryAfterMs };
1612
+ if (OUT_OF_CREDITS.test(message))
1613
+ return { kind: "out_of_credits", retryAfterMs };
1614
+ if (USAGE_LIMIT.test(message))
1615
+ return { kind: "usage_limit", retryAfterMs };
1616
+ if (RATE_LIMIT.test(message))
1617
+ return { kind: "rate_limit", retryAfterMs };
1618
+ return { kind: null };
1619
+ }
1620
+ function describeApiError(kind) {
1621
+ switch (kind) {
1622
+ case "auth":
1623
+ return "Anthropic auth error — agent paused, check API credentials";
1624
+ case "out_of_credits":
1625
+ return "Anthropic credit balance too low — retrying after top-up";
1626
+ case "usage_limit":
1627
+ return "Anthropic usage limit reached — retrying after reset";
1628
+ case "rate_limit":
1629
+ return "Anthropic rate limit hit — retrying shortly";
1630
+ }
1631
+ }
1632
+ function cooldownMsFor(kind) {
1633
+ switch (kind) {
1634
+ case "rate_limit":
1635
+ return 60000;
1636
+ case "usage_limit":
1637
+ return 15 * 60000;
1638
+ case "out_of_credits":
1639
+ return 30 * 60000;
1640
+ case "auth":
1641
+ return 30 * 60000;
1642
+ }
1643
+ }
1644
+ var AUTH, OUT_OF_CREDITS, USAGE_LIMIT, RATE_LIMIT;
1645
+ var init_error_classifier = __esm(() => {
1646
+ AUTH = /\b401\b|invalid x-api-key|authentication_error|unauthorized|oauth token (?:has )?expired|please run .*login|invalid bearer token/i;
1647
+ OUT_OF_CREDITS = /\b402\b|credit balance is too low|insufficient (?:funds|credit|balance)|billing|payment required|purchase more credits/i;
1648
+ USAGE_LIMIT = /usage limit|daily limit|monthly limit|quota (?:exceeded|reached)|reached your .{0,20}limit|usage_limit_reached|limit will reset/i;
1649
+ RATE_LIMIT = /\b429\b|\b529\b|rate[ _-]?limit|too many requests|overloaded_error|"type"\s*:\s*"overloaded"/i;
1650
+ });
1651
+
1376
1652
  // src/queue.ts
1377
1653
  class PriorityQueue {
1378
1654
  config;
@@ -2385,13 +2661,15 @@ class ProgressTracker {
2385
2661
  sessionId = null;
2386
2662
  lastAssistantText = "";
2387
2663
  assistantTextBlocks = [];
2388
- constructor(client, cardId, workerId, subtasks) {
2664
+ constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
2389
2665
  this.client = client;
2390
2666
  this.cardId = cardId;
2391
2667
  this.workerId = workerId;
2392
2668
  this.subtaskTotal = subtasks.length;
2393
2669
  this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
2394
2670
  this.subtaskMode = subtasks.length > 0;
2671
+ this.phase = initialPhase;
2672
+ this.progress = PHASES[initialPhase].min;
2395
2673
  }
2396
2674
  setSessionId(id) {
2397
2675
  this.sessionId = id;
@@ -2719,6 +2997,7 @@ var init_progress_tracker = __esm(() => {
2719
2997
  GIT_COMMIT_RE = /\bgit\s+commit\b/;
2720
2998
  BUILD_CMD_RE = /\b(test|build|lint|check|tsc|vitest|jest|(?:bun|npm|pnpm|yarn) run (?:build|lint))\b/;
2721
2999
  PHASES = {
3000
+ planning: { min: 5, max: 20, label: "Planning" },
2722
3001
  exploring: { min: 10, max: 25, label: "Exploring" },
2723
3002
  implementing: { min: 25, max: 55, label: "Implementing" },
2724
3003
  testing: { min: 55, max: 65, label: "Testing" },
@@ -2726,6 +3005,7 @@ var init_progress_tracker = __esm(() => {
2726
3005
  finishing: { min: 70, max: 75, label: "Finalizing" }
2727
3006
  };
2728
3007
  PHASE_ORDER = {
3008
+ planning: -1,
2729
3009
  exploring: 0,
2730
3010
  implementing: 1,
2731
3011
  testing: 2,
@@ -3031,6 +3311,14 @@ ${finding.description}${locationLine}`
3031
3311
  `);
3032
3312
  await postReviewComment(client, card, "summary", body);
3033
3313
  }
3314
+ if (config.planning.enabled && card.plan_id) {
3315
+ try {
3316
+ await client.updateCard(card.id, { needsPlanRefresh: true });
3317
+ log.info(TAG15, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
3318
+ } catch (err) {
3319
+ log.warn(TAG15, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3320
+ }
3321
+ }
3034
3322
  await moveCardToColumn(client, card, config.review.failColumn);
3035
3323
  const failureSummary = `Review rejected (cycle ${currentCycle}/${maxCycles}): ${criticalFindings.length} critical, ${majorFindings.length} major, ${minorFindings.length} minor`;
3036
3324
  const recoveryBranch = branchName ?? undefined;
@@ -3443,6 +3731,13 @@ class StateStore {
3443
3731
  await this.persist();
3444
3732
  return rec.attempts;
3445
3733
  }
3734
+ async decrementAttempt(cardId) {
3735
+ const rec = this.getCard(cardId);
3736
+ if (!rec || rec.attempts === 0)
3737
+ return;
3738
+ rec.attempts = Math.max(0, rec.attempts - 1);
3739
+ await this.persist();
3740
+ }
3446
3741
  async recordOutcome(cardId, outcome) {
3447
3742
  const rec = this.ensureCard(cardId);
3448
3743
  rec.lastOutcome = outcome;
@@ -3752,7 +4047,6 @@ var init_transitions = __esm(() => {
3752
4047
 
3753
4048
  // src/review-worker.ts
3754
4049
  import { execFileSync as execFileSync8 } from "node:child_process";
3755
- import { createHash } from "node:crypto";
3756
4050
 
3757
4051
  class ReviewWorker {
3758
4052
  config;
@@ -3944,19 +4238,11 @@ class ReviewWorker {
3944
4238
  const systemPrompt = buildReviewSystemPrompt();
3945
4239
  const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diff, this.config.worktree.baseBranch);
3946
4240
  try {
3947
- const sessionResp = await this.client.getAgentSession(card.id);
3948
- const reviewSession2 = sessionResp.session;
3949
- const reviewSessionId = reviewSession2?.id ?? null;
3950
- const contentHash = createHash("sha256").update(systemPrompt).digest("hex");
3951
4241
  await this.client.recordPromptHistory({
3952
4242
  cardId: card.id,
3953
4243
  generatedPrompt: systemPrompt,
3954
4244
  variant: "execute",
3955
- contextIncluded: { source: "review-knowledge", mode: "review" },
3956
- sessionId: reviewSessionId,
3957
- contentHash,
3958
- templateVersion: 1,
3959
- confidence: 0.5
4245
+ contextIncluded: { source: "review-knowledge", mode: "review" }
3960
4246
  });
3961
4247
  } catch (err) {
3962
4248
  log.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
@@ -4408,8 +4694,13 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
4408
4694
  const successorId = link.target_card.id;
4409
4695
  try {
4410
4696
  const { card } = await deps.client.getCard(successorId);
4411
- if (card.assigned_agent_id !== deps.agentId) {
4412
- log.debug(TAG21, `successor #${card.short_id} not assigned to agent — skipping promotion`);
4697
+ if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
4698
+ log.info(TAG21, `successor #${card.short_id} unassigned auto-assigning to continue chain`);
4699
+ await deps.client.updateCard(successorId, {
4700
+ assignedAgentId: deps.agentId
4701
+ });
4702
+ } else {
4703
+ log.debug(TAG21, `successor #${card.short_id} assigned to different entity — skipping`);
4413
4704
  continue;
4414
4705
  }
4415
4706
  await deps.enqueue(successorId);
@@ -4691,6 +4982,7 @@ class Worker {
4691
4982
  projectId;
4692
4983
  stateStore;
4693
4984
  onCardCompleted;
4985
+ onApiError;
4694
4986
  id;
4695
4987
  state = "idle";
4696
4988
  cardId = null;
@@ -4707,7 +4999,7 @@ class Worker {
4707
4999
  verificationFailed = false;
4708
5000
  sessionId = null;
4709
5001
  runId = null;
4710
- constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
5002
+ constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
4711
5003
  this.config = config;
4712
5004
  this.client = client;
4713
5005
  this.agentId = agentId;
@@ -4716,6 +5008,7 @@ class Worker {
4716
5008
  this.projectId = projectId;
4717
5009
  this.stateStore = stateStore;
4718
5010
  this.onCardCompleted = onCardCompleted;
5011
+ this.onApiError = onApiError;
4719
5012
  this.id = id;
4720
5013
  }
4721
5014
  startHeartbeat() {
@@ -4756,7 +5049,7 @@ class Worker {
4756
5049
  return this.state === "idle";
4757
5050
  }
4758
5051
  get isActive() {
4759
- return this.state === "preparing" || this.state === "running" || this.state === "verifying" || this.state === "completing";
5052
+ return this.state === "preparing" || this.state === "planning" || this.state === "running" || this.state === "verifying" || this.state === "completing";
4760
5053
  }
4761
5054
  async run(card, column, labels, subtasks) {
4762
5055
  this.aborted = false;
@@ -4802,7 +5095,7 @@ class Worker {
4802
5095
  }
4803
5096
  this.sessionId = sid;
4804
5097
  await this.recordPhase("preparing");
4805
- const moved = await moveCardAndAddLabel(this.client, card, "In Progress", "agent");
5098
+ const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
4806
5099
  if (!moved) {
4807
5100
  log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
4808
5101
  }
@@ -4811,8 +5104,6 @@ class Worker {
4811
5104
  this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
4812
5105
  if (this.aborted)
4813
5106
  return;
4814
- this.state = "running";
4815
- await this.recordPhase("running");
4816
5107
  const enriched = {
4817
5108
  card,
4818
5109
  column,
@@ -4820,6 +5111,19 @@ class Worker {
4820
5111
  subtasks,
4821
5112
  mode: "implement"
4822
5113
  };
5114
+ if (shouldPlan(enriched, this.config.planning)) {
5115
+ this.state = "planning";
5116
+ await this.recordPhase("planning");
5117
+ const parked = await this.runPlanningPhase(enriched);
5118
+ if (this.aborted)
5119
+ return;
5120
+ if (parked) {
5121
+ log.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
5122
+ return;
5123
+ }
5124
+ }
5125
+ this.state = "running";
5126
+ await this.recordPhase("running");
4823
5127
  const prompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
4824
5128
  await this.client.updateAgentProgress(card.id, {
4825
5129
  agentIdentifier: agentIdentifier(this.id),
@@ -4858,21 +5162,46 @@ class Worker {
4858
5162
  this.state = "error";
4859
5163
  const msg = err instanceof Error ? err.message : String(err);
4860
5164
  log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
5165
+ const rawStderr = err?.stderr;
5166
+ const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
5167
+ const apiError = errClass.kind !== null;
5168
+ if (apiError) {
5169
+ try {
5170
+ this.onApiError?.(errClass);
5171
+ } catch {}
5172
+ }
5173
+ if (this.worktreePath) {
5174
+ try {
5175
+ cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
5176
+ } catch {
5177
+ log.warn(this.tag, "Failed to cleanup worktree before requeue");
5178
+ }
5179
+ this.worktreePath = null;
5180
+ }
5181
+ const failureReason = apiError ? errClass.kind : "other";
5182
+ const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
4861
5183
  try {
4862
5184
  await runTransition(this.client, card, {
5185
+ move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
4863
5186
  endSession: {
4864
- status: "paused",
5187
+ status: "failed",
5188
+ failureReason,
5189
+ failureSummary,
4865
5190
  ...buildTokenPayload(this.lastSessionStats)
4866
5191
  }
4867
5192
  });
4868
5193
  } catch (tErr) {
4869
- log.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
5194
+ log.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
4870
5195
  }
4871
5196
  if (this.runId) {
4872
5197
  try {
4873
- await this.stateStore.endRun(this.runId, "paused", msg);
5198
+ await this.stateStore.endRun(this.runId, "failed", errClass.kind ?? msg);
4874
5199
  } catch {}
4875
- await this.recordOutcome(card.id, "failure");
5200
+ if (apiError) {
5201
+ await this.stateStore.decrementAttempt(card.id);
5202
+ } else {
5203
+ await this.recordOutcome(card.id, "failure");
5204
+ }
4876
5205
  }
4877
5206
  } finally {
4878
5207
  const succeeded = this.runId && this.state !== "error" && !this.aborted && !this.verificationFailed;
@@ -5016,7 +5345,122 @@ class Worker {
5016
5345
  }
5017
5346
  }
5018
5347
  }
5019
- async spawnClaude(prompt, card, subtasks) {
5348
+ async runPlanningPhase(enriched) {
5349
+ const planning = this.config.planning;
5350
+ const { card } = enriched;
5351
+ log.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
5352
+ await this.client.updateAgentProgress(card.id, {
5353
+ agentIdentifier: agentIdentifier(this.id),
5354
+ agentName: AGENT_NAME,
5355
+ status: "working",
5356
+ currentTask: "Planning approach (read-only)",
5357
+ progressPercent: 5,
5358
+ phase: "planning"
5359
+ }).catch(() => {});
5360
+ const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
5361
+ let planTimedOut = false;
5362
+ const planTimeout = setTimeout(() => {
5363
+ planTimedOut = true;
5364
+ log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
5365
+ if (this.process && !this.process.killed) {
5366
+ terminateGroup(this.process, {
5367
+ sigintTimeoutMs: 1e4,
5368
+ sigtermTimeoutMs: 5000
5369
+ }).catch(() => {});
5370
+ }
5371
+ }, Math.min(this.config.maxTimeout, PLAN_PHASE_TIMEOUT));
5372
+ try {
5373
+ await this.spawnClaude(planPrompt, card, [], {
5374
+ model: planning.model,
5375
+ maxTurns: planning.maxTurns,
5376
+ allowedTools: PLAN_ALLOWED_TOOLS,
5377
+ initialPhase: "planning"
5378
+ });
5379
+ } catch (err) {
5380
+ log.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
5381
+ return false;
5382
+ } finally {
5383
+ clearTimeout(planTimeout);
5384
+ }
5385
+ if (this.aborted || planTimedOut)
5386
+ return false;
5387
+ const stats = this.lastSessionStats;
5388
+ if (stats?.cost) {
5389
+ const cents = Math.round(stats.cost.totalCostUsd * 100);
5390
+ if (cents > 0) {
5391
+ try {
5392
+ await this.stateStore.addCost(card.id, cents);
5393
+ } catch {}
5394
+ }
5395
+ }
5396
+ const planText = stats?.lastAssistantText ?? "";
5397
+ if (!planText.trim()) {
5398
+ log.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
5399
+ return false;
5400
+ }
5401
+ const artifact = extractPlanArtifact(planText, card.title);
5402
+ let planId = null;
5403
+ try {
5404
+ const result = await this.client.createPlan(this.projectId, {
5405
+ title: artifact.title,
5406
+ content: artifact.markdown,
5407
+ source: "agent",
5408
+ tasks: artifact.tasks
5409
+ });
5410
+ const plan = result?.plan;
5411
+ const createdId = plan && typeof plan === "object" && "id" in plan ? plan.id : null;
5412
+ if (createdId) {
5413
+ await this.client.updateCard(card.id, {
5414
+ planId: createdId,
5415
+ needsPlanRefresh: false
5416
+ });
5417
+ planId = createdId;
5418
+ }
5419
+ log.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
5420
+ } catch (err) {
5421
+ log.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
5422
+ }
5423
+ if (planning.mode === "gated" && planId) {
5424
+ try {
5425
+ if (planning.postComment) {
5426
+ await this.client.addComment(card.id, buildGatedPlanComment(artifact, planning.awaitingApprovalColumn), {
5427
+ commentType: "decision",
5428
+ agentSessionId: this.sessionId ?? undefined
5429
+ });
5430
+ }
5431
+ await runTransition(this.client, card, {
5432
+ move: { columnName: planning.awaitingApprovalColumn },
5433
+ removeLabels: ["agent"],
5434
+ endSession: {
5435
+ status: "completed",
5436
+ progressPercent: 20,
5437
+ ...buildTokenPayload(stats)
5438
+ }
5439
+ }, { store: this.stateStore, runId: this.runId ?? undefined });
5440
+ log.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
5441
+ this.lastSessionStats = undefined;
5442
+ return true;
5443
+ } catch (err) {
5444
+ log.warn(this.tag, `Gated park failed for #${card.short_id} (non-fatal, implementing directly): ${err instanceof TransitionError ? err.detail : err instanceof Error ? err.message : err}`);
5445
+ }
5446
+ }
5447
+ if (planId && planning.postComment) {
5448
+ try {
5449
+ await this.client.addComment(card.id, buildPlanComment(artifact), {
5450
+ commentType: "decision",
5451
+ agentSessionId: this.sessionId ?? undefined
5452
+ });
5453
+ } catch (err) {
5454
+ log.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
5455
+ }
5456
+ }
5457
+ return false;
5458
+ }
5459
+ async spawnClaude(prompt, card, subtasks, opts = {}) {
5460
+ const model = opts.model ?? this.config.claude.model;
5461
+ const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
5462
+ const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
5463
+ const initialPhase = opts.initialPhase ?? "exploring";
5020
5464
  return new Promise((resolve3, reject) => {
5021
5465
  const args = [
5022
5466
  "-p",
@@ -5024,11 +5468,11 @@ class Worker {
5024
5468
  "--output-format",
5025
5469
  "stream-json",
5026
5470
  "--model",
5027
- this.config.claude.model,
5471
+ model,
5028
5472
  "--max-turns",
5029
- String(this.config.claude.maxTurns),
5473
+ String(maxTurns),
5030
5474
  "--allowedTools",
5031
- "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
5475
+ allowedTools,
5032
5476
  ...this.config.claude.additionalArgs,
5033
5477
  "--",
5034
5478
  prompt
@@ -5047,7 +5491,7 @@ class Worker {
5047
5491
  stdio: ["ignore", "pipe", "pipe"]
5048
5492
  });
5049
5493
  const parser = new StreamParser;
5050
- this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
5494
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
5051
5495
  if (this.sessionId) {
5052
5496
  this.progressTracker.setSessionId(this.sessionId);
5053
5497
  }
@@ -5092,7 +5536,9 @@ class Worker {
5092
5536
  } else if (code === 0) {
5093
5537
  resolve3();
5094
5538
  } else {
5095
- reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
5539
+ const err = new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
5540
+ err.stderr = stderr;
5541
+ reject(err);
5096
5542
  }
5097
5543
  });
5098
5544
  });
@@ -5124,11 +5570,13 @@ class Worker {
5124
5570
  this.sessionId = null;
5125
5571
  }
5126
5572
  }
5127
- var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
5573
+ var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
5128
5574
  var init_worker = __esm(() => {
5129
5575
  init_board_helpers();
5130
5576
  init_completion();
5577
+ init_error_classifier();
5131
5578
  init_log();
5579
+ init_plan_phase();
5132
5580
  init_process_group();
5133
5581
  init_progress_tracker();
5134
5582
  init_prompt();
@@ -5138,6 +5586,7 @@ var init_worker = __esm(() => {
5138
5586
  init_transitions();
5139
5587
  init_types();
5140
5588
  init_worktree();
5589
+ PLAN_PHASE_TIMEOUT = 10 * 60000;
5141
5590
  });
5142
5591
 
5143
5592
  // src/pool.ts
@@ -5153,6 +5602,8 @@ class Pool {
5153
5602
  budget;
5154
5603
  sleepGuard = new SleepGuard;
5155
5604
  shuttingDown = false;
5605
+ apiCooldownUntil = 0;
5606
+ authPaused = false;
5156
5607
  onCardCompleted = null;
5157
5608
  constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
5158
5609
  this.client = client;
@@ -5171,7 +5622,7 @@ class Pool {
5171
5622
  }
5172
5623
  }, workspaceId, projectId, stateStore, async (completedCard) => {
5173
5624
  await this.onCardCompleted?.(completedCard);
5174
- }));
5625
+ }, (err) => this.noteApiError(err)));
5175
5626
  }
5176
5627
  if (config.review.enabled) {
5177
5628
  for (let i = 0;i < config.review.poolSize; i++) {
@@ -5192,6 +5643,17 @@ class Pool {
5192
5643
  return;
5193
5644
  }
5194
5645
  if (mode === "implement") {
5646
+ if (this.authPaused) {
5647
+ log.debug(TAG24, `#${card.short_id} held — agent paused (auth error)`);
5648
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
5649
+ return;
5650
+ }
5651
+ const cooldownMs = this.apiCooldownRemainingMs();
5652
+ if (cooldownMs > 0) {
5653
+ log.debug(TAG24, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
5654
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
5655
+ return;
5656
+ }
5195
5657
  const decision = this.budget.check(card.id);
5196
5658
  if (!decision.allow) {
5197
5659
  if (decision.reason === "daily_budget") {
@@ -5241,6 +5703,26 @@ class Pool {
5241
5703
  log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5242
5704
  }
5243
5705
  }
5706
+ noteApiError(err) {
5707
+ if (!err.kind)
5708
+ return;
5709
+ if (err.kind === "auth") {
5710
+ if (!this.authPaused) {
5711
+ log.error(TAG24, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
5712
+ }
5713
+ this.authPaused = true;
5714
+ return;
5715
+ }
5716
+ const cooldownMs = err.retryAfterMs ?? cooldownMsFor(err.kind);
5717
+ const until = Date.now() + cooldownMs;
5718
+ if (until > this.apiCooldownUntil) {
5719
+ this.apiCooldownUntil = until;
5720
+ log.warn(TAG24, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
5721
+ }
5722
+ }
5723
+ apiCooldownRemainingMs() {
5724
+ return Math.max(0, this.apiCooldownUntil - Date.now());
5725
+ }
5244
5726
  async removeCard(cardId) {
5245
5727
  await this.stateStore.resetAttempts(cardId);
5246
5728
  this.lastWaitingEmit.delete(cardId);
@@ -5368,6 +5850,7 @@ class Pool {
5368
5850
  }
5369
5851
  var TAG24 = "pool";
5370
5852
  var init_pool = __esm(() => {
5853
+ init_error_classifier();
5371
5854
  init_log();
5372
5855
  init_queue();
5373
5856
  init_review_worker();
@@ -5377,6 +5860,78 @@ var init_pool = __esm(() => {
5377
5860
  init_worker();
5378
5861
  });
5379
5862
 
5863
+ // src/port-registry.ts
5864
+ var exports_port_registry = {};
5865
+ __export(exports_port_registry, {
5866
+ recordDaemonPort: () => recordDaemonPort,
5867
+ lookupDaemonPort: () => lookupDaemonPort,
5868
+ defaultRegistryPath: () => defaultRegistryPath,
5869
+ clearDaemonPort: () => clearDaemonPort
5870
+ });
5871
+ import {
5872
+ existsSync as existsSync5,
5873
+ mkdirSync as mkdirSync3,
5874
+ readFileSync as readFileSync4,
5875
+ renameSync as renameSync2,
5876
+ writeFileSync as writeFileSync2
5877
+ } from "node:fs";
5878
+ import { homedir as homedir4 } from "node:os";
5879
+ import { dirname as dirname2, join as join4 } from "node:path";
5880
+ function defaultRegistryPath() {
5881
+ return join4(homedir4(), ".harmony-mcp", "agent-ports.json");
5882
+ }
5883
+ function load(path) {
5884
+ if (!existsSync5(path))
5885
+ return {};
5886
+ try {
5887
+ const raw = readFileSync4(path, "utf-8");
5888
+ const parsed = JSON.parse(raw);
5889
+ if (parsed && typeof parsed === "object")
5890
+ return parsed;
5891
+ return {};
5892
+ } catch (err) {
5893
+ log.warn(TAG25, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
5894
+ return {};
5895
+ }
5896
+ }
5897
+ function save(path, registry) {
5898
+ const dir = dirname2(path);
5899
+ if (!existsSync5(dir))
5900
+ mkdirSync3(dir, { recursive: true });
5901
+ const tmp = `${path}.tmp`;
5902
+ writeFileSync2(tmp, JSON.stringify(registry, null, 2), "utf-8");
5903
+ renameSync2(tmp, path);
5904
+ }
5905
+ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
5906
+ try {
5907
+ const registry = load(path);
5908
+ registry[projectId] = { ...entry, updatedAt: Date.now() };
5909
+ save(path, registry);
5910
+ } catch (err) {
5911
+ log.warn(TAG25, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
5912
+ }
5913
+ }
5914
+ function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
5915
+ const registry = load(path);
5916
+ return registry[projectId] ?? null;
5917
+ }
5918
+ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
5919
+ try {
5920
+ const registry = load(path);
5921
+ const existing = registry[projectId];
5922
+ if (!existing || existing.pid !== pid)
5923
+ return;
5924
+ delete registry[projectId];
5925
+ save(path, registry);
5926
+ } catch (err) {
5927
+ log.warn(TAG25, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
5928
+ }
5929
+ }
5930
+ var TAG25 = "port-registry";
5931
+ var init_port_registry = __esm(() => {
5932
+ init_log();
5933
+ });
5934
+
5380
5935
  // src/recovery.ts
5381
5936
  function isProcessAlive(pid, currentPid) {
5382
5937
  if (pid === currentPid)
@@ -5393,7 +5948,7 @@ async function fetchCardSafely(client, cardId) {
5393
5948
  const { card } = await client.getCard(cardId);
5394
5949
  return card;
5395
5950
  } catch (err) {
5396
- log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5951
+ log.warn(TAG26, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5397
5952
  return null;
5398
5953
  }
5399
5954
  }
@@ -5403,7 +5958,7 @@ async function recoverOrphans(store, client, config) {
5403
5958
  return [];
5404
5959
  }
5405
5960
  const outcomes = [];
5406
- log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
5961
+ log.info(TAG26, `recovering ${active.length} orphan run(s) from prior daemon`);
5407
5962
  for (const run of active) {
5408
5963
  const outcome = {
5409
5964
  runId: run.runId,
@@ -5415,11 +5970,11 @@ async function recoverOrphans(store, client, config) {
5415
5970
  };
5416
5971
  outcomes.push(outcome);
5417
5972
  if (isProcessAlive(run.daemonPid, process.pid)) {
5418
- log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5973
+ log.warn(TAG26, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5419
5974
  outcome.actions.push("skipped: daemon pid still alive");
5420
5975
  continue;
5421
5976
  }
5422
- log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5977
+ log.info(TAG26, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5423
5978
  await recoverRun(run, store, client, config, outcome);
5424
5979
  }
5425
5980
  return outcomes;
@@ -5437,7 +5992,7 @@ async function recoverRun(run, store, client, config, outcome) {
5437
5992
  } catch (err) {
5438
5993
  const msg = err instanceof Error ? err.message : String(err);
5439
5994
  outcome.errors.push(`endAgentSession: ${msg}`);
5440
- log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
5995
+ log.warn(TAG26, `endAgentSession failed for ${run.cardId}: ${msg}`);
5441
5996
  }
5442
5997
  const card = await fetchCardSafely(client, run.cardId);
5443
5998
  if (card) {
@@ -5478,9 +6033,9 @@ async function recoverRun(run, store, client, config, outcome) {
5478
6033
  const msg = err instanceof Error ? err.message : String(err);
5479
6034
  outcome.errors.push(`endRun: ${msg}`);
5480
6035
  }
5481
- log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
6036
+ log.info(TAG26, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5482
6037
  }
5483
- var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
6038
+ var TAG26 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5484
6039
  var init_recovery = __esm(() => {
5485
6040
  init_board_helpers();
5486
6041
  init_log();
@@ -5528,7 +6083,7 @@ class Reconciler {
5528
6083
  clearInterval(this.timer);
5529
6084
  this.timer = null;
5530
6085
  }
5531
- log.info(TAG26, "Heartbeat stopped");
6086
+ log.info(TAG27, "Heartbeat stopped");
5532
6087
  }
5533
6088
  async recoverStaleRuns() {
5534
6089
  if (!this.stateStore || !this.agentConfig)
@@ -5545,7 +6100,7 @@ class Reconciler {
5545
6100
  if (!daemonDead && !(heartbeatStale && ourZombie))
5546
6101
  continue;
5547
6102
  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`;
5548
- log.warn(TAG26, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
6103
+ log.warn(TAG27, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5549
6104
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
5550
6105
  runId: run.runId,
5551
6106
  cardId: run.cardId,
@@ -5556,6 +6111,57 @@ class Reconciler {
5556
6111
  });
5557
6112
  }
5558
6113
  }
6114
+ async recoverStrandedInProgress(cards, columns, knownCardIds) {
6115
+ const inProgressCol = columns.find((c) => c.name.toLowerCase() === IN_PROGRESS_COLUMN.toLowerCase());
6116
+ const pickupName = this.pickupColumns[0];
6117
+ const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
6118
+ if (!inProgressCol || !pickupCol)
6119
+ return;
6120
+ const graceMs = this.agentConfig?.timing.staleHeartbeatMs ?? 120000;
6121
+ const now = Date.now();
6122
+ const activeCardIds = new Set((this.stateStore?.getActiveRuns() ?? []).map((r) => r.cardId));
6123
+ for (const card of cards) {
6124
+ if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== inProgressCol.id || knownCardIds.has(card.id) || activeCardIds.has(card.id)) {
6125
+ continue;
6126
+ }
6127
+ const stalledAt = Date.parse(card.updated_at ?? "");
6128
+ if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
6129
+ continue;
6130
+ log.warn(TAG27, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
6131
+ try {
6132
+ await this.client.moveCard(card.id, pickupCol.id);
6133
+ } catch (err) {
6134
+ log.error(TAG27, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6135
+ }
6136
+ }
6137
+ }
6138
+ async releaseStalledApprovals(cards, columns, knownCardIds) {
6139
+ const planning = this.agentConfig?.planning;
6140
+ if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
6141
+ return;
6142
+ }
6143
+ const parkCol = columns.find((c) => c.name.toLowerCase() === planning.awaitingApprovalColumn.toLowerCase());
6144
+ const pickupName = this.pickupColumns[0];
6145
+ const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
6146
+ if (!parkCol || !pickupCol)
6147
+ return;
6148
+ const ttlMs = planning.approvalTtlHours * 3600000;
6149
+ const now = Date.now();
6150
+ for (const card of cards) {
6151
+ if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== parkCol.id || !card.plan_id || knownCardIds.has(card.id)) {
6152
+ continue;
6153
+ }
6154
+ const parkedAt = Date.parse(card.updated_at ?? "");
6155
+ if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
6156
+ continue;
6157
+ log.warn(TAG27, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
6158
+ try {
6159
+ await this.client.moveCard(card.id, pickupCol.id);
6160
+ } catch (err) {
6161
+ log.error(TAG27, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6162
+ }
6163
+ }
6164
+ }
5559
6165
  async tick() {
5560
6166
  this.lastTickAt = Date.now();
5561
6167
  try {
@@ -5581,37 +6187,39 @@ class Reconciler {
5581
6187
  const subtasks = card.subtasks ?? [];
5582
6188
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
5583
6189
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
5584
- log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
6190
+ log.debug(TAG27, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5585
6191
  continue;
5586
6192
  }
5587
6193
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
5588
- log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
6194
+ log.debug(TAG27, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5589
6195
  continue;
5590
6196
  }
5591
6197
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
5592
- log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
6198
+ log.debug(TAG27, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5593
6199
  continue;
5594
6200
  }
5595
- log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
6201
+ log.info(TAG27, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5596
6202
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
5597
6203
  }
5598
6204
  }
5599
6205
  if (this.stateStore && this.agentConfig) {
5600
6206
  await this.recoverStaleRuns();
5601
6207
  }
6208
+ await this.recoverStrandedInProgress(cards, columns, knownCardIds);
5602
6209
  for (const knownId of knownCardIds) {
5603
6210
  if (!allAgentCardIds.has(knownId)) {
5604
- log.info(TAG26, `Missed unassign: ${knownId} — removing`);
6211
+ log.info(TAG27, `Missed unassign: ${knownId} — removing`);
5605
6212
  await this.pool.removeCard(knownId);
5606
6213
  }
5607
6214
  }
5608
- log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
6215
+ await this.releaseStalledApprovals(cards, columns, knownCardIds);
6216
+ log.debug(TAG27, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5609
6217
  } catch (err) {
5610
- log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
6218
+ log.error(TAG27, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5611
6219
  }
5612
6220
  }
5613
6221
  }
5614
- var TAG26 = "reconcile";
6222
+ var TAG27 = "reconcile";
5615
6223
  var init_reconcile = __esm(() => {
5616
6224
  init_board_helpers();
5617
6225
  init_log();
@@ -5632,6 +6240,7 @@ function prettyBanner(config, version) {
5632
6240
  const checks = [];
5633
6241
  let projectName;
5634
6242
  let gitProvider;
6243
+ let httpPort;
5635
6244
  let failed = false;
5636
6245
  let rendered = false;
5637
6246
  return {
@@ -5641,11 +6250,14 @@ function prettyBanner(config, version) {
5641
6250
  setGitProvider(provider) {
5642
6251
  gitProvider = provider;
5643
6252
  },
6253
+ setHttpPort(port) {
6254
+ httpPort = port;
6255
+ },
5644
6256
  check(message) {
5645
6257
  checks.push({ kind: "ok", message });
5646
6258
  },
5647
6259
  warn(message) {
5648
- log.warn(TAG27, message);
6260
+ log.warn(TAG28, message);
5649
6261
  checks.push({ kind: "warn", message: message.split(`
5650
6262
  `, 1)[0] });
5651
6263
  },
@@ -5661,6 +6273,7 @@ function prettyBanner(config, version) {
5661
6273
  config,
5662
6274
  projectName,
5663
6275
  gitProvider,
6276
+ httpPort,
5664
6277
  checks,
5665
6278
  readyMessage: message
5666
6279
  });
@@ -5669,22 +6282,25 @@ function prettyBanner(config, version) {
5669
6282
  };
5670
6283
  }
5671
6284
  function jsonBanner(config, version) {
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(", ")}`);
6285
+ log.info(TAG28, `Harmony Agent Daemon v${version} starting...`);
6286
+ log.info(TAG28, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5674
6287
  if (config.agent.review.enabled) {
5675
- log.info(TAG27, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
6288
+ log.info(TAG28, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5676
6289
  }
5677
6290
  let failed = false;
5678
6291
  return {
5679
6292
  setProjectName(_name) {},
5680
6293
  setGitProvider(provider) {
5681
- log.info(TAG27, `Git provider: ${provider}`);
6294
+ log.info(TAG28, `Git provider: ${provider}`);
6295
+ },
6296
+ setHttpPort(port) {
6297
+ log.info(TAG28, `HTTP server on port ${port}`);
5682
6298
  },
5683
6299
  check(message) {
5684
- log.info(TAG27, message);
6300
+ log.info(TAG28, message);
5685
6301
  },
5686
6302
  warn(message) {
5687
- log.warn(TAG27, message);
6303
+ log.warn(TAG28, message);
5688
6304
  },
5689
6305
  fail() {
5690
6306
  failed = true;
@@ -5692,17 +6308,25 @@ function jsonBanner(config, version) {
5692
6308
  async ready(message) {
5693
6309
  if (failed)
5694
6310
  return;
5695
- log.info(TAG27, message);
6311
+ log.info(TAG28, message);
5696
6312
  }
5697
6313
  };
5698
6314
  }
5699
6315
  function renderPretty(input) {
5700
- const { version, config, projectName, gitProvider, checks, readyMessage } = input;
6316
+ const {
6317
+ version,
6318
+ config,
6319
+ projectName,
6320
+ gitProvider,
6321
+ httpPort,
6322
+ checks,
6323
+ readyMessage
6324
+ } = input;
5701
6325
  const lines = [];
5702
6326
  lines.push("");
5703
6327
  lines.push(titleRule(`Harmony Agent Daemon v${version}`));
5704
6328
  lines.push("");
5705
- for (const row of configRows(config, projectName, gitProvider)) {
6329
+ for (const row of configRows(config, projectName, gitProvider, httpPort)) {
5706
6330
  lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
5707
6331
  }
5708
6332
  lines.push("");
@@ -5716,7 +6340,7 @@ function renderPretty(input) {
5716
6340
  return lines.join(`
5717
6341
  `);
5718
6342
  }
5719
- function configRows(config, projectName, gitProvider) {
6343
+ function configRows(config, projectName, gitProvider, httpPort) {
5720
6344
  const rows = [];
5721
6345
  const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
5722
6346
  rows.push({ label: "Project", value: projectLabel });
@@ -5732,7 +6356,7 @@ function configRows(config, projectName, gitProvider) {
5732
6356
  if (gitProvider)
5733
6357
  tail.push(gitProvider);
5734
6358
  if (config.agent.http.enabled) {
5735
- tail.push(`HTTP http://${config.agent.http.bindAddr}:${config.agent.http.port}`);
6359
+ tail.push(`HTTP http://${config.agent.http.bindAddr}:${httpPort ?? config.agent.http.port}`);
5736
6360
  }
5737
6361
  if (tail.length > 0) {
5738
6362
  rows.push({ label: "Git", value: tail.join(" · ") });
@@ -5759,7 +6383,7 @@ function cyan(s) {
5759
6383
  function yellow(s) {
5760
6384
  return `${ANSI.yellow}${s}${ANSI.reset}`;
5761
6385
  }
5762
- var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
6386
+ var TAG28 = "daemon", RULE_WIDTH = 70, ANSI;
5763
6387
  var init_startup_banner = __esm(() => {
5764
6388
  init_log();
5765
6389
  ANSI = {
@@ -5906,18 +6530,18 @@ class Watcher {
5906
6530
  }
5907
6531
  async start() {
5908
6532
  if (!isPretty()) {
5909
- log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
6533
+ log.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
5910
6534
  }
5911
6535
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
5912
6536
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
5913
6537
  const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
5914
- log.debug(TAG28, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
6538
+ log.debug(TAG29, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5915
6539
  this.onCardBroadcast({
5916
6540
  event: "card_update",
5917
6541
  payload: msg.payload ?? {}
5918
6542
  });
5919
6543
  }).on("broadcast", { event: "card_created" }, (msg) => {
5920
- log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
6544
+ log.debug(TAG29, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5921
6545
  this.onCardBroadcast({
5922
6546
  event: "card_created",
5923
6547
  payload: msg.payload ?? {}
@@ -5927,29 +6551,29 @@ class Watcher {
5927
6551
  const cardId = payload.card_id;
5928
6552
  const command = payload.command;
5929
6553
  if (cardId && command) {
5930
- log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
6554
+ log.info(TAG29, `Broadcast: agent_command ${command} for ${cardId}`);
5931
6555
  this.onAgentCommand?.({ cardId, command });
5932
6556
  }
5933
6557
  }).subscribe((status) => {
5934
6558
  if (status === "SUBSCRIBED") {
5935
6559
  this.connected = true;
5936
6560
  if (!isPretty() || !this.suppressStartupLogs) {
5937
- log.info(TAG28, "Broadcast subscription active");
6561
+ log.info(TAG29, "Broadcast subscription active");
5938
6562
  }
5939
6563
  this.maybeResolveReady();
5940
6564
  } else if (status === "CHANNEL_ERROR") {
5941
6565
  this.connected = false;
5942
- log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
6566
+ log.error(TAG29, "Broadcast channel error — will rely on reconciliation");
5943
6567
  } else if (status === "TIMED_OUT") {
5944
6568
  this.connected = false;
5945
- log.warn(TAG28, "Broadcast subscription timed out — retrying...");
6569
+ log.warn(TAG29, "Broadcast subscription timed out — retrying...");
5946
6570
  } else if (status === "CLOSED") {
5947
6571
  this.connected = false;
5948
6572
  }
5949
6573
  });
5950
6574
  this.channel = channel;
5951
6575
  presenceChannel.on("presence", { event: "sync" }, () => {
5952
- log.debug(TAG28, "Presence sync");
6576
+ log.debug(TAG29, "Presence sync");
5953
6577
  }).subscribe(async (status) => {
5954
6578
  if (status === "SUBSCRIBED") {
5955
6579
  await presenceChannel.track({
@@ -5962,7 +6586,7 @@ class Watcher {
5962
6586
  agentName: this.identity.agentName
5963
6587
  });
5964
6588
  if (!isPretty() || !this.suppressStartupLogs) {
5965
- log.info(TAG28, "Presence tracked on board-presence channel");
6589
+ log.info(TAG29, "Presence tracked on board-presence channel");
5966
6590
  }
5967
6591
  this.presenceTracked = true;
5968
6592
  this.maybeResolveReady();
@@ -5984,10 +6608,10 @@ class Watcher {
5984
6608
  this.supabase = null;
5985
6609
  }
5986
6610
  this.connected = false;
5987
- log.info(TAG28, "Broadcast subscription stopped");
6611
+ log.info(TAG29, "Broadcast subscription stopped");
5988
6612
  }
5989
6613
  }
5990
- var TAG28 = "watcher";
6614
+ var TAG29 = "watcher";
5991
6615
  var init_watcher = __esm(() => {
5992
6616
  init_log();
5993
6617
  });
@@ -6062,10 +6686,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
6062
6686
  });
6063
6687
  } catch {}
6064
6688
  if (result.removed.length > 0) {
6065
- log.info(TAG29, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
6689
+ log.info(TAG30, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
6066
6690
  }
6067
6691
  if (result.errors.length > 0) {
6068
- log.warn(TAG29, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
6692
+ log.warn(TAG30, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
6069
6693
  }
6070
6694
  return result;
6071
6695
  }
@@ -6142,10 +6766,10 @@ function pruneFailedRemoteBranches(opts) {
6142
6766
  }
6143
6767
  }
6144
6768
  if (result.removed.length > 0) {
6145
- log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6769
+ log.info(TAG30, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6146
6770
  }
6147
6771
  if (result.errors.length > 0) {
6148
- log.warn(TAG29, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
6772
+ log.warn(TAG30, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
6149
6773
  }
6150
6774
  return result;
6151
6775
  }
@@ -6176,13 +6800,13 @@ class WorktreeGc {
6176
6800
  try {
6177
6801
  runWorktreeGc(this.basePath, this.store);
6178
6802
  } catch (err) {
6179
- log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6803
+ log.warn(TAG30, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6180
6804
  }
6181
6805
  if (this.remoteOpts) {
6182
6806
  try {
6183
6807
  pruneFailedRemoteBranches(this.remoteOpts);
6184
6808
  } catch (err) {
6185
- log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6809
+ log.warn(TAG30, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6186
6810
  }
6187
6811
  }
6188
6812
  }
@@ -6196,7 +6820,7 @@ function getRepoRoot2() {
6196
6820
  return null;
6197
6821
  }
6198
6822
  }
6199
- var TAG29 = "worktree-gc";
6823
+ var TAG30 = "worktree-gc";
6200
6824
  var init_worktree_gc = __esm(() => {
6201
6825
  init_log();
6202
6826
  init_worktree();
@@ -6278,7 +6902,7 @@ async function main() {
6278
6902
  } catch (err) {
6279
6903
  if (err instanceof ConfigValidationError) {
6280
6904
  banner.fail();
6281
- log.error(TAG30, err.message);
6905
+ log.error(TAG31, err.message);
6282
6906
  process.exit(1);
6283
6907
  }
6284
6908
  throw err;
@@ -6387,25 +7011,28 @@ async function main() {
6387
7011
  if (shuttingDown)
6388
7012
  return;
6389
7013
  shuttingDown = true;
6390
- log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
7014
+ log.info(TAG31, `Received ${signal}, shutting down gracefully...`);
6391
7015
  reconciler.stop();
6392
7016
  mergeMonitor?.stop();
6393
7017
  worktreeGc.stop();
6394
- await httpServer?.stop();
7018
+ if (httpServer) {
7019
+ clearDaemonPort(config.projectId, process.pid);
7020
+ await httpServer.stop();
7021
+ }
6395
7022
  await watcher.stop();
6396
7023
  await pool.shutdown();
6397
- log.info(TAG30, "Daemon stopped.");
7024
+ log.info(TAG31, "Daemon stopped.");
6398
7025
  process.exit(exitCode);
6399
7026
  };
6400
7027
  process.on("SIGINT", () => shutdown("SIGINT"));
6401
7028
  process.on("SIGTERM", () => shutdown("SIGTERM"));
6402
7029
  process.on("uncaughtException", (err) => {
6403
- log.error(TAG30, `Uncaught exception: ${err.message}`);
7030
+ log.error(TAG31, `Uncaught exception: ${err.message}`);
6404
7031
  exitCode = 1;
6405
7032
  shutdown("uncaughtException");
6406
7033
  });
6407
7034
  process.on("unhandledRejection", (reason) => {
6408
- log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
7035
+ log.error(TAG31, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6409
7036
  exitCode = 1;
6410
7037
  shutdown("unhandledRejection");
6411
7038
  });
@@ -6415,7 +7042,13 @@ async function main() {
6415
7042
  worktreeGc.start();
6416
7043
  if (httpServer) {
6417
7044
  try {
6418
- await httpServer.start();
7045
+ const boundPort = await httpServer.start();
7046
+ recordDaemonPort(config.projectId, {
7047
+ port: boundPort,
7048
+ pid: process.pid,
7049
+ bindAddr: config.agent.http.bindAddr
7050
+ });
7051
+ banner.setHttpPort(boundPort);
6419
7052
  } catch (err) {
6420
7053
  banner.warn(`HTTP server failed to bind: ${err instanceof Error ? err.message : err}`);
6421
7054
  }
@@ -6452,34 +7085,34 @@ async function handleBroadcast(event, client, pool, config, agentId) {
6452
7085
  if (assignedAgentId === undefined)
6453
7086
  return;
6454
7087
  if (assignedAgentId === agentId) {
6455
- log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
7088
+ log.info(TAG31, `Broadcast: card ${cardId} assigned to agent`);
6456
7089
  try {
6457
7090
  await tryEnqueueCard(cardId, client, pool, config, agentId);
6458
7091
  } catch (err) {
6459
- log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
7092
+ log.error(TAG31, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6460
7093
  }
6461
7094
  } else if (pool.isCardKnown(cardId)) {
6462
- log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
7095
+ log.info(TAG31, `Broadcast: card ${cardId} unassigned from agent`);
6463
7096
  await pool.removeCard(cardId);
6464
7097
  }
6465
7098
  }
6466
7099
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6467
7100
  const { card } = await client.getCard(cardId);
6468
7101
  if (card.assigned_agent_id !== agentId) {
6469
- log.debug(TAG30, `Card ${cardId} no longer assigned to agent — skipping`);
7102
+ log.debug(TAG31, `Card ${cardId} no longer assigned to agent — skipping`);
6470
7103
  return;
6471
7104
  }
6472
7105
  const board = await client.getBoard(config.projectId, { summary: true });
6473
7106
  const columns = board.columns;
6474
7107
  const column = columns.find((c) => c.id === card.column_id);
6475
7108
  if (!column) {
6476
- log.warn(TAG30, `Column not found for card ${cardId}`);
7109
+ log.warn(TAG31, `Column not found for card ${cardId}`);
6477
7110
  return;
6478
7111
  }
6479
7112
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6480
7113
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6481
7114
  if (!isPickupColumn && !isReviewColumn) {
6482
- log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
7115
+ log.info(TAG31, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6483
7116
  return;
6484
7117
  }
6485
7118
  const mode = isReviewColumn ? "review" : "implement";
@@ -6487,16 +7120,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6487
7120
  const cardLabels = resolveCardLabels(card, labelMap);
6488
7121
  const subtasks = card.subtasks ?? [];
6489
7122
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
6490
- log.debug(TAG30, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
7123
+ log.debug(TAG31, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6491
7124
  return;
6492
7125
  }
6493
7126
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6494
- log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
7127
+ log.info(TAG31, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6495
7128
  return;
6496
7129
  }
6497
7130
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
6498
7131
  }
6499
- var TAG30 = "daemon", PKG_VERSION;
7132
+ var TAG31 = "daemon", PKG_VERSION;
6500
7133
  var init_src = __esm(() => {
6501
7134
  init_board_helpers();
6502
7135
  init_config();
@@ -6506,6 +7139,7 @@ var init_src = __esm(() => {
6506
7139
  init_log();
6507
7140
  init_merge_monitor();
6508
7141
  init_pool();
7142
+ init_port_registry();
6509
7143
  init_reconcile();
6510
7144
  init_recovery();
6511
7145
  init_review_worktree();