@gethmy/agent 1.14.4 → 1.15.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 +484 -85
  2. package/dist/index.js +434 -85
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -101,6 +101,16 @@ var init_log = __esm(() => {
101
101
  });
102
102
 
103
103
  // src/board-helpers.ts
104
+ var exports_board_helpers = {};
105
+ __export(exports_board_helpers, {
106
+ resolveCardLabels: () => resolveCardLabels,
107
+ moveCardToColumn: () => moveCardToColumn,
108
+ moveCardAndAddLabel: () => moveCardAndAddLabel,
109
+ hasLabel: () => hasLabel,
110
+ findOrCreateLabel: () => findOrCreateLabel,
111
+ buildLabelMap: () => buildLabelMap,
112
+ addLabelByName: () => addLabelByName
113
+ });
104
114
  function buildLabelMap(boardLabels) {
105
115
  const map = new Map;
106
116
  for (const label of boardLabels) {
@@ -694,6 +704,22 @@ var init_config_validation = __esm(() => {
694
704
  });
695
705
 
696
706
  // src/git-pr.ts
707
+ var exports_git_pr = {};
708
+ __export(exports_git_pr, {
709
+ validateGitProviderCli: () => validateGitProviderCli,
710
+ updateExistingPr: () => updateExistingPr,
711
+ resolvePrUrl: () => resolvePrUrl,
712
+ renameRemoteBranch: () => renameRemoteBranch,
713
+ remoteBranchExists: () => remoteBranchExists,
714
+ pushBranch: () => pushBranch,
715
+ getBranchWebUrl: () => getBranchWebUrl,
716
+ findExistingPr: () => findExistingPr,
717
+ extractPrUrl: () => extractPrUrl,
718
+ detectGitProvider: () => detectGitProvider,
719
+ createPullRequest: () => createPullRequest,
720
+ checkPrMergeStatus: () => checkPrMergeStatus,
721
+ buildPrBody: () => buildPrBody
722
+ });
697
723
  import { execFile, execFileSync } from "node:child_process";
698
724
  import { promisify } from "node:util";
699
725
  function detectGitProvider(cwd) {
@@ -815,6 +841,14 @@ function extractPrUrl(description) {
815
841
  return null;
816
842
  }
817
843
  }
844
+ function resolvePrUrl(description, branchName, cwd, provider) {
845
+ const fromDesc = extractPrUrl(description);
846
+ if (fromDesc)
847
+ return fromDesc;
848
+ if (!branchName)
849
+ return null;
850
+ return findExistingPr(branchName, cwd, provider) || null;
851
+ }
818
852
  function remoteBranchExists(branchName, cwd) {
819
853
  try {
820
854
  execFileSync("git", ["ls-remote", "--exit-code", "origin", `refs/heads/${branchName}`], { cwd, stdio: "pipe" });
@@ -1601,6 +1635,48 @@ var init_logger = () => {};
1601
1635
  var init_playbookCatalog = () => {};
1602
1636
 
1603
1637
  // ../harmony-shared/dist/playbookStage.js
1638
+ function normalizeLoopDef(raw) {
1639
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
1640
+ return null;
1641
+ const obj = raw;
1642
+ if (obj.mode !== "converge" && obj.mode !== "fanout")
1643
+ return null;
1644
+ const mode = obj.mode;
1645
+ const rawMax = obj.max_iterations;
1646
+ const maxInt = typeof rawMax === "number" && Number.isFinite(rawMax) && rawMax >= 1 ? Math.floor(rawMax) : DEFAULT_LOOP_MAX_ITERATIONS;
1647
+ const exitGate = obj.exit_gate && typeof obj.exit_gate === "object" && !Array.isArray(obj.exit_gate) ? obj.exit_gate : null;
1648
+ const def = { mode, max_iterations: maxInt };
1649
+ if (exitGate)
1650
+ def.exit_gate = exitGate;
1651
+ if (obj.item_source && typeof obj.item_source === "object" && !Array.isArray(obj.item_source)) {
1652
+ def.item_source = obj.item_source;
1653
+ }
1654
+ if (typeof obj.concurrency === "number" && obj.concurrency >= 1) {
1655
+ def.concurrency = Math.floor(obj.concurrency);
1656
+ }
1657
+ if (obj.on_item_fail === "continue" || obj.on_item_fail === "halt") {
1658
+ def.on_item_fail = obj.on_item_fail;
1659
+ }
1660
+ return def;
1661
+ }
1662
+ function getStageLoop(stage) {
1663
+ return normalizeLoopDef(stage.loop);
1664
+ }
1665
+ function isConvergeLoop(loop) {
1666
+ return loop !== null && loop.mode === "converge";
1667
+ }
1668
+ function decideLoopContinuation(args) {
1669
+ const { loop, gatePassed, hasExitGate, completedIterations } = args;
1670
+ const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
1671
+ if (!hasExitGate) {
1672
+ return completedIterations >= max ? "exit" : "iterate";
1673
+ }
1674
+ if (gatePassed)
1675
+ return "exit";
1676
+ if (completedIterations >= max)
1677
+ return "exhausted";
1678
+ return "iterate";
1679
+ }
1604
1680
  function readStageDefs(def) {
1605
1681
  if (def.steps_version !== 2)
1606
1682
  return [];
@@ -1637,7 +1713,7 @@ function entryActionAllowlist(entryAction) {
1637
1713
  return `mcp__${entryAction}`;
1638
1714
  return null;
1639
1715
  }
1640
- var SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
1716
+ var DEFAULT_LOOP_MAX_ITERATIONS = 5, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE;
1641
1717
  var init_playbookStage = __esm(() => {
1642
1718
  SKILL_TOOL_ALLOWLIST = {
1643
1719
  hmy: "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*",
@@ -2186,6 +2262,10 @@ var init_review_worktree = __esm(() => {
2186
2262
  });
2187
2263
 
2188
2264
  // src/merge-monitor.ts
2265
+ var exports_merge_monitor = {};
2266
+ __export(exports_merge_monitor, {
2267
+ MergeMonitor: () => MergeMonitor
2268
+ });
2189
2269
  import { execFile as execFile2 } from "node:child_process";
2190
2270
  import { promisify as promisify2 } from "node:util";
2191
2271
 
@@ -2219,6 +2299,9 @@ class MergeMonitor {
2219
2299
  }
2220
2300
  log.info(TAG7, "Merge monitor stopped");
2221
2301
  }
2302
+ async runOnce() {
2303
+ await this.tick();
2304
+ }
2222
2305
  async scheduleNext(delayMs) {
2223
2306
  await new Promise((resolve3) => {
2224
2307
  this.timer = setTimeout(() => resolve3(), delayMs);
@@ -2256,9 +2339,10 @@ class MergeMonitor {
2256
2339
  const batch = candidatesWithLabels.slice(0, 5);
2257
2340
  log.debug(TAG7, `Checking ${batch.length} Ready to Merge card(s)`);
2258
2341
  const results = await Promise.allSettled(batch.map(async ({ card, labels }) => {
2259
- const prUrl = extractPrUrl(card.description ?? null);
2342
+ const branchName = extractBranchFromDescription(card.description);
2343
+ const prUrl = resolvePrUrl(card.description ?? null, branchName, this.cwd, this.provider);
2260
2344
  if (!prUrl) {
2261
- log.debug(TAG7, `#${card.short_id} has no PR URL — skipping`);
2345
+ log.debug(TAG7, `#${card.short_id} has no resolvable PR — skipping`);
2262
2346
  return;
2263
2347
  }
2264
2348
  const state = await checkPrMergeStatus(prUrl, this.cwd, this.provider);
@@ -5574,6 +5658,30 @@ class StateStore {
5574
5658
  rec.attempts = 0;
5575
5659
  await this.persist();
5576
5660
  }
5661
+ getLoopIterations(cardId, stageId) {
5662
+ const rec = this.getCard(cardId);
5663
+ if (!rec || rec.loopStageId !== stageId)
5664
+ return 0;
5665
+ return rec.loopIterations ?? 0;
5666
+ }
5667
+ async incrementLoopIteration(cardId, stageId) {
5668
+ const rec = this.ensureCard(cardId);
5669
+ if (rec.loopStageId !== stageId) {
5670
+ rec.loopStageId = stageId;
5671
+ rec.loopIterations = 0;
5672
+ }
5673
+ rec.loopIterations = (rec.loopIterations ?? 0) + 1;
5674
+ await this.persist();
5675
+ return rec.loopIterations;
5676
+ }
5677
+ async resetLoopIterations(cardId) {
5678
+ const rec = this.getCard(cardId);
5679
+ if (!rec || rec.loopIterations == null && rec.loopStageId == null)
5680
+ return;
5681
+ rec.loopStageId = null;
5682
+ rec.loopIterations = 0;
5683
+ await this.persist();
5684
+ }
5577
5685
  async resetAttempts(cardId) {
5578
5686
  const rec = this.getCard(cardId);
5579
5687
  if (!rec || rec.attempts === 0)
@@ -6707,6 +6815,26 @@ class CliAgentRunner {
6707
6815
  this.enqueue({ kind: "playbook_advanced", source: "system", payload });
6708
6816
  this.startTimer();
6709
6817
  }
6818
+ recordLoopIterationStarted(payload) {
6819
+ this.enqueue({ kind: "loop_iteration_started", source: "system", payload });
6820
+ this.startTimer();
6821
+ }
6822
+ recordLoopIterationEvaluated(payload) {
6823
+ this.enqueue({
6824
+ kind: "loop_iteration_evaluated",
6825
+ source: "system",
6826
+ payload: { ...payload, evidence: truncateOutput(payload.evidence) }
6827
+ });
6828
+ this.startTimer();
6829
+ }
6830
+ recordLoopCompleted(payload) {
6831
+ this.enqueue({ kind: "loop_completed", source: "system", payload });
6832
+ this.startTimer();
6833
+ }
6834
+ recordLoopExhausted(payload) {
6835
+ this.enqueue({ kind: "loop_exhausted", source: "system", payload });
6836
+ this.startTimer();
6837
+ }
6710
6838
  record(body) {
6711
6839
  this.enqueue(body);
6712
6840
  this.startTimer();
@@ -6802,7 +6930,7 @@ When finished, call harmony_end_agent_session with status="completed".`
6802
6930
  }
6803
6931
  async function renderCommentsSection(client, cardId) {
6804
6932
  try {
6805
- const { comments } = await client.getComments(cardId, { limit: 200 });
6933
+ const { comments } = await client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc`);
6806
6934
  if (!Array.isArray(comments) || comments.length === 0)
6807
6935
  return "";
6808
6936
  const section = serializeCommentThread(comments, {
@@ -6934,6 +7062,121 @@ async function resolveStageColumnName(client, card, stage) {
6934
7062
  async function persistStagePointer(client, card, body) {
6935
7063
  await client.request("POST", `/cards/${encodeURIComponent(card.id)}/advance-stage`, body);
6936
7064
  }
7065
+ async function advanceStageRun(card, stage, stageIndex, def, evaluation, deps) {
7066
+ const loop = getStageLoop(stage);
7067
+ if (!isConvergeLoop(loop)) {
7068
+ if (!evaluation)
7069
+ return { kind: "no_advance" };
7070
+ return advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps);
7071
+ }
7072
+ return advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps);
7073
+ }
7074
+ function firstErrorMessage(evaluation) {
7075
+ const e = evaluation?.findings.find((f) => f.level === "error");
7076
+ return e ? e.message : null;
7077
+ }
7078
+ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loop, deps) {
7079
+ const hasExitGate = loop.exit_gate != null;
7080
+ const gatePassed = hasExitGate ? evaluation?.passed ?? false : false;
7081
+ const gateResult = !hasExitGate ? "skipped" : gatePassed ? "passed" : "failed";
7082
+ const maxIterations = Math.max(1, Math.floor(loop.max_iterations) || 1);
7083
+ const iteration = await deps.stateStore.incrementLoopIteration(card.id, stage.id);
7084
+ const decision = decideLoopContinuation({
7085
+ loop,
7086
+ gatePassed,
7087
+ hasExitGate,
7088
+ completedIterations: iteration
7089
+ });
7090
+ const evidence = evaluation ? evaluation.findings.map((f) => `[${f.level}] ${f.message}`).join(`
7091
+ `) : undefined;
7092
+ const verdictGloss = !hasExitGate ? "count-only" : gatePassed ? "gate passed" : firstErrorMessage(evaluation) ?? "gate unmet";
7093
+ const summary = `iteration ${iteration}/${maxIterations} — ${verdictGloss}${decision === "iterate" ? " → retrying" : ""}`;
7094
+ deps.sink?.recordLoopIterationEvaluated?.({
7095
+ stageId: stage.id,
7096
+ iteration,
7097
+ maxIterations,
7098
+ gateResult,
7099
+ evidence,
7100
+ summary
7101
+ });
7102
+ log.info(TAG28, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
7103
+ if (decision === "exit") {
7104
+ await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
7105
+ deps.sink?.recordLoopCompleted?.({
7106
+ stageId: stage.id,
7107
+ iterations: iteration,
7108
+ maxIterations,
7109
+ reason: hasExitGate ? "exit gate passed" : `count-only loop completed ${iteration} iteration(s)`
7110
+ });
7111
+ const exitEval = evaluation?.passed ? evaluation : {
7112
+ passed: true,
7113
+ findings: [
7114
+ {
7115
+ level: "info",
7116
+ message: `Converge loop "${stage.name}" complete after ${iteration} iteration(s).`
7117
+ }
7118
+ ],
7119
+ structured: evaluation?.structured ?? {}
7120
+ };
7121
+ return advanceStageOnGate(card, stage, stageIndex, def, exitEval, deps);
7122
+ }
7123
+ if (decision === "exhausted") {
7124
+ await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
7125
+ await deps.stateStore.decrementAttempt(card.id).catch(() => {});
7126
+ const reason = `Converge loop "${stage.name}" exhausted its ${maxIterations}-iteration budget with its exit gate still unmet — ${firstErrorMessage(evaluation) ?? "gate unmet"}. Holding for a human.`;
7127
+ deps.sink?.recordLoopExhausted?.({
7128
+ stageId: stage.id,
7129
+ iterations: iteration,
7130
+ maxIterations,
7131
+ reason
7132
+ });
7133
+ try {
7134
+ await deps.client.updateAgentProgress(card.id, {
7135
+ agentIdentifier: "claude-code-stage",
7136
+ agentName: "Harmony Agent",
7137
+ status: "waiting",
7138
+ currentTask: reason
7139
+ });
7140
+ } catch {}
7141
+ await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
7142
+ keepAttempts: true
7143
+ });
7144
+ log.info(TAG28, `#${card.short_id} LoopExhausted: ${reason}`);
7145
+ return { kind: "held_gate_unmet", reason };
7146
+ }
7147
+ await deps.stateStore.decrementAttempt(card.id).catch(() => {});
7148
+ await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
7149
+ const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
7150
+ try {
7151
+ await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
7152
+ } catch {}
7153
+ await runTransition(deps.client, card, {
7154
+ move: { columnName: toColumn },
7155
+ addLabels: [{ name: AGENT_LABEL }],
7156
+ ...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
7157
+ }, { store: deps.stateStore, runId: deps.runId });
7158
+ log.info(TAG28, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
7159
+ return { kind: "requeued_gate_unmet", toColumn };
7160
+ }
7161
+ async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
7162
+ try {
7163
+ const findings = evaluation?.findings.filter((f) => f.level !== "info") ?? [];
7164
+ const produced = findings.length > 0 ? `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop did not pass its exit gate. Findings:
7165
+ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
7166
+ `)}` : `Iteration ${iteration}/${maxIterations} of the "${stage.name}" converge loop completed; the loop continues.`;
7167
+ const body = buildHandoffCommentBody({
7168
+ stageId: stage.id,
7169
+ stageName: stage.name,
7170
+ artifactType: stage.artifact_type,
7171
+ produced,
7172
+ decisions: [],
7173
+ nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
7174
+ });
7175
+ await deps.client.addComment(card.id, body, { commentType: "decision" });
7176
+ } catch (err) {
7177
+ log.warn(TAG28, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7178
+ }
7179
+ }
6937
7180
  async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
6938
7181
  const summary = gateSummary(stage, evaluation);
6939
7182
  deps.sink?.recordStageGateEvaluated({
@@ -7292,7 +7535,9 @@ class Worker {
7292
7535
  const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
7293
7536
  let prompt = basePrompt;
7294
7537
  if (stageCtx.kind === "run") {
7295
- const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id);
7538
+ const loop = getStageLoop(stageCtx.stage);
7539
+ const isLoop = isConvergeLoop(loop);
7540
+ const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
7296
7541
  prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
7297
7542
 
7298
7543
  `);
@@ -7301,6 +7546,16 @@ class Worker {
7301
7546
  stageName: stageCtx.stage.name,
7302
7547
  owner: stageCtx.stage.owner
7303
7548
  });
7549
+ if (isLoop && loop) {
7550
+ const priorIterations = this.stateStore.getLoopIterations(card.id, stageCtx.stage.id);
7551
+ this.cliRunner?.recordLoopIterationStarted({
7552
+ stageId: stageCtx.stage.id,
7553
+ stageName: stageCtx.stage.name,
7554
+ iteration: priorIterations + 1,
7555
+ maxIterations: Math.max(1, Math.floor(loop.max_iterations) || 1),
7556
+ mode: loop.mode
7557
+ });
7558
+ }
7304
7559
  }
7305
7560
  await this.client.updateAgentProgress(card.id, {
7306
7561
  agentIdentifier: agentIdentifier(this.id),
@@ -7462,15 +7717,19 @@ class Worker {
7462
7717
  } catch {}
7463
7718
  await this.recordOutcome(card.id, "failure");
7464
7719
  } else if (this.runId && this.aborted) {
7465
- try {
7466
- await this.client.updateCard(card.id, { assignedAgentId: null });
7467
- } catch (err) {
7468
- log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
7469
- }
7470
- try {
7471
- await runTransition(this.client, card, { removeLabels: ["agent"] });
7472
- } catch (tErr) {
7473
- log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7720
+ if (!this.completionStarted) {
7721
+ try {
7722
+ await this.client.updateCard(card.id, { assignedAgentId: null });
7723
+ } catch (err) {
7724
+ log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
7725
+ }
7726
+ try {
7727
+ await runTransition(this.client, card, { removeLabels: ["agent"] });
7728
+ } catch (tErr) {
7729
+ log.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7730
+ }
7731
+ } else {
7732
+ log.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
7474
7733
  }
7475
7734
  try {
7476
7735
  await this.stateStore.endRun(this.runId, "paused", {
@@ -7612,15 +7871,13 @@ class Worker {
7612
7871
  } catch {}
7613
7872
  }
7614
7873
  }
7615
- async loadInheritedHandoffSection(cardId, currentStageId) {
7874
+ async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
7616
7875
  try {
7617
- const { comments } = await this.client.getComments(cardId, {
7618
- limit: 200
7619
- });
7876
+ const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
7620
7877
  if (!Array.isArray(comments) || comments.length === 0)
7621
7878
  return "";
7622
7879
  const handoff = extractLatestHandoff(comments, {
7623
- excludeStageId: currentStageId
7880
+ excludeStageId: opts.includeOwnStage ? undefined : currentStageId
7624
7881
  });
7625
7882
  return handoff ? renderInheritedHandoffSection(handoff) : "";
7626
7883
  } catch (err) {
@@ -7648,7 +7905,9 @@ class Worker {
7648
7905
  }
7649
7906
  async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
7650
7907
  try {
7651
- const gate = normalizeGateSpec(stage.gate);
7908
+ const loop = getStageLoop(stage);
7909
+ const gateSource = isConvergeLoop(loop) ? loop?.exit_gate ?? null : stage.gate;
7910
+ const gate = normalizeGateSpec(gateSource);
7652
7911
  if (!gate) {
7653
7912
  return null;
7654
7913
  }
@@ -7691,11 +7950,8 @@ class Worker {
7691
7950
  }
7692
7951
  }
7693
7952
  async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
7694
- if (!evaluation) {
7695
- return { kind: "no_advance" };
7696
- }
7697
7953
  try {
7698
- return await advanceStageOnGate(card, stage, stageIndex, def, evaluation, {
7954
+ return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
7699
7955
  client: this.client,
7700
7956
  stateStore: this.stateStore,
7701
7957
  agentId: this.agentId,
@@ -8671,6 +8927,71 @@ var init_recovery = __esm(() => {
8671
8927
  init_worktree();
8672
8928
  });
8673
8929
 
8930
+ // src/strand-recovery.ts
8931
+ var exports_strand_recovery = {};
8932
+ __export(exports_strand_recovery, {
8933
+ reclaimPreReviewStrands: () => reclaimPreReviewStrands
8934
+ });
8935
+ async function reclaimPreReviewStrands(opts) {
8936
+ const {
8937
+ client,
8938
+ agentId,
8939
+ cards,
8940
+ columns,
8941
+ labelMap,
8942
+ reviewColumns,
8943
+ approvedLabel,
8944
+ graceMs,
8945
+ knownCardIds,
8946
+ cwd,
8947
+ provider
8948
+ } = opts;
8949
+ const now = opts.now ?? Date.now();
8950
+ const maxPerSweep = opts.maxPerSweep ?? 5;
8951
+ const reviewColIds = new Set(columns.filter((c) => reviewColumns.some((n) => n.toLowerCase() === c.name.toLowerCase())).map((c) => c.id));
8952
+ if (reviewColIds.size === 0)
8953
+ return [];
8954
+ const reclaimed = [];
8955
+ let checked = 0;
8956
+ for (const card of cards) {
8957
+ if (checked >= maxPerSweep)
8958
+ break;
8959
+ if (card.archived_at || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
8960
+ continue;
8961
+ }
8962
+ const branch = extractBranchFromDescription(card.description);
8963
+ if (!branch)
8964
+ continue;
8965
+ const labels = resolveCardLabels(card, labelMap);
8966
+ if (hasLabel(labels, approvedLabel) || hasLabel(labels, NEED_REVIEW_LABEL)) {
8967
+ continue;
8968
+ }
8969
+ const stalledAt = Date.parse(card.updated_at ?? "");
8970
+ if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
8971
+ continue;
8972
+ checked++;
8973
+ const prUrl = resolvePrUrl(card.description ?? null, branch, cwd, provider);
8974
+ if (prUrl)
8975
+ continue;
8976
+ log.warn(TAG33, `#${card.short_id} stranded in review (branch pushed, no PR, unowned) — re-asserting daemon assignment`);
8977
+ try {
8978
+ await client.updateCard(card.id, { assignedAgentId: agentId });
8979
+ reclaimed.push(card.id);
8980
+ } catch (err) {
8981
+ log.error(TAG33, `review re-claim failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8982
+ }
8983
+ }
8984
+ return reclaimed;
8985
+ }
8986
+ var TAG33 = "strand-recovery";
8987
+ var init_strand_recovery = __esm(() => {
8988
+ init_board_helpers();
8989
+ init_git_pr();
8990
+ init_log();
8991
+ init_review_worktree();
8992
+ init_types();
8993
+ });
8994
+
8674
8995
  // src/reconcile.ts
8675
8996
  class Reconciler {
8676
8997
  client;
@@ -8685,6 +9006,7 @@ class Reconciler {
8685
9006
  agentConfig;
8686
9007
  timer = null;
8687
9008
  lastTickAt = null;
9009
+ gitProvider = null;
8688
9010
  get lastTick() {
8689
9011
  return this.lastTickAt;
8690
9012
  }
@@ -8712,7 +9034,7 @@ class Reconciler {
8712
9034
  clearInterval(this.timer);
8713
9035
  this.timer = null;
8714
9036
  }
8715
- log.info(TAG33, "Heartbeat stopped");
9037
+ log.info(TAG34, "Heartbeat stopped");
8716
9038
  }
8717
9039
  async recoverStaleRuns() {
8718
9040
  if (!this.stateStore || !this.agentConfig)
@@ -8729,7 +9051,7 @@ class Reconciler {
8729
9051
  if (!daemonDead && !(heartbeatStale && ourZombie))
8730
9052
  continue;
8731
9053
  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`;
8732
- log.warn(TAG33, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9054
+ log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
8733
9055
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
8734
9056
  runId: run.runId,
8735
9057
  cardId: run.cardId,
@@ -8756,14 +9078,38 @@ class Reconciler {
8756
9078
  const stalledAt = Date.parse(card.updated_at ?? "");
8757
9079
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
8758
9080
  continue;
8759
- log.warn(TAG33, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9081
+ log.warn(TAG34, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
8760
9082
  try {
8761
9083
  await this.client.moveCard(card.id, pickupCol.id);
8762
9084
  } catch (err) {
8763
- log.error(TAG33, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9085
+ log.error(TAG34, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8764
9086
  }
8765
9087
  }
8766
9088
  }
9089
+ async recoverStrandedReview(cards, columns, labelMap, knownCardIds) {
9090
+ if (this.reviewColumns.length === 0)
9091
+ return;
9092
+ if (!this.gitProvider) {
9093
+ try {
9094
+ this.gitProvider = detectGitProvider();
9095
+ } catch {
9096
+ return;
9097
+ }
9098
+ }
9099
+ await reclaimPreReviewStrands({
9100
+ client: this.client,
9101
+ agentId: this.agentId,
9102
+ cards,
9103
+ columns,
9104
+ labelMap,
9105
+ reviewColumns: this.reviewColumns,
9106
+ approvedLabel: this.approvedLabel,
9107
+ graceMs: this.agentConfig?.timing.staleHeartbeatMs ?? 120000,
9108
+ knownCardIds,
9109
+ cwd: process.cwd(),
9110
+ provider: this.gitProvider
9111
+ });
9112
+ }
8767
9113
  async releaseStalledApprovals(cards, columns, knownCardIds) {
8768
9114
  const planning = this.agentConfig?.planning;
8769
9115
  if (!planning?.enabled || planning.mode !== "gated" || planning.approvalTtlHours <= 0) {
@@ -8783,11 +9129,11 @@ class Reconciler {
8783
9129
  const parkedAt = Date.parse(card.updated_at ?? "");
8784
9130
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
8785
9131
  continue;
8786
- log.warn(TAG33, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
9132
+ log.warn(TAG34, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
8787
9133
  try {
8788
9134
  await this.client.moveCard(card.id, pickupCol.id);
8789
9135
  } catch (err) {
8790
- log.error(TAG33, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9136
+ log.error(TAG34, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8791
9137
  }
8792
9138
  }
8793
9139
  }
@@ -8830,21 +9176,21 @@ class Reconciler {
8830
9176
  const subtasks = card.subtasks ?? [];
8831
9177
  const mode = route.mode;
8832
9178
  if (route.stage) {
8833
- log.info(TAG33, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
9179
+ log.info(TAG34, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
8834
9180
  }
8835
9181
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
8836
- log.debug(TAG33, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
9182
+ log.debug(TAG34, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
8837
9183
  continue;
8838
9184
  }
8839
9185
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
8840
- log.debug(TAG33, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
9186
+ log.debug(TAG34, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
8841
9187
  continue;
8842
9188
  }
8843
9189
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
8844
- log.debug(TAG33, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
9190
+ log.debug(TAG34, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
8845
9191
  continue;
8846
9192
  }
8847
- log.info(TAG33, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
9193
+ log.info(TAG34, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
8848
9194
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
8849
9195
  }
8850
9196
  }
@@ -8852,25 +9198,28 @@ class Reconciler {
8852
9198
  await this.recoverStaleRuns();
8853
9199
  }
8854
9200
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
9201
+ await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
8855
9202
  for (const knownId of knownCardIds) {
8856
9203
  if (!allAgentCardIds.has(knownId)) {
8857
- log.info(TAG33, `Missed unassign: ${knownId} — removing`);
9204
+ log.info(TAG34, `Missed unassign: ${knownId} — removing`);
8858
9205
  await this.pool.removeCard(knownId);
8859
9206
  }
8860
9207
  }
8861
9208
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
8862
- log.debug(TAG33, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9209
+ log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
8863
9210
  } catch (err) {
8864
- log.error(TAG33, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9211
+ log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
8865
9212
  }
8866
9213
  }
8867
9214
  }
8868
- var TAG33 = "reconcile";
9215
+ var TAG34 = "reconcile";
8869
9216
  var init_reconcile = __esm(() => {
8870
9217
  init_board_helpers();
9218
+ init_git_pr();
8871
9219
  init_log();
8872
9220
  init_recovery();
8873
9221
  init_review_worktree();
9222
+ init_strand_recovery();
8874
9223
  init_types();
8875
9224
  });
8876
9225
 
@@ -8903,7 +9252,7 @@ function prettyBanner(config, version) {
8903
9252
  checks.push({ kind: "ok", message });
8904
9253
  },
8905
9254
  warn(message) {
8906
- log.warn(TAG34, message);
9255
+ log.warn(TAG35, message);
8907
9256
  checks.push({ kind: "warn", message: message.split(`
8908
9257
  `, 1)[0] });
8909
9258
  },
@@ -8928,25 +9277,25 @@ function prettyBanner(config, version) {
8928
9277
  };
8929
9278
  }
8930
9279
  function jsonBanner(config, version) {
8931
- log.info(TAG34, `Harmony Agent Daemon v${version} starting...`);
8932
- log.info(TAG34, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
9280
+ log.info(TAG35, `Harmony Agent Daemon v${version} starting...`);
9281
+ log.info(TAG35, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
8933
9282
  if (config.agent.review.enabled) {
8934
- log.info(TAG34, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
9283
+ log.info(TAG35, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
8935
9284
  }
8936
9285
  let failed = false;
8937
9286
  return {
8938
9287
  setProjectName(_name) {},
8939
9288
  setGitProvider(provider) {
8940
- log.info(TAG34, `Git provider: ${provider}`);
9289
+ log.info(TAG35, `Git provider: ${provider}`);
8941
9290
  },
8942
9291
  setHttpPort(port) {
8943
- log.info(TAG34, `HTTP server on port ${port}`);
9292
+ log.info(TAG35, `HTTP server on port ${port}`);
8944
9293
  },
8945
9294
  check(message) {
8946
- log.info(TAG34, message);
9295
+ log.info(TAG35, message);
8947
9296
  },
8948
9297
  warn(message) {
8949
- log.warn(TAG34, message);
9298
+ log.warn(TAG35, message);
8950
9299
  },
8951
9300
  fail() {
8952
9301
  failed = true;
@@ -8954,7 +9303,7 @@ function jsonBanner(config, version) {
8954
9303
  async ready(message) {
8955
9304
  if (failed)
8956
9305
  return;
8957
- log.info(TAG34, message);
9306
+ log.info(TAG35, message);
8958
9307
  }
8959
9308
  };
8960
9309
  }
@@ -9035,7 +9384,7 @@ function cyan(s) {
9035
9384
  function yellow(s) {
9036
9385
  return `${ANSI.yellow}${s}${ANSI.reset}`;
9037
9386
  }
9038
- var TAG34 = "daemon", RULE_WIDTH = 70, ANSI;
9387
+ var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
9039
9388
  var init_startup_banner = __esm(() => {
9040
9389
  init_log();
9041
9390
  ANSI = {
@@ -9186,13 +9535,13 @@ class Watcher {
9186
9535
  }
9187
9536
  async start() {
9188
9537
  if (!isPretty()) {
9189
- log.info(TAG35, "Connecting to Supabase realtime (broadcast)...");
9538
+ log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
9190
9539
  }
9191
9540
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
9192
9541
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
9193
9542
  this.subscribeBroadcast();
9194
9543
  presenceChannel.on("presence", { event: "sync" }, () => {
9195
- log.debug(TAG35, "Presence sync");
9544
+ log.debug(TAG36, "Presence sync");
9196
9545
  }).subscribe(async (status) => {
9197
9546
  if (status === "SUBSCRIBED") {
9198
9547
  await presenceChannel.track({
@@ -9205,7 +9554,7 @@ class Watcher {
9205
9554
  agentName: this.identity.agentName
9206
9555
  });
9207
9556
  if (!isPretty() || !this.suppressStartupLogs) {
9208
- log.info(TAG35, "Presence tracked on board-presence channel");
9557
+ log.info(TAG36, "Presence tracked on board-presence channel");
9209
9558
  }
9210
9559
  this.presenceTracked = true;
9211
9560
  this.maybeResolveReady();
@@ -9218,13 +9567,13 @@ class Watcher {
9218
9567
  return;
9219
9568
  const gen = ++this.broadcastGen;
9220
9569
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
9221
- log.debug(TAG35, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9570
+ log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9222
9571
  this.onCardBroadcast({
9223
9572
  event: "card_update",
9224
9573
  payload: msg.payload ?? {}
9225
9574
  });
9226
9575
  }).on("broadcast", { event: "card_created" }, (msg) => {
9227
- log.debug(TAG35, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9576
+ log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9228
9577
  this.onCardBroadcast({
9229
9578
  event: "card_created",
9230
9579
  payload: msg.payload ?? {}
@@ -9234,7 +9583,7 @@ class Watcher {
9234
9583
  const cardId = payload.card_id;
9235
9584
  const command = payload.command;
9236
9585
  if (cardId && command) {
9237
- log.info(TAG35, `Broadcast: agent_command ${command} for ${cardId}`);
9586
+ log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
9238
9587
  this.onAgentCommand?.({ cardId, command });
9239
9588
  }
9240
9589
  }).subscribe((status) => {
@@ -9244,13 +9593,13 @@ class Watcher {
9244
9593
  this.connected = true;
9245
9594
  this.reconnectAttempts = 0;
9246
9595
  if (!isPretty() || !this.suppressStartupLogs) {
9247
- log.info(TAG35, "Broadcast subscription active");
9596
+ log.info(TAG36, "Broadcast subscription active");
9248
9597
  }
9249
9598
  this.maybeResolveReady();
9250
9599
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9251
9600
  this.connected = false;
9252
9601
  if (!this.stopping) {
9253
- log.warn(TAG35, `Broadcast subscription ${status} — scheduling reconnect`);
9602
+ log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
9254
9603
  this.scheduleReconnect();
9255
9604
  }
9256
9605
  }
@@ -9269,7 +9618,7 @@ class Watcher {
9269
9618
  async reconnectBroadcast() {
9270
9619
  if (this.stopping || !this.supabase)
9271
9620
  return;
9272
- log.warn(TAG35, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9621
+ log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9273
9622
  if (this.channel) {
9274
9623
  const old = this.channel;
9275
9624
  this.channel = null;
@@ -9299,10 +9648,10 @@ class Watcher {
9299
9648
  this.supabase = null;
9300
9649
  }
9301
9650
  this.connected = false;
9302
- log.info(TAG35, "Broadcast subscription stopped");
9651
+ log.info(TAG36, "Broadcast subscription stopped");
9303
9652
  }
9304
9653
  }
9305
- var TAG35 = "watcher";
9654
+ var TAG36 = "watcher";
9306
9655
  var init_watcher = __esm(() => {
9307
9656
  init_log();
9308
9657
  });
@@ -9389,10 +9738,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
9389
9738
  });
9390
9739
  } catch {}
9391
9740
  if (result.removed.length > 0) {
9392
- log.info(TAG36, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
9741
+ log.info(TAG37, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
9393
9742
  }
9394
9743
  if (result.errors.length > 0) {
9395
- log.warn(TAG36, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
9744
+ log.warn(TAG37, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
9396
9745
  }
9397
9746
  return result;
9398
9747
  }
@@ -9422,7 +9771,7 @@ function pruneFailedRemoteBranches(opts) {
9422
9771
  } catch (err) {
9423
9772
  const detail = gitErrorDetail2(err);
9424
9773
  if (isTransientGitNetworkError(detail)) {
9425
- log.debug(TAG36, `Remote branch GC skipped — remote unreachable: ${detail}`);
9774
+ log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
9426
9775
  return result;
9427
9776
  }
9428
9777
  result.errors.push({ ref: "fetch", error: detail });
@@ -9461,7 +9810,7 @@ function pruneFailedRemoteBranches(opts) {
9461
9810
  continue;
9462
9811
  }
9463
9812
  if (clock() > sweepDeadline) {
9464
- log.debug(TAG36, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
9813
+ log.debug(TAG37, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
9465
9814
  break;
9466
9815
  }
9467
9816
  try {
@@ -9474,17 +9823,17 @@ function pruneFailedRemoteBranches(opts) {
9474
9823
  } catch (err) {
9475
9824
  const detail = gitErrorDetail2(err);
9476
9825
  if (isTransientGitNetworkError(detail)) {
9477
- log.debug(TAG36, `Remote branch GC interrupted — remote unreachable: ${detail}`);
9826
+ log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
9478
9827
  break;
9479
9828
  }
9480
9829
  result.errors.push({ ref, error: detail });
9481
9830
  }
9482
9831
  }
9483
9832
  if (result.removed.length > 0) {
9484
- log.info(TAG36, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
9833
+ log.info(TAG37, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
9485
9834
  }
9486
9835
  if (result.errors.length > 0) {
9487
- log.warn(TAG36, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
9836
+ log.warn(TAG37, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
9488
9837
  }
9489
9838
  return result;
9490
9839
  }
@@ -9515,13 +9864,13 @@ class WorktreeGc {
9515
9864
  try {
9516
9865
  runWorktreeGc(this.basePath, this.store);
9517
9866
  } catch (err) {
9518
- log.warn(TAG36, `GC tick failed: ${err instanceof Error ? err.message : err}`);
9867
+ log.warn(TAG37, `GC tick failed: ${err instanceof Error ? err.message : err}`);
9519
9868
  }
9520
9869
  if (this.remoteOpts) {
9521
9870
  try {
9522
9871
  pruneFailedRemoteBranches(this.remoteOpts);
9523
9872
  } catch (err) {
9524
- log.warn(TAG36, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
9873
+ log.warn(TAG37, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
9525
9874
  }
9526
9875
  }
9527
9876
  }
@@ -9535,7 +9884,7 @@ function getRepoRoot2() {
9535
9884
  return null;
9536
9885
  }
9537
9886
  }
9538
- var TAG36 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
9887
+ var TAG37 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
9539
9888
  var init_worktree_gc = __esm(() => {
9540
9889
  init_log();
9541
9890
  init_worktree();
@@ -9639,7 +9988,7 @@ async function main() {
9639
9988
  } catch (err) {
9640
9989
  if (err instanceof ConfigValidationError) {
9641
9990
  banner.fail();
9642
- log.error(TAG37, err.message);
9991
+ log.error(TAG38, err.message);
9643
9992
  process.exit(1);
9644
9993
  }
9645
9994
  throw err;
@@ -9749,7 +10098,7 @@ async function main() {
9749
10098
  if (shuttingDown)
9750
10099
  return;
9751
10100
  shuttingDown = true;
9752
- log.info(TAG37, `Received ${signal}, shutting down gracefully...`);
10101
+ log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
9753
10102
  reconciler.stop();
9754
10103
  mergeMonitor?.stop();
9755
10104
  worktreeGc.stop();
@@ -9759,18 +10108,18 @@ async function main() {
9759
10108
  }
9760
10109
  await watcher.stop();
9761
10110
  await pool.shutdown();
9762
- log.info(TAG37, "Daemon stopped.");
10111
+ log.info(TAG38, "Daemon stopped.");
9763
10112
  process.exit(exitCode);
9764
10113
  };
9765
10114
  process.on("SIGINT", () => shutdown("SIGINT"));
9766
10115
  process.on("SIGTERM", () => shutdown("SIGTERM"));
9767
10116
  process.on("uncaughtException", (err) => {
9768
- log.error(TAG37, `Uncaught exception: ${err.message}`);
10117
+ log.error(TAG38, `Uncaught exception: ${err.message}`);
9769
10118
  exitCode = 1;
9770
10119
  shutdown("uncaughtException");
9771
10120
  });
9772
10121
  process.on("unhandledRejection", (reason) => {
9773
- log.error(TAG37, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
10122
+ log.error(TAG38, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
9774
10123
  exitCode = 1;
9775
10124
  shutdown("unhandledRejection");
9776
10125
  });
@@ -9823,29 +10172,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
9823
10172
  if (assignedAgentId === undefined)
9824
10173
  return;
9825
10174
  if (assignedAgentId === agentId) {
9826
- log.info(TAG37, `Broadcast: card ${cardId} assigned to agent`);
10175
+ log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
9827
10176
  try {
9828
10177
  await pool.resetAttemptsForReassign(cardId);
9829
10178
  await tryEnqueueCard(cardId, client, pool, config, agentId);
9830
10179
  } catch (err) {
9831
- log.error(TAG37, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
10180
+ log.error(TAG38, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
9832
10181
  }
9833
10182
  } else if (pool.isCardKnown(cardId)) {
9834
- log.info(TAG37, `Broadcast: card ${cardId} unassigned from agent`);
10183
+ log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
9835
10184
  await pool.removeCard(cardId);
9836
10185
  }
9837
10186
  }
9838
10187
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
9839
10188
  const { card } = await client.getCard(cardId);
9840
10189
  if (card.assigned_agent_id !== agentId) {
9841
- log.debug(TAG37, `Card ${cardId} no longer assigned to agent — skipping`);
10190
+ log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
9842
10191
  return;
9843
10192
  }
9844
10193
  const board = await client.getBoard(config.projectId, { summary: true });
9845
10194
  const columns = board.columns;
9846
10195
  const column = columns.find((c) => c.id === card.column_id);
9847
10196
  if (!column) {
9848
- log.warn(TAG37, `Column not found for card ${cardId}`);
10197
+ log.warn(TAG38, `Column not found for card ${cardId}`);
9849
10198
  return;
9850
10199
  }
9851
10200
  const route = classifyPickup(card, column.name, {
@@ -9854,27 +10203,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
9854
10203
  playbooks: config.agent.playbooks
9855
10204
  });
9856
10205
  if (!route) {
9857
- log.info(TAG37, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
10206
+ log.info(TAG38, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
9858
10207
  return;
9859
10208
  }
9860
10209
  if (route.stage) {
9861
- log.info(TAG37, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
10210
+ log.info(TAG38, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
9862
10211
  }
9863
10212
  const mode = route.mode;
9864
10213
  const labelMap = buildLabelMap(board.labels ?? []);
9865
10214
  const cardLabels = resolveCardLabels(card, labelMap);
9866
10215
  const subtasks = card.subtasks ?? [];
9867
10216
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
9868
- log.debug(TAG37, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
10217
+ log.debug(TAG38, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
9869
10218
  return;
9870
10219
  }
9871
10220
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
9872
- log.info(TAG37, `Card #${card.short_id} has no branch reference — skipping auto-review`);
10221
+ log.info(TAG38, `Card #${card.short_id} has no branch reference — skipping auto-review`);
9873
10222
  return;
9874
10223
  }
9875
10224
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
9876
10225
  }
9877
- var TAG37 = "daemon", PKG_VERSION;
10226
+ var TAG38 = "daemon", PKG_VERSION;
9878
10227
  var init_src = __esm(() => {
9879
10228
  init_board_helpers();
9880
10229
  init_config();