@gethmy/agent 1.9.1 → 1.9.2

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 +739 -97
  2. package/dist/index.js +734 -96
  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;
@@ -4691,6 +4986,7 @@ class Worker {
4691
4986
  projectId;
4692
4987
  stateStore;
4693
4988
  onCardCompleted;
4989
+ onApiError;
4694
4990
  id;
4695
4991
  state = "idle";
4696
4992
  cardId = null;
@@ -4707,7 +5003,7 @@ class Worker {
4707
5003
  verificationFailed = false;
4708
5004
  sessionId = null;
4709
5005
  runId = null;
4710
- constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted) {
5006
+ constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
4711
5007
  this.config = config;
4712
5008
  this.client = client;
4713
5009
  this.agentId = agentId;
@@ -4716,6 +5012,7 @@ class Worker {
4716
5012
  this.projectId = projectId;
4717
5013
  this.stateStore = stateStore;
4718
5014
  this.onCardCompleted = onCardCompleted;
5015
+ this.onApiError = onApiError;
4719
5016
  this.id = id;
4720
5017
  }
4721
5018
  startHeartbeat() {
@@ -4756,7 +5053,7 @@ class Worker {
4756
5053
  return this.state === "idle";
4757
5054
  }
4758
5055
  get isActive() {
4759
- return this.state === "preparing" || this.state === "running" || this.state === "verifying" || this.state === "completing";
5056
+ return this.state === "preparing" || this.state === "planning" || this.state === "running" || this.state === "verifying" || this.state === "completing";
4760
5057
  }
4761
5058
  async run(card, column, labels, subtasks) {
4762
5059
  this.aborted = false;
@@ -4802,7 +5099,7 @@ class Worker {
4802
5099
  }
4803
5100
  this.sessionId = sid;
4804
5101
  await this.recordPhase("preparing");
4805
- const moved = await moveCardAndAddLabel(this.client, card, "In Progress", "agent");
5102
+ const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
4806
5103
  if (!moved) {
4807
5104
  log.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
4808
5105
  }
@@ -4811,8 +5108,6 @@ class Worker {
4811
5108
  this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName);
4812
5109
  if (this.aborted)
4813
5110
  return;
4814
- this.state = "running";
4815
- await this.recordPhase("running");
4816
5111
  const enriched = {
4817
5112
  card,
4818
5113
  column,
@@ -4820,6 +5115,19 @@ class Worker {
4820
5115
  subtasks,
4821
5116
  mode: "implement"
4822
5117
  };
5118
+ if (shouldPlan(enriched, this.config.planning)) {
5119
+ this.state = "planning";
5120
+ await this.recordPhase("planning");
5121
+ const parked = await this.runPlanningPhase(enriched);
5122
+ if (this.aborted)
5123
+ return;
5124
+ if (parked) {
5125
+ log.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
5126
+ return;
5127
+ }
5128
+ }
5129
+ this.state = "running";
5130
+ await this.recordPhase("running");
4823
5131
  const prompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
4824
5132
  await this.client.updateAgentProgress(card.id, {
4825
5133
  agentIdentifier: agentIdentifier(this.id),
@@ -4858,21 +5166,46 @@ class Worker {
4858
5166
  this.state = "error";
4859
5167
  const msg = err instanceof Error ? err.message : String(err);
4860
5168
  log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
5169
+ const rawStderr = err?.stderr;
5170
+ const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
5171
+ const apiError = errClass.kind !== null;
5172
+ if (apiError) {
5173
+ try {
5174
+ this.onApiError?.(errClass);
5175
+ } catch {}
5176
+ }
5177
+ if (this.worktreePath) {
5178
+ try {
5179
+ cleanupWorktree(this.worktreePath, this.branchName ?? undefined);
5180
+ } catch {
5181
+ log.warn(this.tag, "Failed to cleanup worktree before requeue");
5182
+ }
5183
+ this.worktreePath = null;
5184
+ }
5185
+ const failureReason = apiError ? errClass.kind : "other";
5186
+ const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
4861
5187
  try {
4862
5188
  await runTransition(this.client, card, {
5189
+ move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
4863
5190
  endSession: {
4864
- status: "paused",
5191
+ status: "failed",
5192
+ failureReason,
5193
+ failureSummary,
4865
5194
  ...buildTokenPayload(this.lastSessionStats)
4866
5195
  }
4867
5196
  });
4868
5197
  } catch (tErr) {
4869
- log.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
5198
+ log.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
4870
5199
  }
4871
5200
  if (this.runId) {
4872
5201
  try {
4873
- await this.stateStore.endRun(this.runId, "paused", msg);
5202
+ await this.stateStore.endRun(this.runId, "failed", errClass.kind ?? msg);
4874
5203
  } catch {}
4875
- await this.recordOutcome(card.id, "failure");
5204
+ if (apiError) {
5205
+ await this.stateStore.decrementAttempt(card.id);
5206
+ } else {
5207
+ await this.recordOutcome(card.id, "failure");
5208
+ }
4876
5209
  }
4877
5210
  } finally {
4878
5211
  const succeeded = this.runId && this.state !== "error" && !this.aborted && !this.verificationFailed;
@@ -5016,7 +5349,122 @@ class Worker {
5016
5349
  }
5017
5350
  }
5018
5351
  }
5019
- async spawnClaude(prompt, card, subtasks) {
5352
+ async runPlanningPhase(enriched) {
5353
+ const planning = this.config.planning;
5354
+ const { card } = enriched;
5355
+ log.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
5356
+ await this.client.updateAgentProgress(card.id, {
5357
+ agentIdentifier: agentIdentifier(this.id),
5358
+ agentName: AGENT_NAME,
5359
+ status: "working",
5360
+ currentTask: "Planning approach (read-only)",
5361
+ progressPercent: 5,
5362
+ phase: "planning"
5363
+ }).catch(() => {});
5364
+ const planPrompt = buildPlanPrompt(enriched, this.worktreePath);
5365
+ let planTimedOut = false;
5366
+ const planTimeout = setTimeout(() => {
5367
+ planTimedOut = true;
5368
+ log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
5369
+ if (this.process && !this.process.killed) {
5370
+ terminateGroup(this.process, {
5371
+ sigintTimeoutMs: 1e4,
5372
+ sigtermTimeoutMs: 5000
5373
+ }).catch(() => {});
5374
+ }
5375
+ }, Math.min(this.config.maxTimeout, PLAN_PHASE_TIMEOUT));
5376
+ try {
5377
+ await this.spawnClaude(planPrompt, card, [], {
5378
+ model: planning.model,
5379
+ maxTurns: planning.maxTurns,
5380
+ allowedTools: PLAN_ALLOWED_TOOLS,
5381
+ initialPhase: "planning"
5382
+ });
5383
+ } catch (err) {
5384
+ log.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
5385
+ return false;
5386
+ } finally {
5387
+ clearTimeout(planTimeout);
5388
+ }
5389
+ if (this.aborted || planTimedOut)
5390
+ return false;
5391
+ const stats = this.lastSessionStats;
5392
+ if (stats?.cost) {
5393
+ const cents = Math.round(stats.cost.totalCostUsd * 100);
5394
+ if (cents > 0) {
5395
+ try {
5396
+ await this.stateStore.addCost(card.id, cents);
5397
+ } catch {}
5398
+ }
5399
+ }
5400
+ const planText = stats?.lastAssistantText ?? "";
5401
+ if (!planText.trim()) {
5402
+ log.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
5403
+ return false;
5404
+ }
5405
+ const artifact = extractPlanArtifact(planText, card.title);
5406
+ let planId = null;
5407
+ try {
5408
+ const result = await this.client.createPlan(this.projectId, {
5409
+ title: artifact.title,
5410
+ content: artifact.markdown,
5411
+ source: "agent",
5412
+ tasks: artifact.tasks
5413
+ });
5414
+ const plan = result?.plan;
5415
+ const createdId = plan && typeof plan === "object" && "id" in plan ? plan.id : null;
5416
+ if (createdId) {
5417
+ await this.client.updateCard(card.id, {
5418
+ planId: createdId,
5419
+ needsPlanRefresh: false
5420
+ });
5421
+ planId = createdId;
5422
+ }
5423
+ log.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
5424
+ } catch (err) {
5425
+ log.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
5426
+ }
5427
+ if (planning.mode === "gated" && planId) {
5428
+ try {
5429
+ if (planning.postComment) {
5430
+ await this.client.addComment(card.id, buildGatedPlanComment(artifact, planning.awaitingApprovalColumn), {
5431
+ commentType: "decision",
5432
+ agentSessionId: this.sessionId ?? undefined
5433
+ });
5434
+ }
5435
+ await runTransition(this.client, card, {
5436
+ move: { columnName: planning.awaitingApprovalColumn },
5437
+ removeLabels: ["agent"],
5438
+ endSession: {
5439
+ status: "completed",
5440
+ progressPercent: 20,
5441
+ ...buildTokenPayload(stats)
5442
+ }
5443
+ }, { store: this.stateStore, runId: this.runId ?? undefined });
5444
+ log.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
5445
+ this.lastSessionStats = undefined;
5446
+ return true;
5447
+ } catch (err) {
5448
+ 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}`);
5449
+ }
5450
+ }
5451
+ if (planId && planning.postComment) {
5452
+ try {
5453
+ await this.client.addComment(card.id, buildPlanComment(artifact), {
5454
+ commentType: "decision",
5455
+ agentSessionId: this.sessionId ?? undefined
5456
+ });
5457
+ } catch (err) {
5458
+ log.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
5459
+ }
5460
+ }
5461
+ return false;
5462
+ }
5463
+ async spawnClaude(prompt, card, subtasks, opts = {}) {
5464
+ const model = opts.model ?? this.config.claude.model;
5465
+ const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
5466
+ const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
5467
+ const initialPhase = opts.initialPhase ?? "exploring";
5020
5468
  return new Promise((resolve3, reject) => {
5021
5469
  const args = [
5022
5470
  "-p",
@@ -5024,11 +5472,11 @@ class Worker {
5024
5472
  "--output-format",
5025
5473
  "stream-json",
5026
5474
  "--model",
5027
- this.config.claude.model,
5475
+ model,
5028
5476
  "--max-turns",
5029
- String(this.config.claude.maxTurns),
5477
+ String(maxTurns),
5030
5478
  "--allowedTools",
5031
- "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
5479
+ allowedTools,
5032
5480
  ...this.config.claude.additionalArgs,
5033
5481
  "--",
5034
5482
  prompt
@@ -5047,7 +5495,7 @@ class Worker {
5047
5495
  stdio: ["ignore", "pipe", "pipe"]
5048
5496
  });
5049
5497
  const parser = new StreamParser;
5050
- this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
5498
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
5051
5499
  if (this.sessionId) {
5052
5500
  this.progressTracker.setSessionId(this.sessionId);
5053
5501
  }
@@ -5092,7 +5540,9 @@ class Worker {
5092
5540
  } else if (code === 0) {
5093
5541
  resolve3();
5094
5542
  } else {
5095
- reject(new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`));
5543
+ const err = new Error(`claude exited with code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ""}`);
5544
+ err.stderr = stderr;
5545
+ reject(err);
5096
5546
  }
5097
5547
  });
5098
5548
  });
@@ -5124,11 +5574,13 @@ class Worker {
5124
5574
  this.sessionId = null;
5125
5575
  }
5126
5576
  }
5127
- var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4;
5577
+ 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
5578
  var init_worker = __esm(() => {
5129
5579
  init_board_helpers();
5130
5580
  init_completion();
5581
+ init_error_classifier();
5131
5582
  init_log();
5583
+ init_plan_phase();
5132
5584
  init_process_group();
5133
5585
  init_progress_tracker();
5134
5586
  init_prompt();
@@ -5138,6 +5590,7 @@ var init_worker = __esm(() => {
5138
5590
  init_transitions();
5139
5591
  init_types();
5140
5592
  init_worktree();
5593
+ PLAN_PHASE_TIMEOUT = 10 * 60000;
5141
5594
  });
5142
5595
 
5143
5596
  // src/pool.ts
@@ -5153,6 +5606,8 @@ class Pool {
5153
5606
  budget;
5154
5607
  sleepGuard = new SleepGuard;
5155
5608
  shuttingDown = false;
5609
+ apiCooldownUntil = 0;
5610
+ authPaused = false;
5156
5611
  onCardCompleted = null;
5157
5612
  constructor(config, client, _userEmail, workspaceId, projectId, stateStore, agentId) {
5158
5613
  this.client = client;
@@ -5171,7 +5626,7 @@ class Pool {
5171
5626
  }
5172
5627
  }, workspaceId, projectId, stateStore, async (completedCard) => {
5173
5628
  await this.onCardCompleted?.(completedCard);
5174
- }));
5629
+ }, (err) => this.noteApiError(err)));
5175
5630
  }
5176
5631
  if (config.review.enabled) {
5177
5632
  for (let i = 0;i < config.review.poolSize; i++) {
@@ -5192,6 +5647,17 @@ class Pool {
5192
5647
  return;
5193
5648
  }
5194
5649
  if (mode === "implement") {
5650
+ if (this.authPaused) {
5651
+ log.debug(TAG24, `#${card.short_id} held — agent paused (auth error)`);
5652
+ await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
5653
+ return;
5654
+ }
5655
+ const cooldownMs = this.apiCooldownRemainingMs();
5656
+ if (cooldownMs > 0) {
5657
+ log.debug(TAG24, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
5658
+ await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
5659
+ return;
5660
+ }
5195
5661
  const decision = this.budget.check(card.id);
5196
5662
  if (!decision.allow) {
5197
5663
  if (decision.reason === "daily_budget") {
@@ -5241,6 +5707,26 @@ class Pool {
5241
5707
  log.debug(TAG24, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5242
5708
  }
5243
5709
  }
5710
+ noteApiError(err) {
5711
+ if (!err.kind)
5712
+ return;
5713
+ if (err.kind === "auth") {
5714
+ if (!this.authPaused) {
5715
+ log.error(TAG24, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
5716
+ }
5717
+ this.authPaused = true;
5718
+ return;
5719
+ }
5720
+ const cooldownMs = err.retryAfterMs ?? cooldownMsFor(err.kind);
5721
+ const until = Date.now() + cooldownMs;
5722
+ if (until > this.apiCooldownUntil) {
5723
+ this.apiCooldownUntil = until;
5724
+ log.warn(TAG24, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
5725
+ }
5726
+ }
5727
+ apiCooldownRemainingMs() {
5728
+ return Math.max(0, this.apiCooldownUntil - Date.now());
5729
+ }
5244
5730
  async removeCard(cardId) {
5245
5731
  await this.stateStore.resetAttempts(cardId);
5246
5732
  this.lastWaitingEmit.delete(cardId);
@@ -5368,6 +5854,7 @@ class Pool {
5368
5854
  }
5369
5855
  var TAG24 = "pool";
5370
5856
  var init_pool = __esm(() => {
5857
+ init_error_classifier();
5371
5858
  init_log();
5372
5859
  init_queue();
5373
5860
  init_review_worker();
@@ -5377,6 +5864,78 @@ var init_pool = __esm(() => {
5377
5864
  init_worker();
5378
5865
  });
5379
5866
 
5867
+ // src/port-registry.ts
5868
+ var exports_port_registry = {};
5869
+ __export(exports_port_registry, {
5870
+ recordDaemonPort: () => recordDaemonPort,
5871
+ lookupDaemonPort: () => lookupDaemonPort,
5872
+ defaultRegistryPath: () => defaultRegistryPath,
5873
+ clearDaemonPort: () => clearDaemonPort
5874
+ });
5875
+ import {
5876
+ existsSync as existsSync5,
5877
+ mkdirSync as mkdirSync3,
5878
+ readFileSync as readFileSync4,
5879
+ renameSync as renameSync2,
5880
+ writeFileSync as writeFileSync2
5881
+ } from "node:fs";
5882
+ import { homedir as homedir4 } from "node:os";
5883
+ import { dirname as dirname2, join as join4 } from "node:path";
5884
+ function defaultRegistryPath() {
5885
+ return join4(homedir4(), ".harmony-mcp", "agent-ports.json");
5886
+ }
5887
+ function load(path) {
5888
+ if (!existsSync5(path))
5889
+ return {};
5890
+ try {
5891
+ const raw = readFileSync4(path, "utf-8");
5892
+ const parsed = JSON.parse(raw);
5893
+ if (parsed && typeof parsed === "object")
5894
+ return parsed;
5895
+ return {};
5896
+ } catch (err) {
5897
+ log.warn(TAG25, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
5898
+ return {};
5899
+ }
5900
+ }
5901
+ function save(path, registry) {
5902
+ const dir = dirname2(path);
5903
+ if (!existsSync5(dir))
5904
+ mkdirSync3(dir, { recursive: true });
5905
+ const tmp = `${path}.tmp`;
5906
+ writeFileSync2(tmp, JSON.stringify(registry, null, 2), "utf-8");
5907
+ renameSync2(tmp, path);
5908
+ }
5909
+ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
5910
+ try {
5911
+ const registry = load(path);
5912
+ registry[projectId] = { ...entry, updatedAt: Date.now() };
5913
+ save(path, registry);
5914
+ } catch (err) {
5915
+ log.warn(TAG25, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
5916
+ }
5917
+ }
5918
+ function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
5919
+ const registry = load(path);
5920
+ return registry[projectId] ?? null;
5921
+ }
5922
+ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
5923
+ try {
5924
+ const registry = load(path);
5925
+ const existing = registry[projectId];
5926
+ if (!existing || existing.pid !== pid)
5927
+ return;
5928
+ delete registry[projectId];
5929
+ save(path, registry);
5930
+ } catch (err) {
5931
+ log.warn(TAG25, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
5932
+ }
5933
+ }
5934
+ var TAG25 = "port-registry";
5935
+ var init_port_registry = __esm(() => {
5936
+ init_log();
5937
+ });
5938
+
5380
5939
  // src/recovery.ts
5381
5940
  function isProcessAlive(pid, currentPid) {
5382
5941
  if (pid === currentPid)
@@ -5393,7 +5952,7 @@ async function fetchCardSafely(client, cardId) {
5393
5952
  const { card } = await client.getCard(cardId);
5394
5953
  return card;
5395
5954
  } catch (err) {
5396
- log.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5955
+ log.warn(TAG26, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
5397
5956
  return null;
5398
5957
  }
5399
5958
  }
@@ -5403,7 +5962,7 @@ async function recoverOrphans(store, client, config) {
5403
5962
  return [];
5404
5963
  }
5405
5964
  const outcomes = [];
5406
- log.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
5965
+ log.info(TAG26, `recovering ${active.length} orphan run(s) from prior daemon`);
5407
5966
  for (const run of active) {
5408
5967
  const outcome = {
5409
5968
  runId: run.runId,
@@ -5415,11 +5974,11 @@ async function recoverOrphans(store, client, config) {
5415
5974
  };
5416
5975
  outcomes.push(outcome);
5417
5976
  if (isProcessAlive(run.daemonPid, process.pid)) {
5418
- log.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5977
+ log.warn(TAG26, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
5419
5978
  outcome.actions.push("skipped: daemon pid still alive");
5420
5979
  continue;
5421
5980
  }
5422
- log.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5981
+ log.info(TAG26, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
5423
5982
  await recoverRun(run, store, client, config, outcome);
5424
5983
  }
5425
5984
  return outcomes;
@@ -5437,7 +5996,7 @@ async function recoverRun(run, store, client, config, outcome) {
5437
5996
  } catch (err) {
5438
5997
  const msg = err instanceof Error ? err.message : String(err);
5439
5998
  outcome.errors.push(`endAgentSession: ${msg}`);
5440
- log.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
5999
+ log.warn(TAG26, `endAgentSession failed for ${run.cardId}: ${msg}`);
5441
6000
  }
5442
6001
  const card = await fetchCardSafely(client, run.cardId);
5443
6002
  if (card) {
@@ -5478,9 +6037,9 @@ async function recoverRun(run, store, client, config, outcome) {
5478
6037
  const msg = err instanceof Error ? err.message : String(err);
5479
6038
  outcome.errors.push(`endRun: ${msg}`);
5480
6039
  }
5481
- log.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
6040
+ log.info(TAG26, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
5482
6041
  }
5483
- var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
6042
+ var TAG26 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
5484
6043
  var init_recovery = __esm(() => {
5485
6044
  init_board_helpers();
5486
6045
  init_log();
@@ -5528,7 +6087,7 @@ class Reconciler {
5528
6087
  clearInterval(this.timer);
5529
6088
  this.timer = null;
5530
6089
  }
5531
- log.info(TAG26, "Heartbeat stopped");
6090
+ log.info(TAG27, "Heartbeat stopped");
5532
6091
  }
5533
6092
  async recoverStaleRuns() {
5534
6093
  if (!this.stateStore || !this.agentConfig)
@@ -5545,7 +6104,7 @@ class Reconciler {
5545
6104
  if (!daemonDead && !(heartbeatStale && ourZombie))
5546
6105
  continue;
5547
6106
  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`);
6107
+ log.warn(TAG27, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
5549
6108
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
5550
6109
  runId: run.runId,
5551
6110
  cardId: run.cardId,
@@ -5556,6 +6115,57 @@ class Reconciler {
5556
6115
  });
5557
6116
  }
5558
6117
  }
6118
+ async recoverStrandedInProgress(cards, columns, knownCardIds) {
6119
+ const inProgressCol = columns.find((c) => c.name.toLowerCase() === IN_PROGRESS_COLUMN.toLowerCase());
6120
+ const pickupName = this.pickupColumns[0];
6121
+ const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
6122
+ if (!inProgressCol || !pickupCol)
6123
+ return;
6124
+ const graceMs = this.agentConfig?.timing.staleHeartbeatMs ?? 120000;
6125
+ const now = Date.now();
6126
+ const activeCardIds = new Set((this.stateStore?.getActiveRuns() ?? []).map((r) => r.cardId));
6127
+ for (const card of cards) {
6128
+ if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== inProgressCol.id || knownCardIds.has(card.id) || activeCardIds.has(card.id)) {
6129
+ continue;
6130
+ }
6131
+ const stalledAt = Date.parse(card.updated_at ?? "");
6132
+ if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
6133
+ continue;
6134
+ log.warn(TAG27, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
6135
+ try {
6136
+ await this.client.moveCard(card.id, pickupCol.id);
6137
+ } catch (err) {
6138
+ log.error(TAG27, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6139
+ }
6140
+ }
6141
+ }
6142
+ async releaseStalledApprovals(cards, columns, knownCardIds) {
6143
+ const planning = this.agentConfig?.planning;
6144
+ if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
6145
+ return;
6146
+ }
6147
+ const parkCol = columns.find((c) => c.name.toLowerCase() === planning.awaitingApprovalColumn.toLowerCase());
6148
+ const pickupName = this.pickupColumns[0];
6149
+ const pickupCol = pickupName ? columns.find((c) => c.name.toLowerCase() === pickupName.toLowerCase()) : undefined;
6150
+ if (!parkCol || !pickupCol)
6151
+ return;
6152
+ const ttlMs = planning.approvalTtlHours * 3600000;
6153
+ const now = Date.now();
6154
+ for (const card of cards) {
6155
+ if (card.assigned_agent_id !== this.agentId || card.archived_at || card.column_id !== parkCol.id || !card.plan_id || knownCardIds.has(card.id)) {
6156
+ continue;
6157
+ }
6158
+ const parkedAt = Date.parse(card.updated_at ?? "");
6159
+ if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
6160
+ continue;
6161
+ log.warn(TAG27, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
6162
+ try {
6163
+ await this.client.moveCard(card.id, pickupCol.id);
6164
+ } catch (err) {
6165
+ log.error(TAG27, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6166
+ }
6167
+ }
6168
+ }
5559
6169
  async tick() {
5560
6170
  this.lastTickAt = Date.now();
5561
6171
  try {
@@ -5581,37 +6191,39 @@ class Reconciler {
5581
6191
  const subtasks = card.subtasks ?? [];
5582
6192
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
5583
6193
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
5584
- log.debug(TAG26, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
6194
+ log.debug(TAG27, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
5585
6195
  continue;
5586
6196
  }
5587
6197
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
5588
- log.debug(TAG26, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
6198
+ log.debug(TAG27, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
5589
6199
  continue;
5590
6200
  }
5591
6201
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
5592
- log.debug(TAG26, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
6202
+ log.debug(TAG27, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
5593
6203
  continue;
5594
6204
  }
5595
- log.info(TAG26, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
6205
+ log.info(TAG27, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
5596
6206
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
5597
6207
  }
5598
6208
  }
5599
6209
  if (this.stateStore && this.agentConfig) {
5600
6210
  await this.recoverStaleRuns();
5601
6211
  }
6212
+ await this.recoverStrandedInProgress(cards, columns, knownCardIds);
5602
6213
  for (const knownId of knownCardIds) {
5603
6214
  if (!allAgentCardIds.has(knownId)) {
5604
- log.info(TAG26, `Missed unassign: ${knownId} — removing`);
6215
+ log.info(TAG27, `Missed unassign: ${knownId} — removing`);
5605
6216
  await this.pool.removeCard(knownId);
5606
6217
  }
5607
6218
  }
5608
- log.debug(TAG26, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
6219
+ await this.releaseStalledApprovals(cards, columns, knownCardIds);
6220
+ log.debug(TAG27, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
5609
6221
  } catch (err) {
5610
- log.error(TAG26, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
6222
+ log.error(TAG27, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
5611
6223
  }
5612
6224
  }
5613
6225
  }
5614
- var TAG26 = "reconcile";
6226
+ var TAG27 = "reconcile";
5615
6227
  var init_reconcile = __esm(() => {
5616
6228
  init_board_helpers();
5617
6229
  init_log();
@@ -5632,6 +6244,7 @@ function prettyBanner(config, version) {
5632
6244
  const checks = [];
5633
6245
  let projectName;
5634
6246
  let gitProvider;
6247
+ let httpPort;
5635
6248
  let failed = false;
5636
6249
  let rendered = false;
5637
6250
  return {
@@ -5641,11 +6254,14 @@ function prettyBanner(config, version) {
5641
6254
  setGitProvider(provider) {
5642
6255
  gitProvider = provider;
5643
6256
  },
6257
+ setHttpPort(port) {
6258
+ httpPort = port;
6259
+ },
5644
6260
  check(message) {
5645
6261
  checks.push({ kind: "ok", message });
5646
6262
  },
5647
6263
  warn(message) {
5648
- log.warn(TAG27, message);
6264
+ log.warn(TAG28, message);
5649
6265
  checks.push({ kind: "warn", message: message.split(`
5650
6266
  `, 1)[0] });
5651
6267
  },
@@ -5661,6 +6277,7 @@ function prettyBanner(config, version) {
5661
6277
  config,
5662
6278
  projectName,
5663
6279
  gitProvider,
6280
+ httpPort,
5664
6281
  checks,
5665
6282
  readyMessage: message
5666
6283
  });
@@ -5669,22 +6286,25 @@ function prettyBanner(config, version) {
5669
6286
  };
5670
6287
  }
5671
6288
  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(", ")}`);
6289
+ log.info(TAG28, `Harmony Agent Daemon v${version} starting...`);
6290
+ log.info(TAG28, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
5674
6291
  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}`);
6292
+ log.info(TAG28, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
5676
6293
  }
5677
6294
  let failed = false;
5678
6295
  return {
5679
6296
  setProjectName(_name) {},
5680
6297
  setGitProvider(provider) {
5681
- log.info(TAG27, `Git provider: ${provider}`);
6298
+ log.info(TAG28, `Git provider: ${provider}`);
6299
+ },
6300
+ setHttpPort(port) {
6301
+ log.info(TAG28, `HTTP server on port ${port}`);
5682
6302
  },
5683
6303
  check(message) {
5684
- log.info(TAG27, message);
6304
+ log.info(TAG28, message);
5685
6305
  },
5686
6306
  warn(message) {
5687
- log.warn(TAG27, message);
6307
+ log.warn(TAG28, message);
5688
6308
  },
5689
6309
  fail() {
5690
6310
  failed = true;
@@ -5692,17 +6312,25 @@ function jsonBanner(config, version) {
5692
6312
  async ready(message) {
5693
6313
  if (failed)
5694
6314
  return;
5695
- log.info(TAG27, message);
6315
+ log.info(TAG28, message);
5696
6316
  }
5697
6317
  };
5698
6318
  }
5699
6319
  function renderPretty(input) {
5700
- const { version, config, projectName, gitProvider, checks, readyMessage } = input;
6320
+ const {
6321
+ version,
6322
+ config,
6323
+ projectName,
6324
+ gitProvider,
6325
+ httpPort,
6326
+ checks,
6327
+ readyMessage
6328
+ } = input;
5701
6329
  const lines = [];
5702
6330
  lines.push("");
5703
6331
  lines.push(titleRule(`Harmony Agent Daemon v${version}`));
5704
6332
  lines.push("");
5705
- for (const row of configRows(config, projectName, gitProvider)) {
6333
+ for (const row of configRows(config, projectName, gitProvider, httpPort)) {
5706
6334
  lines.push(` ${dim(row.label.padEnd(9))} ${row.value}`);
5707
6335
  }
5708
6336
  lines.push("");
@@ -5716,7 +6344,7 @@ function renderPretty(input) {
5716
6344
  return lines.join(`
5717
6345
  `);
5718
6346
  }
5719
- function configRows(config, projectName, gitProvider) {
6347
+ function configRows(config, projectName, gitProvider, httpPort) {
5720
6348
  const rows = [];
5721
6349
  const projectLabel = projectName ? `${projectName} (${shortenId(config.projectId)})` : shortenId(config.projectId);
5722
6350
  rows.push({ label: "Project", value: projectLabel });
@@ -5732,7 +6360,7 @@ function configRows(config, projectName, gitProvider) {
5732
6360
  if (gitProvider)
5733
6361
  tail.push(gitProvider);
5734
6362
  if (config.agent.http.enabled) {
5735
- tail.push(`HTTP http://${config.agent.http.bindAddr}:${config.agent.http.port}`);
6363
+ tail.push(`HTTP http://${config.agent.http.bindAddr}:${httpPort ?? config.agent.http.port}`);
5736
6364
  }
5737
6365
  if (tail.length > 0) {
5738
6366
  rows.push({ label: "Git", value: tail.join(" · ") });
@@ -5759,7 +6387,7 @@ function cyan(s) {
5759
6387
  function yellow(s) {
5760
6388
  return `${ANSI.yellow}${s}${ANSI.reset}`;
5761
6389
  }
5762
- var TAG27 = "daemon", RULE_WIDTH = 70, ANSI;
6390
+ var TAG28 = "daemon", RULE_WIDTH = 70, ANSI;
5763
6391
  var init_startup_banner = __esm(() => {
5764
6392
  init_log();
5765
6393
  ANSI = {
@@ -5906,18 +6534,18 @@ class Watcher {
5906
6534
  }
5907
6535
  async start() {
5908
6536
  if (!isPretty()) {
5909
- log.info(TAG28, "Connecting to Supabase realtime (broadcast)...");
6537
+ log.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
5910
6538
  }
5911
6539
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
5912
6540
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
5913
6541
  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)}`);
6542
+ log.debug(TAG29, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
5915
6543
  this.onCardBroadcast({
5916
6544
  event: "card_update",
5917
6545
  payload: msg.payload ?? {}
5918
6546
  });
5919
6547
  }).on("broadcast", { event: "card_created" }, (msg) => {
5920
- log.debug(TAG28, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
6548
+ log.debug(TAG29, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
5921
6549
  this.onCardBroadcast({
5922
6550
  event: "card_created",
5923
6551
  payload: msg.payload ?? {}
@@ -5927,29 +6555,29 @@ class Watcher {
5927
6555
  const cardId = payload.card_id;
5928
6556
  const command = payload.command;
5929
6557
  if (cardId && command) {
5930
- log.info(TAG28, `Broadcast: agent_command ${command} for ${cardId}`);
6558
+ log.info(TAG29, `Broadcast: agent_command ${command} for ${cardId}`);
5931
6559
  this.onAgentCommand?.({ cardId, command });
5932
6560
  }
5933
6561
  }).subscribe((status) => {
5934
6562
  if (status === "SUBSCRIBED") {
5935
6563
  this.connected = true;
5936
6564
  if (!isPretty() || !this.suppressStartupLogs) {
5937
- log.info(TAG28, "Broadcast subscription active");
6565
+ log.info(TAG29, "Broadcast subscription active");
5938
6566
  }
5939
6567
  this.maybeResolveReady();
5940
6568
  } else if (status === "CHANNEL_ERROR") {
5941
6569
  this.connected = false;
5942
- log.error(TAG28, "Broadcast channel error — will rely on reconciliation");
6570
+ log.error(TAG29, "Broadcast channel error — will rely on reconciliation");
5943
6571
  } else if (status === "TIMED_OUT") {
5944
6572
  this.connected = false;
5945
- log.warn(TAG28, "Broadcast subscription timed out — retrying...");
6573
+ log.warn(TAG29, "Broadcast subscription timed out — retrying...");
5946
6574
  } else if (status === "CLOSED") {
5947
6575
  this.connected = false;
5948
6576
  }
5949
6577
  });
5950
6578
  this.channel = channel;
5951
6579
  presenceChannel.on("presence", { event: "sync" }, () => {
5952
- log.debug(TAG28, "Presence sync");
6580
+ log.debug(TAG29, "Presence sync");
5953
6581
  }).subscribe(async (status) => {
5954
6582
  if (status === "SUBSCRIBED") {
5955
6583
  await presenceChannel.track({
@@ -5962,7 +6590,7 @@ class Watcher {
5962
6590
  agentName: this.identity.agentName
5963
6591
  });
5964
6592
  if (!isPretty() || !this.suppressStartupLogs) {
5965
- log.info(TAG28, "Presence tracked on board-presence channel");
6593
+ log.info(TAG29, "Presence tracked on board-presence channel");
5966
6594
  }
5967
6595
  this.presenceTracked = true;
5968
6596
  this.maybeResolveReady();
@@ -5984,10 +6612,10 @@ class Watcher {
5984
6612
  this.supabase = null;
5985
6613
  }
5986
6614
  this.connected = false;
5987
- log.info(TAG28, "Broadcast subscription stopped");
6615
+ log.info(TAG29, "Broadcast subscription stopped");
5988
6616
  }
5989
6617
  }
5990
- var TAG28 = "watcher";
6618
+ var TAG29 = "watcher";
5991
6619
  var init_watcher = __esm(() => {
5992
6620
  init_log();
5993
6621
  });
@@ -6062,10 +6690,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
6062
6690
  });
6063
6691
  } catch {}
6064
6692
  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(", ")}`);
6693
+ log.info(TAG30, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
6066
6694
  }
6067
6695
  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("; ")}`);
6696
+ log.warn(TAG30, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
6069
6697
  }
6070
6698
  return result;
6071
6699
  }
@@ -6142,10 +6770,10 @@ function pruneFailedRemoteBranches(opts) {
6142
6770
  }
6143
6771
  }
6144
6772
  if (result.removed.length > 0) {
6145
- log.info(TAG29, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6773
+ log.info(TAG30, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
6146
6774
  }
6147
6775
  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("; ")}`);
6776
+ log.warn(TAG30, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
6149
6777
  }
6150
6778
  return result;
6151
6779
  }
@@ -6176,13 +6804,13 @@ class WorktreeGc {
6176
6804
  try {
6177
6805
  runWorktreeGc(this.basePath, this.store);
6178
6806
  } catch (err) {
6179
- log.warn(TAG29, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6807
+ log.warn(TAG30, `GC tick failed: ${err instanceof Error ? err.message : err}`);
6180
6808
  }
6181
6809
  if (this.remoteOpts) {
6182
6810
  try {
6183
6811
  pruneFailedRemoteBranches(this.remoteOpts);
6184
6812
  } catch (err) {
6185
- log.warn(TAG29, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6813
+ log.warn(TAG30, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
6186
6814
  }
6187
6815
  }
6188
6816
  }
@@ -6196,7 +6824,7 @@ function getRepoRoot2() {
6196
6824
  return null;
6197
6825
  }
6198
6826
  }
6199
- var TAG29 = "worktree-gc";
6827
+ var TAG30 = "worktree-gc";
6200
6828
  var init_worktree_gc = __esm(() => {
6201
6829
  init_log();
6202
6830
  init_worktree();
@@ -6278,7 +6906,7 @@ async function main() {
6278
6906
  } catch (err) {
6279
6907
  if (err instanceof ConfigValidationError) {
6280
6908
  banner.fail();
6281
- log.error(TAG30, err.message);
6909
+ log.error(TAG31, err.message);
6282
6910
  process.exit(1);
6283
6911
  }
6284
6912
  throw err;
@@ -6387,25 +7015,28 @@ async function main() {
6387
7015
  if (shuttingDown)
6388
7016
  return;
6389
7017
  shuttingDown = true;
6390
- log.info(TAG30, `Received ${signal}, shutting down gracefully...`);
7018
+ log.info(TAG31, `Received ${signal}, shutting down gracefully...`);
6391
7019
  reconciler.stop();
6392
7020
  mergeMonitor?.stop();
6393
7021
  worktreeGc.stop();
6394
- await httpServer?.stop();
7022
+ if (httpServer) {
7023
+ clearDaemonPort(config.projectId, process.pid);
7024
+ await httpServer.stop();
7025
+ }
6395
7026
  await watcher.stop();
6396
7027
  await pool.shutdown();
6397
- log.info(TAG30, "Daemon stopped.");
7028
+ log.info(TAG31, "Daemon stopped.");
6398
7029
  process.exit(exitCode);
6399
7030
  };
6400
7031
  process.on("SIGINT", () => shutdown("SIGINT"));
6401
7032
  process.on("SIGTERM", () => shutdown("SIGTERM"));
6402
7033
  process.on("uncaughtException", (err) => {
6403
- log.error(TAG30, `Uncaught exception: ${err.message}`);
7034
+ log.error(TAG31, `Uncaught exception: ${err.message}`);
6404
7035
  exitCode = 1;
6405
7036
  shutdown("uncaughtException");
6406
7037
  });
6407
7038
  process.on("unhandledRejection", (reason) => {
6408
- log.error(TAG30, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
7039
+ log.error(TAG31, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
6409
7040
  exitCode = 1;
6410
7041
  shutdown("unhandledRejection");
6411
7042
  });
@@ -6415,7 +7046,13 @@ async function main() {
6415
7046
  worktreeGc.start();
6416
7047
  if (httpServer) {
6417
7048
  try {
6418
- await httpServer.start();
7049
+ const boundPort = await httpServer.start();
7050
+ recordDaemonPort(config.projectId, {
7051
+ port: boundPort,
7052
+ pid: process.pid,
7053
+ bindAddr: config.agent.http.bindAddr
7054
+ });
7055
+ banner.setHttpPort(boundPort);
6419
7056
  } catch (err) {
6420
7057
  banner.warn(`HTTP server failed to bind: ${err instanceof Error ? err.message : err}`);
6421
7058
  }
@@ -6452,34 +7089,34 @@ async function handleBroadcast(event, client, pool, config, agentId) {
6452
7089
  if (assignedAgentId === undefined)
6453
7090
  return;
6454
7091
  if (assignedAgentId === agentId) {
6455
- log.info(TAG30, `Broadcast: card ${cardId} assigned to agent`);
7092
+ log.info(TAG31, `Broadcast: card ${cardId} assigned to agent`);
6456
7093
  try {
6457
7094
  await tryEnqueueCard(cardId, client, pool, config, agentId);
6458
7095
  } catch (err) {
6459
- log.error(TAG30, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
7096
+ log.error(TAG31, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
6460
7097
  }
6461
7098
  } else if (pool.isCardKnown(cardId)) {
6462
- log.info(TAG30, `Broadcast: card ${cardId} unassigned from agent`);
7099
+ log.info(TAG31, `Broadcast: card ${cardId} unassigned from agent`);
6463
7100
  await pool.removeCard(cardId);
6464
7101
  }
6465
7102
  }
6466
7103
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6467
7104
  const { card } = await client.getCard(cardId);
6468
7105
  if (card.assigned_agent_id !== agentId) {
6469
- log.debug(TAG30, `Card ${cardId} no longer assigned to agent — skipping`);
7106
+ log.debug(TAG31, `Card ${cardId} no longer assigned to agent — skipping`);
6470
7107
  return;
6471
7108
  }
6472
7109
  const board = await client.getBoard(config.projectId, { summary: true });
6473
7110
  const columns = board.columns;
6474
7111
  const column = columns.find((c) => c.id === card.column_id);
6475
7112
  if (!column) {
6476
- log.warn(TAG30, `Column not found for card ${cardId}`);
7113
+ log.warn(TAG31, `Column not found for card ${cardId}`);
6477
7114
  return;
6478
7115
  }
6479
7116
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6480
7117
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
6481
7118
  if (!isPickupColumn && !isReviewColumn) {
6482
- log.info(TAG30, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
7119
+ log.info(TAG31, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
6483
7120
  return;
6484
7121
  }
6485
7122
  const mode = isReviewColumn ? "review" : "implement";
@@ -6487,16 +7124,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
6487
7124
  const cardLabels = resolveCardLabels(card, labelMap);
6488
7125
  const subtasks = card.subtasks ?? [];
6489
7126
  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`);
7127
+ log.debug(TAG31, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
6491
7128
  return;
6492
7129
  }
6493
7130
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6494
- log.info(TAG30, `Card #${card.short_id} has no branch reference — skipping auto-review`);
7131
+ log.info(TAG31, `Card #${card.short_id} has no branch reference — skipping auto-review`);
6495
7132
  return;
6496
7133
  }
6497
7134
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
6498
7135
  }
6499
- var TAG30 = "daemon", PKG_VERSION;
7136
+ var TAG31 = "daemon", PKG_VERSION;
6500
7137
  var init_src = __esm(() => {
6501
7138
  init_board_helpers();
6502
7139
  init_config();
@@ -6506,6 +7143,7 @@ var init_src = __esm(() => {
6506
7143
  init_log();
6507
7144
  init_merge_monitor();
6508
7145
  init_pool();
7146
+ init_port_registry();
6509
7147
  init_reconcile();
6510
7148
  init_recovery();
6511
7149
  init_review_worktree();