@gethmy/agent 1.14.3 → 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 +504 -86
  2. package/dist/index.js +454 -86
  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({
@@ -7087,6 +7330,9 @@ function buildStagePreamble(stage) {
7087
7330
  }
7088
7331
  }
7089
7332
  lines.push("Do only this stage's work. When the stage's handoff is met, end your session — advancement to the next stage is handled by the board.");
7333
+ if (normalizeGateSpec(stage.gate)?.kind === "review_passed") {
7334
+ lines.push("", "**This stage's gate is `review_passed` — your deliverable is a review verdict, not a prose handoff.** Do NOT end the session until you have actually reviewed the change and emitted EXACTLY one JSON block as the LAST thing you output (nothing after it):", "```json", REVIEW_VERDICT_SCHEMA, "```", "Decision rules:", REVIEW_DECISION_RULES, "The board reads this verdict to decide advancement: `approved` advances the card, `rejected` sends it back. A missing or unparseable verdict blocks the card. Do NOT modify any code — this is a read-only review.");
7335
+ }
7090
7336
  return lines.filter(Boolean).join(`
7091
7337
  `);
7092
7338
  }
@@ -7131,6 +7377,7 @@ class Worker {
7131
7377
  lastDrainedSeq = 0;
7132
7378
  runCostCents = 0;
7133
7379
  runTurns = 0;
7380
+ lastRunText = "";
7134
7381
  constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
7135
7382
  this.config = config;
7136
7383
  this.client = client;
@@ -7199,6 +7446,7 @@ class Worker {
7199
7446
  this.completionStarted = false;
7200
7447
  this.runCostCents = 0;
7201
7448
  this.runTurns = 0;
7449
+ this.lastRunText = "";
7202
7450
  this.cliSessionId = null;
7203
7451
  this.lastDrainedSeq = 0;
7204
7452
  this.cardId = card.id;
@@ -7287,7 +7535,9 @@ class Worker {
7287
7535
  const basePrompt = await buildPrompt(enriched, this.branchName, this.worktreePath, this.client, this.workspaceId, this.projectId);
7288
7536
  let prompt = basePrompt;
7289
7537
  if (stageCtx.kind === "run") {
7290
- 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 });
7291
7541
  prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
7292
7542
 
7293
7543
  `);
@@ -7296,6 +7546,16 @@ class Worker {
7296
7546
  stageName: stageCtx.stage.name,
7297
7547
  owner: stageCtx.stage.owner
7298
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
+ }
7299
7559
  }
7300
7560
  await this.client.updateAgentProgress(card.id, {
7301
7561
  agentIdentifier: agentIdentifier(this.id),
@@ -7457,15 +7717,19 @@ class Worker {
7457
7717
  } catch {}
7458
7718
  await this.recordOutcome(card.id, "failure");
7459
7719
  } else if (this.runId && this.aborted) {
7460
- try {
7461
- await this.client.updateCard(card.id, { assignedAgentId: null });
7462
- } catch (err) {
7463
- log.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
7464
- }
7465
- try {
7466
- await runTransition(this.client, card, { removeLabels: ["agent"] });
7467
- } catch (tErr) {
7468
- 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)`);
7469
7733
  }
7470
7734
  try {
7471
7735
  await this.stateStore.endRun(this.runId, "paused", {
@@ -7607,15 +7871,13 @@ class Worker {
7607
7871
  } catch {}
7608
7872
  }
7609
7873
  }
7610
- async loadInheritedHandoffSection(cardId, currentStageId) {
7874
+ async loadInheritedHandoffSection(cardId, currentStageId, opts = {}) {
7611
7875
  try {
7612
- const { comments } = await this.client.getComments(cardId, {
7613
- limit: 200
7614
- });
7876
+ const { comments } = await this.client.request("GET", `/cards/${encodeURIComponent(cardId)}/comments?limit=200&order=desc&comment_type=decision`);
7615
7877
  if (!Array.isArray(comments) || comments.length === 0)
7616
7878
  return "";
7617
7879
  const handoff = extractLatestHandoff(comments, {
7618
- excludeStageId: currentStageId
7880
+ excludeStageId: opts.includeOwnStage ? undefined : currentStageId
7619
7881
  });
7620
7882
  return handoff ? renderInheritedHandoffSection(handoff) : "";
7621
7883
  } catch (err) {
@@ -7643,7 +7905,9 @@ class Worker {
7643
7905
  }
7644
7906
  async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
7645
7907
  try {
7646
- 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);
7647
7911
  if (!gate) {
7648
7912
  return null;
7649
7913
  }
@@ -7651,6 +7915,10 @@ class Worker {
7651
7915
  log.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
7652
7916
  return null;
7653
7917
  }
7918
+ const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
7919
+ if (review) {
7920
+ log.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
7921
+ }
7654
7922
  const registry = buildGateCollectorRegistry({
7655
7923
  build: {
7656
7924
  worktreePath,
@@ -7661,7 +7929,8 @@ class Worker {
7661
7929
  worktreePath,
7662
7930
  artifactType: stage.artifact_type
7663
7931
  },
7664
- checklist: { subtasks, cardDone: card.done ?? false }
7932
+ checklist: { subtasks, cardDone: card.done ?? false },
7933
+ ...review ? { review } : {}
7665
7934
  });
7666
7935
  const context = {
7667
7936
  cardId: card.id,
@@ -7681,11 +7950,8 @@ class Worker {
7681
7950
  }
7682
7951
  }
7683
7952
  async advanceFromGateEvaluation(card, stage, stageIndex, def, evaluation) {
7684
- if (!evaluation) {
7685
- return { kind: "no_advance" };
7686
- }
7687
7953
  try {
7688
- return await advanceStageOnGate(card, stage, stageIndex, def, evaluation, {
7954
+ return await advanceStageRun(card, stage, stageIndex, def, evaluation, {
7689
7955
  client: this.client,
7690
7956
  stateStore: this.stateStore,
7691
7957
  agentId: this.agentId,
@@ -7999,6 +8265,9 @@ class Worker {
7999
8265
  });
8000
8266
  }
8001
8267
  }
8268
+ parser.on("text", (content) => {
8269
+ this.lastRunText += content;
8270
+ });
8002
8271
  parser.on("parse_error", (msg) => {
8003
8272
  log.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
8004
8273
  runLog?.stream.write(`
@@ -8092,6 +8361,10 @@ class Worker {
8092
8361
  try {
8093
8362
  for await (const ev of stream) {
8094
8363
  this.progressTracker?.ingest(ev);
8364
+ if (ev.kind === "assistant_text") {
8365
+ this.lastRunText += `${ev.payload.text}
8366
+ `;
8367
+ }
8095
8368
  if (ev.source === "agent")
8096
8369
  this.cliRunner?.record(ev);
8097
8370
  if (ev.kind === "error") {
@@ -8179,6 +8452,8 @@ var init_worker = __esm(() => {
8179
8452
  init_process_group();
8180
8453
  init_progress_tracker();
8181
8454
  init_prompt();
8455
+ init_review_completion();
8456
+ init_review_knowledge();
8182
8457
  init_run_log();
8183
8458
  init_sdk_agent_runner();
8184
8459
  init_stage_advance();
@@ -8652,6 +8927,71 @@ var init_recovery = __esm(() => {
8652
8927
  init_worktree();
8653
8928
  });
8654
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
+
8655
8995
  // src/reconcile.ts
8656
8996
  class Reconciler {
8657
8997
  client;
@@ -8666,6 +9006,7 @@ class Reconciler {
8666
9006
  agentConfig;
8667
9007
  timer = null;
8668
9008
  lastTickAt = null;
9009
+ gitProvider = null;
8669
9010
  get lastTick() {
8670
9011
  return this.lastTickAt;
8671
9012
  }
@@ -8693,7 +9034,7 @@ class Reconciler {
8693
9034
  clearInterval(this.timer);
8694
9035
  this.timer = null;
8695
9036
  }
8696
- log.info(TAG33, "Heartbeat stopped");
9037
+ log.info(TAG34, "Heartbeat stopped");
8697
9038
  }
8698
9039
  async recoverStaleRuns() {
8699
9040
  if (!this.stateStore || !this.agentConfig)
@@ -8710,7 +9051,7 @@ class Reconciler {
8710
9051
  if (!daemonDead && !(heartbeatStale && ourZombie))
8711
9052
  continue;
8712
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`;
8713
- log.warn(TAG33, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9054
+ log.warn(TAG34, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
8714
9055
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
8715
9056
  runId: run.runId,
8716
9057
  cardId: run.cardId,
@@ -8737,13 +9078,37 @@ class Reconciler {
8737
9078
  const stalledAt = Date.parse(card.updated_at ?? "");
8738
9079
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
8739
9080
  continue;
8740
- 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}"`);
8741
9082
  try {
8742
9083
  await this.client.moveCard(card.id, pickupCol.id);
8743
9084
  } catch (err) {
8744
- 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}`);
9086
+ }
9087
+ }
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;
8745
9097
  }
8746
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
+ });
8747
9112
  }
8748
9113
  async releaseStalledApprovals(cards, columns, knownCardIds) {
8749
9114
  const planning = this.agentConfig?.planning;
@@ -8764,11 +9129,11 @@ class Reconciler {
8764
9129
  const parkedAt = Date.parse(card.updated_at ?? "");
8765
9130
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
8766
9131
  continue;
8767
- 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}"`);
8768
9133
  try {
8769
9134
  await this.client.moveCard(card.id, pickupCol.id);
8770
9135
  } catch (err) {
8771
- 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}`);
8772
9137
  }
8773
9138
  }
8774
9139
  }
@@ -8811,21 +9176,21 @@ class Reconciler {
8811
9176
  const subtasks = card.subtasks ?? [];
8812
9177
  const mode = route.mode;
8813
9178
  if (route.stage) {
8814
- 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`);
8815
9180
  }
8816
9181
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
8817
- 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`);
8818
9183
  continue;
8819
9184
  }
8820
9185
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
8821
- 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)`);
8822
9187
  continue;
8823
9188
  }
8824
9189
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
8825
- 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)`);
8826
9191
  continue;
8827
9192
  }
8828
- 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`);
8829
9194
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
8830
9195
  }
8831
9196
  }
@@ -8833,25 +9198,28 @@ class Reconciler {
8833
9198
  await this.recoverStaleRuns();
8834
9199
  }
8835
9200
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
9201
+ await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
8836
9202
  for (const knownId of knownCardIds) {
8837
9203
  if (!allAgentCardIds.has(knownId)) {
8838
- log.info(TAG33, `Missed unassign: ${knownId} — removing`);
9204
+ log.info(TAG34, `Missed unassign: ${knownId} — removing`);
8839
9205
  await this.pool.removeCard(knownId);
8840
9206
  }
8841
9207
  }
8842
9208
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
8843
- log.debug(TAG33, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
9209
+ log.debug(TAG34, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
8844
9210
  } catch (err) {
8845
- log.error(TAG33, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
9211
+ log.error(TAG34, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
8846
9212
  }
8847
9213
  }
8848
9214
  }
8849
- var TAG33 = "reconcile";
9215
+ var TAG34 = "reconcile";
8850
9216
  var init_reconcile = __esm(() => {
8851
9217
  init_board_helpers();
9218
+ init_git_pr();
8852
9219
  init_log();
8853
9220
  init_recovery();
8854
9221
  init_review_worktree();
9222
+ init_strand_recovery();
8855
9223
  init_types();
8856
9224
  });
8857
9225
 
@@ -8884,7 +9252,7 @@ function prettyBanner(config, version) {
8884
9252
  checks.push({ kind: "ok", message });
8885
9253
  },
8886
9254
  warn(message) {
8887
- log.warn(TAG34, message);
9255
+ log.warn(TAG35, message);
8888
9256
  checks.push({ kind: "warn", message: message.split(`
8889
9257
  `, 1)[0] });
8890
9258
  },
@@ -8909,25 +9277,25 @@ function prettyBanner(config, version) {
8909
9277
  };
8910
9278
  }
8911
9279
  function jsonBanner(config, version) {
8912
- log.info(TAG34, `Harmony Agent Daemon v${version} starting...`);
8913
- 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(", ")}`);
8914
9282
  if (config.agent.review.enabled) {
8915
- 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}`);
8916
9284
  }
8917
9285
  let failed = false;
8918
9286
  return {
8919
9287
  setProjectName(_name) {},
8920
9288
  setGitProvider(provider) {
8921
- log.info(TAG34, `Git provider: ${provider}`);
9289
+ log.info(TAG35, `Git provider: ${provider}`);
8922
9290
  },
8923
9291
  setHttpPort(port) {
8924
- log.info(TAG34, `HTTP server on port ${port}`);
9292
+ log.info(TAG35, `HTTP server on port ${port}`);
8925
9293
  },
8926
9294
  check(message) {
8927
- log.info(TAG34, message);
9295
+ log.info(TAG35, message);
8928
9296
  },
8929
9297
  warn(message) {
8930
- log.warn(TAG34, message);
9298
+ log.warn(TAG35, message);
8931
9299
  },
8932
9300
  fail() {
8933
9301
  failed = true;
@@ -8935,7 +9303,7 @@ function jsonBanner(config, version) {
8935
9303
  async ready(message) {
8936
9304
  if (failed)
8937
9305
  return;
8938
- log.info(TAG34, message);
9306
+ log.info(TAG35, message);
8939
9307
  }
8940
9308
  };
8941
9309
  }
@@ -9016,7 +9384,7 @@ function cyan(s) {
9016
9384
  function yellow(s) {
9017
9385
  return `${ANSI.yellow}${s}${ANSI.reset}`;
9018
9386
  }
9019
- var TAG34 = "daemon", RULE_WIDTH = 70, ANSI;
9387
+ var TAG35 = "daemon", RULE_WIDTH = 70, ANSI;
9020
9388
  var init_startup_banner = __esm(() => {
9021
9389
  init_log();
9022
9390
  ANSI = {
@@ -9167,13 +9535,13 @@ class Watcher {
9167
9535
  }
9168
9536
  async start() {
9169
9537
  if (!isPretty()) {
9170
- log.info(TAG35, "Connecting to Supabase realtime (broadcast)...");
9538
+ log.info(TAG36, "Connecting to Supabase realtime (broadcast)...");
9171
9539
  }
9172
9540
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
9173
9541
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
9174
9542
  this.subscribeBroadcast();
9175
9543
  presenceChannel.on("presence", { event: "sync" }, () => {
9176
- log.debug(TAG35, "Presence sync");
9544
+ log.debug(TAG36, "Presence sync");
9177
9545
  }).subscribe(async (status) => {
9178
9546
  if (status === "SUBSCRIBED") {
9179
9547
  await presenceChannel.track({
@@ -9186,7 +9554,7 @@ class Watcher {
9186
9554
  agentName: this.identity.agentName
9187
9555
  });
9188
9556
  if (!isPretty() || !this.suppressStartupLogs) {
9189
- log.info(TAG35, "Presence tracked on board-presence channel");
9557
+ log.info(TAG36, "Presence tracked on board-presence channel");
9190
9558
  }
9191
9559
  this.presenceTracked = true;
9192
9560
  this.maybeResolveReady();
@@ -9199,13 +9567,13 @@ class Watcher {
9199
9567
  return;
9200
9568
  const gen = ++this.broadcastGen;
9201
9569
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
9202
- log.debug(TAG35, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9570
+ log.debug(TAG36, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
9203
9571
  this.onCardBroadcast({
9204
9572
  event: "card_update",
9205
9573
  payload: msg.payload ?? {}
9206
9574
  });
9207
9575
  }).on("broadcast", { event: "card_created" }, (msg) => {
9208
- log.debug(TAG35, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9576
+ log.debug(TAG36, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
9209
9577
  this.onCardBroadcast({
9210
9578
  event: "card_created",
9211
9579
  payload: msg.payload ?? {}
@@ -9215,7 +9583,7 @@ class Watcher {
9215
9583
  const cardId = payload.card_id;
9216
9584
  const command = payload.command;
9217
9585
  if (cardId && command) {
9218
- log.info(TAG35, `Broadcast: agent_command ${command} for ${cardId}`);
9586
+ log.info(TAG36, `Broadcast: agent_command ${command} for ${cardId}`);
9219
9587
  this.onAgentCommand?.({ cardId, command });
9220
9588
  }
9221
9589
  }).subscribe((status) => {
@@ -9225,13 +9593,13 @@ class Watcher {
9225
9593
  this.connected = true;
9226
9594
  this.reconnectAttempts = 0;
9227
9595
  if (!isPretty() || !this.suppressStartupLogs) {
9228
- log.info(TAG35, "Broadcast subscription active");
9596
+ log.info(TAG36, "Broadcast subscription active");
9229
9597
  }
9230
9598
  this.maybeResolveReady();
9231
9599
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
9232
9600
  this.connected = false;
9233
9601
  if (!this.stopping) {
9234
- log.warn(TAG35, `Broadcast subscription ${status} — scheduling reconnect`);
9602
+ log.warn(TAG36, `Broadcast subscription ${status} — scheduling reconnect`);
9235
9603
  this.scheduleReconnect();
9236
9604
  }
9237
9605
  }
@@ -9250,7 +9618,7 @@ class Watcher {
9250
9618
  async reconnectBroadcast() {
9251
9619
  if (this.stopping || !this.supabase)
9252
9620
  return;
9253
- log.warn(TAG35, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9621
+ log.warn(TAG36, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
9254
9622
  if (this.channel) {
9255
9623
  const old = this.channel;
9256
9624
  this.channel = null;
@@ -9280,10 +9648,10 @@ class Watcher {
9280
9648
  this.supabase = null;
9281
9649
  }
9282
9650
  this.connected = false;
9283
- log.info(TAG35, "Broadcast subscription stopped");
9651
+ log.info(TAG36, "Broadcast subscription stopped");
9284
9652
  }
9285
9653
  }
9286
- var TAG35 = "watcher";
9654
+ var TAG36 = "watcher";
9287
9655
  var init_watcher = __esm(() => {
9288
9656
  init_log();
9289
9657
  });
@@ -9370,10 +9738,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
9370
9738
  });
9371
9739
  } catch {}
9372
9740
  if (result.removed.length > 0) {
9373
- 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(", ")}`);
9374
9742
  }
9375
9743
  if (result.errors.length > 0) {
9376
- 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("; ")}`);
9377
9745
  }
9378
9746
  return result;
9379
9747
  }
@@ -9403,7 +9771,7 @@ function pruneFailedRemoteBranches(opts) {
9403
9771
  } catch (err) {
9404
9772
  const detail = gitErrorDetail2(err);
9405
9773
  if (isTransientGitNetworkError(detail)) {
9406
- log.debug(TAG36, `Remote branch GC skipped — remote unreachable: ${detail}`);
9774
+ log.debug(TAG37, `Remote branch GC skipped — remote unreachable: ${detail}`);
9407
9775
  return result;
9408
9776
  }
9409
9777
  result.errors.push({ ref: "fetch", error: detail });
@@ -9442,7 +9810,7 @@ function pruneFailedRemoteBranches(opts) {
9442
9810
  continue;
9443
9811
  }
9444
9812
  if (clock() > sweepDeadline) {
9445
- 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`);
9446
9814
  break;
9447
9815
  }
9448
9816
  try {
@@ -9455,17 +9823,17 @@ function pruneFailedRemoteBranches(opts) {
9455
9823
  } catch (err) {
9456
9824
  const detail = gitErrorDetail2(err);
9457
9825
  if (isTransientGitNetworkError(detail)) {
9458
- log.debug(TAG36, `Remote branch GC interrupted — remote unreachable: ${detail}`);
9826
+ log.debug(TAG37, `Remote branch GC interrupted — remote unreachable: ${detail}`);
9459
9827
  break;
9460
9828
  }
9461
9829
  result.errors.push({ ref, error: detail });
9462
9830
  }
9463
9831
  }
9464
9832
  if (result.removed.length > 0) {
9465
- 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(", ")}`);
9466
9834
  }
9467
9835
  if (result.errors.length > 0) {
9468
- 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("; ")}`);
9469
9837
  }
9470
9838
  return result;
9471
9839
  }
@@ -9496,13 +9864,13 @@ class WorktreeGc {
9496
9864
  try {
9497
9865
  runWorktreeGc(this.basePath, this.store);
9498
9866
  } catch (err) {
9499
- 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}`);
9500
9868
  }
9501
9869
  if (this.remoteOpts) {
9502
9870
  try {
9503
9871
  pruneFailedRemoteBranches(this.remoteOpts);
9504
9872
  } catch (err) {
9505
- 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}`);
9506
9874
  }
9507
9875
  }
9508
9876
  }
@@ -9516,7 +9884,7 @@ function getRepoRoot2() {
9516
9884
  return null;
9517
9885
  }
9518
9886
  }
9519
- 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;
9520
9888
  var init_worktree_gc = __esm(() => {
9521
9889
  init_log();
9522
9890
  init_worktree();
@@ -9620,7 +9988,7 @@ async function main() {
9620
9988
  } catch (err) {
9621
9989
  if (err instanceof ConfigValidationError) {
9622
9990
  banner.fail();
9623
- log.error(TAG37, err.message);
9991
+ log.error(TAG38, err.message);
9624
9992
  process.exit(1);
9625
9993
  }
9626
9994
  throw err;
@@ -9730,7 +10098,7 @@ async function main() {
9730
10098
  if (shuttingDown)
9731
10099
  return;
9732
10100
  shuttingDown = true;
9733
- log.info(TAG37, `Received ${signal}, shutting down gracefully...`);
10101
+ log.info(TAG38, `Received ${signal}, shutting down gracefully...`);
9734
10102
  reconciler.stop();
9735
10103
  mergeMonitor?.stop();
9736
10104
  worktreeGc.stop();
@@ -9740,18 +10108,18 @@ async function main() {
9740
10108
  }
9741
10109
  await watcher.stop();
9742
10110
  await pool.shutdown();
9743
- log.info(TAG37, "Daemon stopped.");
10111
+ log.info(TAG38, "Daemon stopped.");
9744
10112
  process.exit(exitCode);
9745
10113
  };
9746
10114
  process.on("SIGINT", () => shutdown("SIGINT"));
9747
10115
  process.on("SIGTERM", () => shutdown("SIGTERM"));
9748
10116
  process.on("uncaughtException", (err) => {
9749
- log.error(TAG37, `Uncaught exception: ${err.message}`);
10117
+ log.error(TAG38, `Uncaught exception: ${err.message}`);
9750
10118
  exitCode = 1;
9751
10119
  shutdown("uncaughtException");
9752
10120
  });
9753
10121
  process.on("unhandledRejection", (reason) => {
9754
- 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)}`);
9755
10123
  exitCode = 1;
9756
10124
  shutdown("unhandledRejection");
9757
10125
  });
@@ -9804,29 +10172,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
9804
10172
  if (assignedAgentId === undefined)
9805
10173
  return;
9806
10174
  if (assignedAgentId === agentId) {
9807
- log.info(TAG37, `Broadcast: card ${cardId} assigned to agent`);
10175
+ log.info(TAG38, `Broadcast: card ${cardId} assigned to agent`);
9808
10176
  try {
9809
10177
  await pool.resetAttemptsForReassign(cardId);
9810
10178
  await tryEnqueueCard(cardId, client, pool, config, agentId);
9811
10179
  } catch (err) {
9812
- 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}`);
9813
10181
  }
9814
10182
  } else if (pool.isCardKnown(cardId)) {
9815
- log.info(TAG37, `Broadcast: card ${cardId} unassigned from agent`);
10183
+ log.info(TAG38, `Broadcast: card ${cardId} unassigned from agent`);
9816
10184
  await pool.removeCard(cardId);
9817
10185
  }
9818
10186
  }
9819
10187
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
9820
10188
  const { card } = await client.getCard(cardId);
9821
10189
  if (card.assigned_agent_id !== agentId) {
9822
- log.debug(TAG37, `Card ${cardId} no longer assigned to agent — skipping`);
10190
+ log.debug(TAG38, `Card ${cardId} no longer assigned to agent — skipping`);
9823
10191
  return;
9824
10192
  }
9825
10193
  const board = await client.getBoard(config.projectId, { summary: true });
9826
10194
  const columns = board.columns;
9827
10195
  const column = columns.find((c) => c.id === card.column_id);
9828
10196
  if (!column) {
9829
- log.warn(TAG37, `Column not found for card ${cardId}`);
10197
+ log.warn(TAG38, `Column not found for card ${cardId}`);
9830
10198
  return;
9831
10199
  }
9832
10200
  const route = classifyPickup(card, column.name, {
@@ -9835,27 +10203,27 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
9835
10203
  playbooks: config.agent.playbooks
9836
10204
  });
9837
10205
  if (!route) {
9838
- 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`);
9839
10207
  return;
9840
10208
  }
9841
10209
  if (route.stage) {
9842
- 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`);
9843
10211
  }
9844
10212
  const mode = route.mode;
9845
10213
  const labelMap = buildLabelMap(board.labels ?? []);
9846
10214
  const cardLabels = resolveCardLabels(card, labelMap);
9847
10215
  const subtasks = card.subtasks ?? [];
9848
10216
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
9849
- 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`);
9850
10218
  return;
9851
10219
  }
9852
10220
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
9853
- 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`);
9854
10222
  return;
9855
10223
  }
9856
10224
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
9857
10225
  }
9858
- var TAG37 = "daemon", PKG_VERSION;
10226
+ var TAG38 = "daemon", PKG_VERSION;
9859
10227
  var init_src = __esm(() => {
9860
10228
  init_board_helpers();
9861
10229
  init_config();