@gethmy/agent 1.28.0 → 1.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1082 -457
  2. package/dist/index.js +1080 -455
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -783,6 +783,76 @@ var init_constants = __esm(() => {
783
783
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
784
784
  };
785
785
  });
786
+ // ../harmony-shared/dist/fanoutSource.js
787
+ function fanoutItemKey(item) {
788
+ return item.sourceId ? `src:${item.sourceId}` : `idx:${item.index}`;
789
+ }
790
+ function parseFanoutKeyMarker(description) {
791
+ if (typeof description !== "string")
792
+ return null;
793
+ const match = FANOUT_KEY_RE.exec(description);
794
+ const key = match?.[1]?.trim();
795
+ return key ? key : null;
796
+ }
797
+ function subtaskItems(subtasks) {
798
+ return [...subtasks].sort((a, b) => (a.position ?? 0) - (b.position ?? 0)).map((s) => ({ label: s.title ?? "", sourceId: s.id }));
799
+ }
800
+ function parseMarkdownListItems(text) {
801
+ if (typeof text !== "string" || !text)
802
+ return [];
803
+ const items = [];
804
+ for (const rawLine of text.split(`
805
+ `)) {
806
+ const match = /^ {0,1}(?:[-*+]|\d+[.)])\s+(.*)$/.exec(rawLine);
807
+ if (!match)
808
+ continue;
809
+ const withoutCheckbox = match[1].replace(/^\[[ xX]\]\s*/, "");
810
+ const entry = withoutCheckbox.trim();
811
+ if (entry)
812
+ items.push(entry);
813
+ }
814
+ return items;
815
+ }
816
+ function checklistItemsFromDescription(description, field) {
817
+ if (typeof description !== "string" || !description)
818
+ return [];
819
+ const wanted = field.trim().toLowerCase();
820
+ if (!wanted)
821
+ return [];
822
+ const lines = description.split(`
823
+ `);
824
+ const headingAt = lines.findIndex((line) => {
825
+ const heading = /^#{1,6}\s+(.*)$/.exec(line);
826
+ if (!heading)
827
+ return false;
828
+ const text = heading[1].replace(/[*_`]/g, "").replace(/[::]\s*$/, "").trim().toLowerCase();
829
+ return text === wanted;
830
+ });
831
+ if (headingAt < 0)
832
+ return [];
833
+ const body = [];
834
+ for (let i = headingAt + 1;i < lines.length; i++) {
835
+ if (/^#{1,6}\s+/.test(lines[i]))
836
+ break;
837
+ body.push(lines[i]);
838
+ }
839
+ return parseMarkdownListItems(body.join(`
840
+ `)).map((label) => ({ label }));
841
+ }
842
+ function handoffListItems(handoff) {
843
+ if (!handoff)
844
+ return [];
845
+ const explicit = Array.isArray(handoff.producedItems) ? handoff.producedItems.filter((s) => typeof s === "string").map((s) => s.trim()).filter(Boolean) : [];
846
+ if (explicit.length > 0)
847
+ return explicit.map((label) => ({ label }));
848
+ return parseMarkdownListItems(handoff.produced ?? "").map((label) => ({
849
+ label
850
+ }));
851
+ }
852
+ var FANOUT_KEY_MARKER = "harmony:fanout-item", FANOUT_KEY_RE;
853
+ var init_fanoutSource = __esm(() => {
854
+ FANOUT_KEY_RE = new RegExp(`^\\[${FANOUT_KEY_MARKER}\\]:\\s*#(\\S+)\\s*$`, "m");
855
+ });
786
856
  // ../harmony-shared/dist/gateConfigError.js
787
857
  var GATE_CONFIG_ERROR_KEY = "configError", GATE_CONFIG_ERROR_MARK;
788
858
  var init_gateConfigError = __esm(() => {
@@ -1185,6 +1255,78 @@ function getStageLoop(stage) {
1185
1255
  function isConvergeLoop(loop) {
1186
1256
  return loop !== null && loop.mode === "converge";
1187
1257
  }
1258
+ function isFanoutLoop(loop) {
1259
+ return loop !== null && loop.mode === "fanout";
1260
+ }
1261
+ function normalizeItemSource(raw) {
1262
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
1263
+ return null;
1264
+ const obj = raw;
1265
+ switch (obj.kind) {
1266
+ case "subtasks":
1267
+ return { kind: "subtasks" };
1268
+ case "list_handoff": {
1269
+ const from = typeof obj.from_stage === "string" ? obj.from_stage.trim() : "";
1270
+ return from ? { kind: "list_handoff", from_stage: from } : null;
1271
+ }
1272
+ case "checklist": {
1273
+ const field = typeof obj.field === "string" ? obj.field.trim() : "";
1274
+ return field ? { kind: "checklist", field } : null;
1275
+ }
1276
+ default:
1277
+ return null;
1278
+ }
1279
+ }
1280
+ function getLoopItemSource(loop) {
1281
+ return normalizeItemSource(loop.item_source);
1282
+ }
1283
+ function resolveLoopConcurrency(loop) {
1284
+ const raw = loop.concurrency;
1285
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 1) {
1286
+ return DEFAULT_LOOP_CONCURRENCY;
1287
+ }
1288
+ return Math.floor(raw);
1289
+ }
1290
+ function resolveOnItemFail(loop) {
1291
+ return loop.on_item_fail ?? DEFAULT_ON_ITEM_FAIL;
1292
+ }
1293
+ function planFanoutItems(raw, loop) {
1294
+ const max = Math.max(1, Math.floor(loop.max_iterations) || 1);
1295
+ let invalid = 0;
1296
+ const valid = [];
1297
+ for (const entry of raw) {
1298
+ const label = typeof entry?.label === "string" ? entry.label.trim() : "";
1299
+ if (!label) {
1300
+ invalid += 1;
1301
+ continue;
1302
+ }
1303
+ valid.push({ ...entry, label });
1304
+ }
1305
+ const kept = valid.slice(0, max);
1306
+ const items = kept.map((entry, index) => {
1307
+ const item = { index, label: entry.label };
1308
+ const detail = typeof entry.detail === "string" ? entry.detail.trim() : "";
1309
+ if (detail)
1310
+ item.detail = detail;
1311
+ const sourceId = typeof entry.sourceId === "string" ? entry.sourceId.trim() : "";
1312
+ if (sourceId)
1313
+ item.sourceId = sourceId;
1314
+ return item;
1315
+ });
1316
+ return {
1317
+ items,
1318
+ total: valid.length,
1319
+ truncated: valid.length - kept.length,
1320
+ invalid
1321
+ };
1322
+ }
1323
+ function decideFanoutAggregation(args) {
1324
+ const { loop, outcomes } = args;
1325
+ if (resolveOnItemFail(loop) === "halt" && outcomes.some((o) => o === "failed")) {
1326
+ return "halt";
1327
+ }
1328
+ return outcomes.every((o) => o !== "pending") ? "complete" : "pending";
1329
+ }
1188
1330
  function resolveLoopExitGate(stage, loop) {
1189
1331
  return loop.exit_gate ?? stage.gate ?? null;
1190
1332
  }
@@ -1289,7 +1431,7 @@ function referencedGateMetrics(def) {
1289
1431
  }
1290
1432
  return out;
1291
1433
  }
1292
- var DEFAULT_LOOP_MAX_ITERATIONS = 5, PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
1434
+ var DEFAULT_LOOP_MAX_ITERATIONS = 5, DEFAULT_LOOP_CONCURRENCY = 1, DEFAULT_ON_ITEM_FAIL = "continue", PLAYBOOK_STAGE_ROLES, SKILL_TOOL_ALLOWLIST, HARMONY_TOOL_RE, STAGE_DAEMON_OWNED_TOOLS;
1293
1435
  var init_playbookStage = __esm(() => {
1294
1436
  PLAYBOOK_STAGE_ROLES = [
1295
1437
  "author",
@@ -1474,6 +1616,12 @@ var init_reviewTools = __esm(() => {
1474
1616
  ];
1475
1617
  });
1476
1618
  // ../harmony-shared/dist/stageHandoff.js
1619
+ function isFanoutHandoffItem(value) {
1620
+ if (typeof value !== "object" || value === null)
1621
+ return false;
1622
+ const v = value;
1623
+ return typeof v.parentCardId === "string" && typeof v.stageId === "string" && typeof v.index === "number" && Number.isFinite(v.index) && typeof v.total === "number" && Number.isFinite(v.total) && typeof v.label === "string" && (v.detail === undefined || typeof v.detail === "string") && (v.sourceId === undefined || typeof v.sourceId === "string");
1624
+ }
1477
1625
  function buildHandoffCommentBody(input) {
1478
1626
  const handoff = {
1479
1627
  version: STAGE_HANDOFF_VERSION,
@@ -1485,11 +1633,22 @@ function buildHandoffCommentBody(input) {
1485
1633
  nextStageNeeds: input.nextStageNeeds,
1486
1634
  producedAt: input.producedAt ?? new Date().toISOString()
1487
1635
  };
1636
+ if (input.fanoutItem)
1637
+ handoff.fanoutItem = input.fanoutItem;
1638
+ if (input.producedItems && input.producedItems.length > 0) {
1639
+ handoff.producedItems = input.producedItems;
1640
+ }
1488
1641
  const decisionLines = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
1489
1642
  `) : "_None._";
1643
+ const item = handoff.fanoutItem;
1490
1644
  const prose = [
1491
1645
  `**Stage handoff — ${handoff.stageName}**`,
1492
1646
  "",
1647
+ ...item ? [
1648
+ `**Your item (${item.index + 1} of ${item.total}):** ${item.label}`,
1649
+ ...item.detail ? ["", item.detail] : [],
1650
+ ""
1651
+ ] : [],
1493
1652
  `**Produced:** ${handoff.produced}`,
1494
1653
  "",
1495
1654
  "**Decisions (settled — do not re-litigate):**",
@@ -1521,7 +1680,18 @@ function parseHandoffCommentBody(body) {
1521
1680
  return null;
1522
1681
  try {
1523
1682
  const parsed = JSON.parse(match[1]);
1524
- return isTypedStageHandoff(parsed) ? parsed : null;
1683
+ if (!isTypedStageHandoff(parsed))
1684
+ return null;
1685
+ const cleaned = { ...parsed };
1686
+ if (cleaned.fanoutItem !== undefined && !isFanoutHandoffItem(cleaned.fanoutItem)) {
1687
+ delete cleaned.fanoutItem;
1688
+ }
1689
+ if (cleaned.producedItems !== undefined) {
1690
+ if (!Array.isArray(cleaned.producedItems) || !cleaned.producedItems.every((s) => typeof s === "string")) {
1691
+ delete cleaned.producedItems;
1692
+ }
1693
+ }
1694
+ return cleaned;
1525
1695
  } catch {
1526
1696
  return null;
1527
1697
  }
@@ -1547,6 +1717,25 @@ function extractLatestHandoff(comments, identity, opts = {}) {
1547
1717
  function renderInheritedHandoffSection(handoff) {
1548
1718
  const decisions = handoff.decisions.length > 0 ? handoff.decisions.map((d) => `- ${d}`).join(`
1549
1719
  `) : "- (none recorded)";
1720
+ const item = handoff.fanoutItem;
1721
+ if (item) {
1722
+ return [
1723
+ "## Your fan-out item",
1724
+ "",
1725
+ `You are one of ${item.total} cards working this stage in parallel. Do **only** your own item — the others are being handled on their own cards.`,
1726
+ "",
1727
+ `**Item ${item.index + 1} of ${item.total}:** ${item.label}`,
1728
+ ...item.detail ? ["", item.detail] : [],
1729
+ "",
1730
+ `**Context from the dispatching stage:** ${handoff.produced}`,
1731
+ "",
1732
+ "**Decisions you must respect:**",
1733
+ decisions,
1734
+ "",
1735
+ `**What you need to do with it:** ${handoff.nextStageNeeds}`
1736
+ ].join(`
1737
+ `);
1738
+ }
1550
1739
  return [
1551
1740
  "## Inherited handoff (from the previous stage)",
1552
1741
  "",
@@ -1579,6 +1768,7 @@ var init_dist = __esm(() => {
1579
1768
  init_columnSort();
1580
1769
  init_commentSerializer();
1581
1770
  init_constants();
1771
+ init_fanoutSource();
1582
1772
  init_gateConfigError();
1583
1773
  init_gateEvaluate();
1584
1774
  init_logger();
@@ -3380,6 +3570,40 @@ var init_episode_writer = __esm(() => {
3380
3570
  INSIGHT_RE = /\b(root cause|turned out|the (?:issue|problem|bug) (?:was|is)|the fix (?:was|is)|gotcha|caused by|because|the key (?:was|insight)|note that|caveat|the trick (?:was|is))\b/i;
3381
3571
  });
3382
3572
 
3573
+ // src/run-closeout.ts
3574
+ import { log as log9 } from "@gethmy/harness";
3575
+ async function transferCardToCompletion(deps, card, moveToColumn, onPromoted) {
3576
+ await moveCardToColumn(deps.client, card, moveToColumn);
3577
+ try {
3578
+ await releaseAssignedAgent(deps.client, card.id);
3579
+ } catch (err) {
3580
+ log9.warn(deps.tag, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3581
+ }
3582
+ if (onPromoted) {
3583
+ try {
3584
+ await onPromoted(card);
3585
+ } catch (err) {
3586
+ log9.warn(deps.tag, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3587
+ }
3588
+ }
3589
+ }
3590
+ async function endRunSession(deps, card, disposition, extraPayload, onError) {
3591
+ try {
3592
+ await deps.client.endAgentSession(card.id, {
3593
+ ...disposition,
3594
+ progressPercent: 100,
3595
+ ...extraPayload
3596
+ });
3597
+ } catch (err) {
3598
+ if (onError === "throw")
3599
+ throw err;
3600
+ log9.error(deps.tag, `endAgentSession after the run failed on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3601
+ }
3602
+ }
3603
+ var init_run_closeout = __esm(() => {
3604
+ init_board_helpers();
3605
+ });
3606
+
3383
3607
  // src/completion.ts
3384
3608
  import { execFileSync as execFileSync3 } from "node:child_process";
3385
3609
  import {
@@ -3388,7 +3612,7 @@ import {
3388
3612
  createPullRequest,
3389
3613
  detectGitProvider as detectGitProvider3,
3390
3614
  getBranchWebUrl,
3391
- log as log9,
3615
+ log as log10,
3392
3616
  pushBranch,
3393
3617
  reportFindings,
3394
3618
  runFormatFix,
@@ -3425,7 +3649,7 @@ function buildTokenPayload(stats) {
3425
3649
  numTurns: stats.cost.numTurns
3426
3650
  };
3427
3651
  }
3428
- async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
3652
+ async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionIdentifier, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion, onBeforeWorktreeCleanup, runBaselineSha, effectiveMaxTurns) {
3429
3653
  let verificationResult = {
3430
3654
  passed: true,
3431
3655
  buildErrors: [],
@@ -3442,10 +3666,10 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3442
3666
  if (!hasCommits) {
3443
3667
  const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, effectiveMaxTurns ?? config.claude.maxTurns);
3444
3668
  if (noCommitOutcome(maxTurnsExhausted, config.budget.pause.enabled) === "park") {
3445
- log9.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; parking for a decision`);
3669
+ log10.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; parking for a decision`);
3446
3670
  return "park";
3447
3671
  }
3448
- log9.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
3672
+ log10.warn(TAG9, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
3449
3673
  await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
3450
3674
  await client.endAgentSession(card.id, {
3451
3675
  status: "failed",
@@ -3456,18 +3680,18 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3456
3680
  await teardownWorktree(client, card.id, worktreePath, branchName);
3457
3681
  return false;
3458
3682
  }
3459
- log9.info(TAG9, `Pushing branch ${branchName} (pre-verify)...`);
3683
+ log10.info(TAG9, `Pushing branch ${branchName} (pre-verify)...`);
3460
3684
  let lastPushedSha = null;
3461
3685
  try {
3462
3686
  pushBranch(branchName, worktreePath);
3463
3687
  lastPushedSha = readHeadSha(worktreePath);
3464
3688
  } catch (err) {
3465
- log9.error(TAG9, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3689
+ log10.error(TAG9, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3466
3690
  }
3467
3691
  const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
3468
3692
  if (config.verification.enabled) {
3469
3693
  await client.updateAgentProgress(card.id, {
3470
- agentIdentifier: agentIdentifier(workerId),
3694
+ agentIdentifier: sessionIdentifier,
3471
3695
  agentName: AGENT_NAME,
3472
3696
  status: "working",
3473
3697
  currentTask: "Verifying build...",
@@ -3477,9 +3701,9 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3477
3701
  let autoFixAttempts = 0;
3478
3702
  if (!result.passed && config.verification.autoFix) {
3479
3703
  for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
3480
- log9.info(TAG9, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
3704
+ log10.info(TAG9, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
3481
3705
  await client.updateAgentProgress(card.id, {
3482
- agentIdentifier: agentIdentifier(workerId),
3706
+ agentIdentifier: sessionIdentifier,
3483
3707
  agentName: AGENT_NAME,
3484
3708
  status: "working",
3485
3709
  currentTask: `Fixing issues (attempt ${attempt + 1})...`,
@@ -3494,14 +3718,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3494
3718
  result = await runVerification(worktreePath, config, workerId);
3495
3719
  autoFixAttempts = attempt + 1;
3496
3720
  if (result.passed) {
3497
- log9.info(TAG9, `Auto-fix succeeded on attempt ${attempt + 1}`);
3721
+ log10.info(TAG9, `Auto-fix succeeded on attempt ${attempt + 1}`);
3498
3722
  const sha = readHeadSha(worktreePath);
3499
3723
  if (sha && sha !== lastPushedSha) {
3500
3724
  try {
3501
3725
  pushBranch(branchName, worktreePath);
3502
3726
  lastPushedSha = sha;
3503
3727
  } catch (err) {
3504
- log9.warn(TAG9, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3728
+ log10.warn(TAG9, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3505
3729
  }
3506
3730
  }
3507
3731
  break;
@@ -3510,14 +3734,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3510
3734
  }
3511
3735
  verificationResult = result;
3512
3736
  if (!result.passed) {
3513
- log9.warn(TAG9, `Verification failed for #${card.short_id} — reporting findings`);
3737
+ log10.warn(TAG9, `Verification failed for #${card.short_id} — reporting findings`);
3514
3738
  const failSha = readHeadSha(worktreePath);
3515
3739
  if (failSha && failSha !== lastPushedSha) {
3516
3740
  try {
3517
3741
  pushBranch(branchName, worktreePath);
3518
3742
  lastPushedSha = failSha;
3519
3743
  } catch (err) {
3520
- log9.warn(TAG9, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3744
+ log10.warn(TAG9, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
3521
3745
  }
3522
3746
  }
3523
3747
  const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
@@ -3528,7 +3752,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3528
3752
  recoveryBranch: branchName
3529
3753
  });
3530
3754
  } catch (err) {
3531
- log9.debug(TAG9, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
3755
+ log10.debug(TAG9, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
3532
3756
  }
3533
3757
  await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
3534
3758
  await moveCardToColumn(client, card, config.verification.failColumn);
@@ -3542,7 +3766,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3542
3766
  await teardownWorktree(client, card.id, worktreePath, branchName);
3543
3767
  return false;
3544
3768
  }
3545
- log9.info(TAG9, `Verification passed for #${card.short_id}`);
3769
+ log10.info(TAG9, `Verification passed for #${card.short_id}`);
3546
3770
  }
3547
3771
  let prUrl = null;
3548
3772
  if (config.completion.createPR) {
@@ -3550,19 +3774,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3550
3774
  prUrl = createPullRequest(card, branchName, worktreePath, config, provider);
3551
3775
  }
3552
3776
  if (config.completion.moveToColumn) {
3553
- await moveCardToColumn(client, card, config.completion.moveToColumn);
3554
- try {
3555
- await releaseAssignedAgent(client, card.id);
3556
- } catch (err) {
3557
- log9.warn(TAG9, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3558
- }
3559
- if (onMovedToCompletion) {
3560
- try {
3561
- await onMovedToCompletion(card);
3562
- } catch (err) {
3563
- log9.warn(TAG9, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3564
- }
3565
- }
3777
+ await transferCardToCompletion({ client, tag: TAG9 }, card, config.completion.moveToColumn, onMovedToCompletion);
3566
3778
  }
3567
3779
  if (config.completion.postSummary) {
3568
3780
  await postSummary(client, card, branchName, worktreePath, prUrl, config.worktree.baseBranch, sessionStats);
@@ -3574,14 +3786,10 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3574
3786
  if (disposition)
3575
3787
  endDisposition = disposition;
3576
3788
  } catch (err) {
3577
- log9.warn(TAG9, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3789
+ log10.warn(TAG9, `onBeforeWorktreeCleanup hook failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3578
3790
  }
3579
3791
  }
3580
- await client.endAgentSession(card.id, {
3581
- ...endDisposition,
3582
- progressPercent: 100,
3583
- ...buildTokenPayload(sessionStats)
3584
- });
3792
+ await endRunSession({ client, tag: TAG9 }, card, endDisposition, buildTokenPayload(sessionStats), "throw");
3585
3793
  if (workspaceId) {
3586
3794
  const diffStat = captureDiffStat(worktreePath, config.worktree.baseBranch);
3587
3795
  const changedFiles = diffStat && diffStat.files.length > 0 ? diffStat.files : sessionStats?.filesEditedPaths ?? [];
@@ -3604,7 +3812,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
3604
3812
  });
3605
3813
  }
3606
3814
  await teardownWorktree(client, card.id, worktreePath, branchName);
3607
- log9.info(TAG9, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
3815
+ log10.info(TAG9, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
3608
3816
  return true;
3609
3817
  }
3610
3818
  function buildVerificationFailureSummary(result, autoFixAttempts) {
@@ -3646,7 +3854,7 @@ function commitUncommittedChanges(worktreePath, card) {
3646
3854
  encoding: "utf-8"
3647
3855
  }).trim();
3648
3856
  } catch (err) {
3649
- log9.warn(TAG9, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
3857
+ log10.warn(TAG9, `git status failed in ${worktreePath}: ${err instanceof Error ? err.message : err}`);
3650
3858
  return false;
3651
3859
  }
3652
3860
  if (status.length === 0)
@@ -3662,10 +3870,10 @@ function commitUncommittedChanges(worktreePath, card) {
3662
3870
  cwd: worktreePath,
3663
3871
  encoding: "utf-8"
3664
3872
  });
3665
- log9.warn(TAG9, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
3873
+ log10.warn(TAG9, `Auto-committed uncommitted worktree changes for #${card.short_id} — agent ended without committing`);
3666
3874
  return true;
3667
3875
  } catch (err) {
3668
- log9.error(TAG9, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3876
+ log10.error(TAG9, `auto-commit failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3669
3877
  return false;
3670
3878
  }
3671
3879
  }
@@ -3733,20 +3941,21 @@ ${commitLog}
3733
3941
  description: baseDesc + parts.join(`
3734
3942
  `)
3735
3943
  });
3736
- log9.info(TAG9, `Posted completion summary to #${card.short_id}`);
3944
+ log10.info(TAG9, `Posted completion summary to #${card.short_id}`);
3737
3945
  } catch (err) {
3738
- log9.error(TAG9, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
3946
+ log10.error(TAG9, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
3739
3947
  }
3740
3948
  }
3741
3949
  var TAG9 = "completion";
3742
3950
  var init_completion = __esm(() => {
3743
3951
  init_board_helpers();
3744
3952
  init_episode_writer();
3953
+ init_run_closeout();
3745
3954
  init_types2();
3746
3955
  });
3747
3956
 
3748
3957
  // src/progress-tracker.ts
3749
- import { log as log10 } from "@gethmy/harness";
3958
+ import { log as log11 } from "@gethmy/harness";
3750
3959
  function truncate(str, max) {
3751
3960
  return str.length > max ? `${str.slice(0, max - 3)}...` : str;
3752
3961
  }
@@ -3754,7 +3963,7 @@ function truncate(str, max) {
3754
3963
  class ProgressTracker {
3755
3964
  client;
3756
3965
  cardId;
3757
- workerId;
3966
+ sessionIdentifier;
3758
3967
  phase = "exploring";
3759
3968
  progress = 10;
3760
3969
  toolCallCount = 0;
@@ -3776,10 +3985,10 @@ class ProgressTracker {
3776
3985
  lastEmittedProgress = -1;
3777
3986
  lastAssistantText = "";
3778
3987
  assistantTextBlocks = [];
3779
- constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
3988
+ constructor(client, cardId, sessionIdentifier, subtasks, initialPhase = "exploring") {
3780
3989
  this.client = client;
3781
3990
  this.cardId = cardId;
3782
- this.workerId = workerId;
3991
+ this.sessionIdentifier = sessionIdentifier;
3783
3992
  this.subtaskTotal = subtasks.length;
3784
3993
  this.subtaskCompleted = subtasks.filter((s) => s.completed).length;
3785
3994
  this.subtaskMode = subtasks.length > 0;
@@ -3865,7 +4074,7 @@ class ProgressTracker {
3865
4074
  }
3866
4075
  onToolStart(name, input) {
3867
4076
  this.toolCallCount++;
3868
- log10.debug(TAG10, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
4077
+ log11.debug(TAG10, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
3869
4078
  const filePath = this.extractString(input, "file_path");
3870
4079
  if (filePath) {
3871
4080
  if (EDIT_TOOLS.has(name)) {
@@ -3936,7 +4145,7 @@ class ProgressTracker {
3936
4145
  transitionTo(newPhase) {
3937
4146
  if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
3938
4147
  return;
3939
- log10.info(TAG10, `Phase: ${this.phase} → ${newPhase}`);
4148
+ log11.info(TAG10, `Phase: ${this.phase} → ${newPhase}`);
3940
4149
  const previousPhase = this.phase;
3941
4150
  this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
3942
4151
  this.phase = newPhase;
@@ -4041,9 +4250,9 @@ class ProgressTracker {
4041
4250
  }
4042
4251
  sendUpdate(currentTask) {
4043
4252
  this.lastUpdateAt = Date.now();
4044
- log10.debug(TAG10, `Progress: ${this.progress}% — ${currentTask}`);
4253
+ log11.debug(TAG10, `Progress: ${this.progress}% — ${currentTask}`);
4045
4254
  this.client.updateAgentProgress(this.cardId, {
4046
- agentIdentifier: agentIdentifier(this.workerId),
4255
+ agentIdentifier: this.sessionIdentifier,
4047
4256
  agentName: AGENT_NAME,
4048
4257
  status: "working",
4049
4258
  currentTask: truncate(currentTask, MAX_TASK_LENGTH),
@@ -4058,7 +4267,7 @@ class ProgressTracker {
4058
4267
  modelName: this.lastCost?.modelName ?? this.requestedModel ?? undefined,
4059
4268
  numTurns: this.lastCost?.numTurns ?? 0
4060
4269
  }).catch((err) => {
4061
- log10.warn(TAG10, `Failed to send progress update: ${err}`);
4270
+ log11.warn(TAG10, `Failed to send progress update: ${err}`);
4062
4271
  });
4063
4272
  if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
4064
4273
  this.lastEmittedProgress = this.progress;
@@ -4123,7 +4332,7 @@ var init_progress_tracker = __esm(() => {
4123
4332
  });
4124
4333
 
4125
4334
  // src/prompt.ts
4126
- import { log as log11 } from "@gethmy/harness";
4335
+ import { log as log12 } from "@gethmy/harness";
4127
4336
  function buildSteeringPrompt(messages) {
4128
4337
  if (messages.length === 1)
4129
4338
  return messages[0];
@@ -4160,11 +4369,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
4160
4369
  Do NOT push to main. All your work stays on \`${branchName}\`.
4161
4370
  The daemon owns the run lifecycle: once your work is committed it ends the agent session, pushes the branch, and moves the card to Review for you. Do NOT call harmony_end_agent_session, do NOT start a new session, and do NOT move the card or change its column yourself. If the skill driving this work tells you to move the card or end the session as a final step, SKIP it — it is handled for you (those tools are disabled for this run). Finish the implementation, commit, and stop.`
4162
4371
  });
4163
- log11.info(TAG11, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4372
+ log12.info(TAG11, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
4164
4373
  return result.prompt + pastEpisodesSection + referenceSection;
4165
4374
  } catch (err) {
4166
4375
  const msg = err instanceof Error ? err.message : String(err);
4167
- log11.warn(TAG11, `Failed to generate prompt via API, using fallback: ${msg}`);
4376
+ log12.warn(TAG11, `Failed to generate prompt via API, using fallback: ${msg}`);
4168
4377
  const commentsSection = await renderCommentsSection(client, card.id);
4169
4378
  return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection + referenceSection;
4170
4379
  }
@@ -4182,7 +4391,7 @@ async function renderCommentsSection(client, cardId) {
4182
4391
 
4183
4392
  ${section}` : "";
4184
4393
  } catch (err) {
4185
- log11.warn(TAG11, "comment-thread fetch failed", {
4394
+ log12.warn(TAG11, "comment-thread fetch failed", {
4186
4395
  event: "comment_fetch_failed",
4187
4396
  error: err instanceof Error ? err.message : String(err)
4188
4397
  });
@@ -4234,7 +4443,7 @@ ${description}`.trim();
4234
4443
  ## Similar past tasks
4235
4444
  ${bullets}`;
4236
4445
  } catch (err) {
4237
- log11.warn(TAG11, "past-episodes recall failed", {
4446
+ log12.warn(TAG11, "past-episodes recall failed", {
4238
4447
  event: "episode_recall_failed",
4239
4448
  error: err instanceof Error ? err.message : String(err)
4240
4449
  });
@@ -4267,7 +4476,7 @@ ${description}`.trim();
4267
4476
  ## How we work here
4268
4477
  ${bullets}`;
4269
4478
  } catch (err) {
4270
- log11.warn(TAG11, "reference recall failed", {
4479
+ log12.warn(TAG11, "reference recall failed", {
4271
4480
  event: "reference_recall_failed",
4272
4481
  error: err instanceof Error ? err.message : String(err)
4273
4482
  });
@@ -4322,7 +4531,7 @@ import {
4322
4531
  extractPrUrl as extractPrUrl2,
4323
4532
  getBranchWebUrl as getBranchWebUrl2,
4324
4533
  getHeadSha,
4325
- log as log12,
4534
+ log as log13,
4326
4535
  pushBranch as pushBranch2,
4327
4536
  renameRemoteBranch,
4328
4537
  upsertReviewedSha
@@ -4447,7 +4656,7 @@ function parseReviewOutput(stdout) {
4447
4656
  try {
4448
4657
  const parsed = JSON.parse(raw);
4449
4658
  if (parsed && typeof parsed === "object" && "verdict" in parsed) {
4450
- log12.debug(TAG12, "Parsed review output from fenced JSON block");
4659
+ log13.debug(TAG12, "Parsed review output from fenced JSON block");
4451
4660
  return extractResult(parsed);
4452
4661
  }
4453
4662
  } catch {}
@@ -4473,21 +4682,21 @@ function parseReviewOutput(stdout) {
4473
4682
  try {
4474
4683
  const parsed = JSON.parse(candidates[i]);
4475
4684
  if (parsed && typeof parsed === "object" && "verdict" in parsed) {
4476
- log12.debug(TAG12, "Parsed review output from raw JSON object");
4685
+ log13.debug(TAG12, "Parsed review output from raw JSON object");
4477
4686
  return extractResult(parsed);
4478
4687
  }
4479
4688
  } catch {}
4480
4689
  }
4481
4690
  const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
4482
4691
  if (verdictMatch) {
4483
- log12.warn(TAG12, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
4692
+ log13.warn(TAG12, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
4484
4693
  return {
4485
4694
  verdict: verdictMatch[1].toLowerCase(),
4486
4695
  summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
4487
4696
  findings: []
4488
4697
  };
4489
4698
  }
4490
- log12.warn(TAG12, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
4699
+ log13.warn(TAG12, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
4491
4700
  return {
4492
4701
  verdict: "error",
4493
4702
  summary: stdout.slice(0, 500),
@@ -4520,7 +4729,7 @@ async function postReviewComment(client, card, commentType, body) {
4520
4729
  try {
4521
4730
  await client.addComment(card.id, body, { commentType });
4522
4731
  } catch (err) {
4523
- log12.error(TAG12, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4732
+ log13.error(TAG12, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4524
4733
  }
4525
4734
  }
4526
4735
  async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore, resolvedFromPrUrl) {
@@ -4534,11 +4743,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
4534
4743
  const currentCycle = getReviewCycle(freshDesc) + 1;
4535
4744
  const maxCycles = config.review.maxReviewCycles;
4536
4745
  if (result.verdict === "error") {
4537
- log12.warn(TAG12, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
4746
+ log13.warn(TAG12, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
4538
4747
  try {
4539
4748
  await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
4540
4749
  } catch (err) {
4541
- log12.warn(TAG12, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
4750
+ log13.warn(TAG12, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
4542
4751
  }
4543
4752
  if (config.review.postFindings) {
4544
4753
  const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
@@ -4581,7 +4790,7 @@ ${runLogTail}
4581
4790
  renameRemoteBranch(branchName, newRef, worktreePath);
4582
4791
  approvedBranch = newRef;
4583
4792
  } catch (err) {
4584
- log12.warn(TAG12, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
4793
+ log13.warn(TAG12, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
4585
4794
  }
4586
4795
  }
4587
4796
  if (config.review.createPR && approvedBranch) {
@@ -4602,14 +4811,14 @@ ${runLogTail}
4602
4811
  });
4603
4812
  }
4604
4813
  } catch (err) {
4605
- log12.warn(TAG12, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
4814
+ log13.warn(TAG12, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
4606
4815
  }
4607
4816
  }
4608
4817
  if (branchName) {
4609
4818
  try {
4610
4819
  await persistReviewedSha(client, card, worktreePath);
4611
4820
  } catch (err) {
4612
- log12.warn(TAG12, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4821
+ log13.warn(TAG12, `Failed to persist Reviewed-SHA to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4613
4822
  }
4614
4823
  }
4615
4824
  if (config.review.postFindings) {
@@ -4631,7 +4840,7 @@ ${runLogTail}
4631
4840
  progressPercent: 100,
4632
4841
  ...buildTokenPayload(sessionStats)
4633
4842
  });
4634
- log12.info(TAG12, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
4843
+ log13.info(TAG12, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
4635
4844
  } else {
4636
4845
  const reworkFindings = result.findings.filter((f) => f.relatedToDiff !== false);
4637
4846
  const criticalFindings = reworkFindings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
@@ -4639,7 +4848,7 @@ ${runLogTail}
4639
4848
  const linkedFindings = [...criticalFindings, ...majorFindings];
4640
4849
  const minorFindings = reworkFindings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
4641
4850
  if (currentCycle >= maxCycles) {
4642
- log12.warn(TAG12, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
4851
+ log13.warn(TAG12, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
4643
4852
  await moveCardToColumn(client, card, config.review.moveToColumn);
4644
4853
  const body = [
4645
4854
  "**Review — needs human review.**",
@@ -4679,7 +4888,7 @@ ${runLogTail}
4679
4888
  try {
4680
4889
  await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
4681
4890
  } catch (err) {
4682
- log12.error(TAG12, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
4891
+ log13.error(TAG12, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
4683
4892
  }
4684
4893
  }));
4685
4894
  if (linkedFindings.length > 0) {
@@ -4691,7 +4900,7 @@ ${runLogTail}
4691
4900
  try {
4692
4901
  await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
4693
4902
  } catch (err) {
4694
- log12.error(TAG12, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
4903
+ log13.error(TAG12, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
4695
4904
  }
4696
4905
  }));
4697
4906
  const baseDesc = stripReviewSummary(freshDesc);
@@ -4699,7 +4908,7 @@ ${runLogTail}
4699
4908
  try {
4700
4909
  await client.updateCard(card.id, { description: updatedDesc });
4701
4910
  } catch (err) {
4702
- log12.error(TAG12, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
4911
+ log13.error(TAG12, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
4703
4912
  }
4704
4913
  const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
4705
4914
  const body = [
@@ -4716,9 +4925,9 @@ ${runLogTail}
4716
4925
  if (config.planning.enabled && card.plan_id) {
4717
4926
  try {
4718
4927
  await client.updateCard(card.id, { needsPlanRefresh: true });
4719
- log12.info(TAG12, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
4928
+ log13.info(TAG12, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
4720
4929
  } catch (err) {
4721
- log12.warn(TAG12, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4930
+ log13.warn(TAG12, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
4722
4931
  }
4723
4932
  }
4724
4933
  await moveCardToColumn(client, card, config.review.failColumn);
@@ -4732,10 +4941,10 @@ ${runLogTail}
4732
4941
  recoveryBranch
4733
4942
  });
4734
4943
  } catch (err) {
4735
- log12.debug(TAG12, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
4944
+ log13.debug(TAG12, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
4736
4945
  }
4737
4946
  if (recoveryBranch) {
4738
- log12.info(TAG12, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
4947
+ log13.info(TAG12, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
4739
4948
  }
4740
4949
  await client.endAgentSession(card.id, {
4741
4950
  status: "failed",
@@ -4744,7 +4953,7 @@ ${runLogTail}
4744
4953
  recoveryBranch,
4745
4954
  ...buildTokenPayload(sessionStats)
4746
4955
  });
4747
- log12.info(TAG12, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
4956
+ log13.info(TAG12, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
4748
4957
  }
4749
4958
  if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
4750
4959
  const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
@@ -4913,7 +5122,7 @@ var init_review_prompt = __esm(() => {
4913
5122
  import { createWriteStream, mkdirSync } from "node:fs";
4914
5123
  import { homedir as homedir2 } from "node:os";
4915
5124
  import { join as join2 } from "node:path";
4916
- import { log as log13 } from "@gethmy/harness";
5125
+ import { log as log14 } from "@gethmy/harness";
4917
5126
  function openRunLog(tag, runId, shortId) {
4918
5127
  if (!runId)
4919
5128
  return null;
@@ -4924,7 +5133,7 @@ function openRunLog(tag, runId, shortId) {
4924
5133
  const stream = createWriteStream(path, { flags: "a" });
4925
5134
  return { path, stream };
4926
5135
  } catch (err) {
4927
- log13.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
5136
+ log14.warn(tag, `Failed to open run log: ${err instanceof Error ? err.message : err}`);
4928
5137
  return null;
4929
5138
  }
4930
5139
  }
@@ -4959,7 +5168,7 @@ import {
4959
5168
  } from "node:fs";
4960
5169
  import { homedir as homedir3 } from "node:os";
4961
5170
  import { dirname, join as join3 } from "node:path";
4962
- import { log as log14 } from "@gethmy/harness";
5171
+ import { log as log15 } from "@gethmy/harness";
4963
5172
  function emptyState() {
4964
5173
  return {
4965
5174
  version: SCHEMA_VERSION,
@@ -5015,7 +5224,7 @@ class StateStore {
5015
5224
  const raw = readFileSync3(this.path, "utf-8");
5016
5225
  const parsed = JSON.parse(raw);
5017
5226
  if (parsed?.version !== SCHEMA_VERSION) {
5018
- log14.warn(TAG13, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
5227
+ log15.warn(TAG13, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — migrating (preserving card budget/attempts, dropping in-flight runs)`);
5019
5228
  return {
5020
5229
  version: SCHEMA_VERSION,
5021
5230
  daemonId: null,
@@ -5036,7 +5245,7 @@ class StateStore {
5036
5245
  daily: parsed.daily ?? []
5037
5246
  };
5038
5247
  } catch (err) {
5039
- log14.error(TAG13, `failed to read state file: ${err instanceof Error ? err.message : err}`);
5248
+ log15.error(TAG13, `failed to read state file: ${err instanceof Error ? err.message : err}`);
5040
5249
  return emptyState();
5041
5250
  }
5042
5251
  }
@@ -5207,6 +5416,28 @@ class StateStore {
5207
5416
  rec.loopIterations = 0;
5208
5417
  await this.persist();
5209
5418
  }
5419
+ async markFanoutSettled(cardId, stageId, childCardId) {
5420
+ const rec = this.ensureCard(cardId);
5421
+ if (rec.fanoutStageId !== stageId) {
5422
+ rec.fanoutStageId = stageId;
5423
+ rec.fanoutSettledChildIds = [];
5424
+ }
5425
+ const seen = rec.fanoutSettledChildIds ?? [];
5426
+ if (seen.includes(childCardId))
5427
+ return false;
5428
+ seen.push(childCardId);
5429
+ rec.fanoutSettledChildIds = seen;
5430
+ await this.persist();
5431
+ return true;
5432
+ }
5433
+ async resetFanoutSettled(cardId) {
5434
+ const rec = this.getCard(cardId);
5435
+ if (!rec || rec.fanoutStageId == null && rec.fanoutSettledChildIds == null)
5436
+ return;
5437
+ rec.fanoutStageId = null;
5438
+ rec.fanoutSettledChildIds = [];
5439
+ await this.persist();
5440
+ }
5210
5441
  async markAwaitingDecision(cardId, opts) {
5211
5442
  const rec = this.ensureCard(cardId);
5212
5443
  rec.awaitingDecisionUntil = opts.until;
@@ -5274,7 +5505,7 @@ var init_state_store = () => {};
5274
5505
 
5275
5506
  // src/stream-parser.ts
5276
5507
  import { EventEmitter } from "node:events";
5277
- import { log as log15 } from "@gethmy/harness";
5508
+ import { log as log16 } from "@gethmy/harness";
5278
5509
  function normalizeToolResultContent(raw) {
5279
5510
  if (raw == null)
5280
5511
  return;
@@ -5342,14 +5573,14 @@ var init_stream_parser = __esm(() => {
5342
5573
  try {
5343
5574
  msg = JSON.parse(line);
5344
5575
  } catch {
5345
- log15.debug(TAG14, `Non-JSON line: ${line.slice(0, 100)}`);
5576
+ log16.debug(TAG14, `Non-JSON line: ${line.slice(0, 100)}`);
5346
5577
  return;
5347
5578
  }
5348
5579
  try {
5349
5580
  this.handleMessage(msg);
5350
5581
  } catch (err) {
5351
5582
  const errMsg = err instanceof Error ? err.message : String(err);
5352
- log15.warn(TAG14, `Error handling stream event: ${errMsg}`);
5583
+ log16.warn(TAG14, `Error handling stream event: ${errMsg}`);
5353
5584
  this.emit("parse_error", errMsg);
5354
5585
  }
5355
5586
  }
@@ -5425,7 +5656,7 @@ var init_stream_parser = __esm(() => {
5425
5656
  });
5426
5657
 
5427
5658
  // src/transitions.ts
5428
- import { log as log16 } from "@gethmy/harness";
5659
+ import { log as log17 } from "@gethmy/harness";
5429
5660
  async function withRetry(step, cardShortId, op, attempts, backoffMs) {
5430
5661
  let lastErr;
5431
5662
  for (let i = 0;i < attempts; i++) {
@@ -5436,7 +5667,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
5436
5667
  const msg2 = err instanceof Error ? err.message : String(err);
5437
5668
  if (i < attempts - 1) {
5438
5669
  const wait = backoffMs * 2 ** i;
5439
- log16.warn(TAG15, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
5670
+ log17.warn(TAG15, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
5440
5671
  await new Promise((r) => setTimeout(r, wait));
5441
5672
  }
5442
5673
  }
@@ -5460,10 +5691,10 @@ async function runTransition(client, card, plan, opts = {}) {
5460
5691
  if (opts.strictColumn) {
5461
5692
  throw new TransitionError("move", 1, msg);
5462
5693
  }
5463
- log16.warn(TAG15, `#${shortId}: ${msg} — skipping move`);
5694
+ log17.warn(TAG15, `#${shortId}: ${msg} — skipping move`);
5464
5695
  } else if (card.column_id !== target.id) {
5465
5696
  await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
5466
- log16.info(TAG15, `#${shortId} → "${target.name}"`);
5697
+ log17.info(TAG15, `#${shortId} → "${target.name}"`);
5467
5698
  card.column_id = target.id;
5468
5699
  moveLanded = true;
5469
5700
  } else {
@@ -5482,7 +5713,7 @@ async function runTransition(client, card, plan, opts = {}) {
5482
5713
  continue;
5483
5714
  await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
5484
5715
  existing.add(labelId);
5485
- log16.info(TAG15, `#${shortId} +label "${name}"`);
5716
+ log17.info(TAG15, `#${shortId} +label "${name}"`);
5486
5717
  }
5487
5718
  card.labelIds = Array.from(existing);
5488
5719
  }
@@ -5494,23 +5725,23 @@ async function runTransition(client, card, plan, opts = {}) {
5494
5725
  continue;
5495
5726
  await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
5496
5727
  existing.delete(match.id);
5497
- log16.info(TAG15, `#${shortId} -label "${name}"`);
5728
+ log17.info(TAG15, `#${shortId} -label "${name}"`);
5498
5729
  }
5499
5730
  card.labelIds = Array.from(existing);
5500
5731
  }
5501
5732
  if (plan.updateCard) {
5502
5733
  await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
5503
- log16.info(TAG15, `#${shortId} updated`);
5734
+ log17.info(TAG15, `#${shortId} updated`);
5504
5735
  }
5505
5736
  if (plan.endSession) {
5506
5737
  const endResult = await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
5507
5738
  result.endSession = endResult;
5508
- log16.info(TAG15, `#${shortId} session ended (${plan.endSession.status})`);
5739
+ log17.info(TAG15, `#${shortId} session ended (${plan.endSession.status})`);
5509
5740
  }
5510
5741
  if (plan.assignAgent !== undefined) {
5511
5742
  const assignedAgentId = plan.assignAgent;
5512
5743
  await withRetry("assignAgent", shortId, () => client.updateCard(card.id, { assignedAgentId }), attempts, backoffMs);
5513
- log16.info(TAG15, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
5744
+ log17.info(TAG15, assignedAgentId ? `#${shortId} assigned → agent ${assignedAgentId}` : `#${shortId} unassigned`);
5514
5745
  }
5515
5746
  if (opts.store && opts.runId) {
5516
5747
  try {
@@ -5524,7 +5755,7 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
5524
5755
  const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
5525
5756
  return result?.label?.id ?? null;
5526
5757
  } catch (err) {
5527
- log16.warn(TAG15, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
5758
+ log17.warn(TAG15, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
5528
5759
  return null;
5529
5760
  }
5530
5761
  }
@@ -5552,7 +5783,7 @@ import {
5552
5783
  collectGateEvidence,
5553
5784
  DevServerReadinessError,
5554
5785
  formatDiffSummary,
5555
- log as log17,
5786
+ log as log18,
5556
5787
  probeDevServer,
5557
5788
  resolveStageGate,
5558
5789
  signalGroup,
@@ -5591,6 +5822,7 @@ class ReviewWorker {
5591
5822
  cliSessionId = null;
5592
5823
  grantedTurns = null;
5593
5824
  resumeMessage = null;
5825
+ sessionIdentifier = "";
5594
5826
  get effectiveMaxTurns() {
5595
5827
  return this.grantedTurns ?? this.config.claude.reviewMaxTurns;
5596
5828
  }
@@ -5602,6 +5834,7 @@ class ReviewWorker {
5602
5834
  this.stateStore = stateStore;
5603
5835
  this.workspaceId = workspaceId;
5604
5836
  this.id = id;
5837
+ this.sessionIdentifier = agentIdentifier(id);
5605
5838
  }
5606
5839
  startHeartbeat() {
5607
5840
  this.stopHeartbeat();
@@ -5640,7 +5873,7 @@ class ReviewWorker {
5640
5873
  cliSessionId: this.cliSessionId
5641
5874
  });
5642
5875
  } catch (err) {
5643
- log17.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
5876
+ log18.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
5644
5877
  }
5645
5878
  }
5646
5879
  get tag() {
@@ -5680,6 +5913,7 @@ class ReviewWorker {
5680
5913
  this.startedAt = Date.now();
5681
5914
  this.runId = newRunId();
5682
5915
  const resuming = this.stateStore.getResumableRunForCard(card.id);
5916
+ this.sessionIdentifier = agentIdentifier(resuming?.workerId ?? this.id);
5683
5917
  if (resuming) {
5684
5918
  this.runId = resuming.runId;
5685
5919
  this.worktreePath = resuming.worktreePath;
@@ -5694,12 +5928,12 @@ class ReviewWorker {
5694
5928
  resumeMessage: null
5695
5929
  });
5696
5930
  } catch (err) {
5697
- log17.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
5931
+ log18.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
5698
5932
  }
5699
5933
  }
5700
5934
  try {
5701
5935
  this.state = "preparing";
5702
- log17.info(this.tag, resuming ? `Resuming review of #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing review for #${card.short_id} "${card.title}"`);
5936
+ log18.info(this.tag, resuming ? `Resuming review of #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing review for #${card.short_id} "${card.title}"`);
5703
5937
  this.startHeartbeat();
5704
5938
  if (!resuming) {
5705
5939
  await this.stateStore.insertRun({
@@ -5727,16 +5961,16 @@ class ReviewWorker {
5727
5961
  const resolution = await resolveReviewBranch(card.description, repoRoot);
5728
5962
  if (resolution.kind !== "branch") {
5729
5963
  const why = resolution.kind === "skip" ? resolution.reason : "no branch or PR reference";
5730
- log17.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
5964
+ log18.info(this.tag, `#${card.short_id} not auto-reviewable (${why}) — marking for human review (staying in Review)`);
5731
5965
  await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
5732
5966
  return;
5733
5967
  }
5734
5968
  this.branchName = resolution.branch;
5735
- log17.info(this.tag, `Review branch: ${this.branchName}`);
5969
+ log18.info(this.tag, `Review branch: ${this.branchName}`);
5736
5970
  let reviewSession;
5737
5971
  try {
5738
5972
  const started = await this.client.startAgentSession(card.id, {
5739
- agentIdentifier: agentIdentifier(this.id),
5973
+ agentIdentifier: this.sessionIdentifier,
5740
5974
  agentName: `${AGENT_NAME} (Review)`,
5741
5975
  agentId: this.identity.agentId,
5742
5976
  status: "working",
@@ -5750,7 +5984,7 @@ class ReviewWorker {
5750
5984
  } catch (err) {
5751
5985
  if (isSessionConflict(err)) {
5752
5986
  this.sessionConflict = true;
5753
- log17.info(this.tag, `Skipping review of #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
5987
+ log18.info(this.tag, `Skipping review of #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
5754
5988
  return;
5755
5989
  }
5756
5990
  throw err;
@@ -5769,7 +6003,7 @@ class ReviewWorker {
5769
6003
  }
5770
6004
  const port = this.reviewPort;
5771
6005
  const cwd = this.worktreePath;
5772
- log17.info(this.tag, `Starting dev server on port ${port}...`);
6006
+ log18.info(this.tag, `Starting dev server on port ${port}...`);
5773
6007
  const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
5774
6008
  this.devServerProcess = spawnInGroup(devCmd, devArgs, {
5775
6009
  cwd,
@@ -5780,7 +6014,7 @@ class ReviewWorker {
5780
6014
  devServerSpawnError = err;
5781
6015
  });
5782
6016
  await this.client.updateAgentProgress(card.id, {
5783
- agentIdentifier: agentIdentifier(this.id),
6017
+ agentIdentifier: this.sessionIdentifier,
5784
6018
  agentName: `${AGENT_NAME} (Review)`,
5785
6019
  status: "waiting",
5786
6020
  currentTask: `Starting dev server on port ${port}…`,
@@ -5791,9 +6025,9 @@ class ReviewWorker {
5791
6025
  }
5792
6026
  await waitForDevServer(this.devServerProcess, 30000);
5793
6027
  await probeDevServer(port);
5794
- log17.info(this.tag, `Dev server ready on port ${port}`);
6028
+ log18.info(this.tag, `Dev server ready on port ${port}`);
5795
6029
  await this.client.updateAgentProgress(card.id, {
5796
- agentIdentifier: agentIdentifier(this.id),
6030
+ agentIdentifier: this.sessionIdentifier,
5797
6031
  agentName: `${AGENT_NAME} (Review)`,
5798
6032
  status: "working",
5799
6033
  currentTask: "Reviewing changes",
@@ -5824,10 +6058,10 @@ class ReviewWorker {
5824
6058
  pinnedContract = extractPinnedContract(comments, this.identity);
5825
6059
  }
5826
6060
  } catch (err) {
5827
- log17.warn(this.tag, `pinned-contract fetch failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6061
+ log18.warn(this.tag, `pinned-contract fetch failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
5828
6062
  }
5829
6063
  if (pinnedContract) {
5830
- log17.info(this.tag, `Grading pinned contract for #${card.short_id} (${pinnedContract.assertions.length} criteria)`);
6064
+ log18.info(this.tag, `Grading pinned contract for #${card.short_id} (${pinnedContract.assertions.length} criteria)`);
5831
6065
  }
5832
6066
  }
5833
6067
  const systemPrompt = buildReviewSystemPrompt();
@@ -5845,21 +6079,21 @@ ${userPrompt}`;
5845
6079
  contextIncluded: { source: "review-knowledge", mode: "review" }
5846
6080
  });
5847
6081
  } catch (err) {
5848
- log17.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
6082
+ log18.warn(this.tag, `prompt_history persistence skipped: ${err instanceof Error ? err.message : String(err)}`);
5849
6083
  }
5850
6084
  await this.client.updateAgentProgress(card.id, {
5851
- agentIdentifier: agentIdentifier(this.id),
6085
+ agentIdentifier: this.sessionIdentifier,
5852
6086
  agentName: `${AGENT_NAME} (Review)`,
5853
6087
  status: "working",
5854
6088
  currentTask: "Running Claude review",
5855
6089
  progressPercent: 20
5856
6090
  });
5857
6091
  this.timeoutTimer = setTimeout(() => {
5858
- log17.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
6092
+ log18.warn(this.tag, `Review timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
5859
6093
  this.timedOut = true;
5860
6094
  this.cancel("timeout");
5861
6095
  }, this.config.review.maxTimeout);
5862
- this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks);
6096
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks);
5863
6097
  this.progressTracker.setRequestedModel(this.config.claude.reviewModel);
5864
6098
  const stdout = await this.spawnClaude(userPrompt, systemPrompt, this.progressTracker, card.short_id, {
5865
6099
  maxTurns: this.grantedTurns ?? undefined,
@@ -5877,12 +6111,12 @@ ${userPrompt}`;
5877
6111
  }
5878
6112
  this.state = "completing";
5879
6113
  await this.recordPhase("completing");
5880
- log17.info(this.tag, `Claude review finished for #${card.short_id}`);
6114
+ log18.info(this.tag, `Claude review finished for #${card.short_id}`);
5881
6115
  this.killDevServer();
5882
6116
  const result = parseReviewOutput(stdout);
5883
- log17.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
6117
+ log18.info(this.tag, `Review verdict: ${result.verdict} (${result.findings.length} finding(s))`);
5884
6118
  await this.client.updateAgentProgress(card.id, {
5885
- agentIdentifier: agentIdentifier(this.id),
6119
+ agentIdentifier: this.sessionIdentifier,
5886
6120
  agentName: `${AGENT_NAME} (Review)`,
5887
6121
  status: "working",
5888
6122
  currentTask: `Processing ${result.verdict} verdict`,
@@ -5901,7 +6135,7 @@ ${userPrompt}`;
5901
6135
  }
5902
6136
  this.state = "error";
5903
6137
  const msg = err instanceof Error ? err.message : String(err);
5904
- log17.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
6138
+ log18.error(this.tag, `Error reviewing #${card.short_id}: ${msg}`);
5905
6139
  try {
5906
6140
  const stats = this.lastSessionStats ?? this.progressTracker?.stats;
5907
6141
  await runTransition(this.client, card, {
@@ -5911,21 +6145,21 @@ ${userPrompt}`;
5911
6145
  }
5912
6146
  });
5913
6147
  } catch (tErr) {
5914
- log17.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
6148
+ log18.error(this.tag, `endAgentSession unrecoverable on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
5915
6149
  }
5916
6150
  if (err instanceof DevServerReadinessError) {
5917
6151
  try {
5918
6152
  await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
5919
- log17.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
6153
+ log18.info(this.tag, `#${card.short_id} kept in Review — dev server unavailable, human review needed`);
5920
6154
  } catch {
5921
- log17.warn(this.tag, "Failed to add Need Review label after dev-server failure");
6155
+ log18.warn(this.tag, "Failed to add Need Review label after dev-server failure");
5922
6156
  }
5923
6157
  } else {
5924
6158
  try {
5925
6159
  await moveCardToColumn(this.client, card, this.config.review.failColumn);
5926
- log17.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
6160
+ log18.info(this.tag, `Moved #${card.short_id} to "${this.config.review.failColumn}" after error`);
5927
6161
  } catch {
5928
- log17.warn(this.tag, "Failed to move card to fail column after error");
6162
+ log18.warn(this.tag, "Failed to move card to fail column after error");
5929
6163
  }
5930
6164
  }
5931
6165
  if (this.runId) {
@@ -5965,7 +6199,7 @@ ${userPrompt}`;
5965
6199
  const holderMessage = err instanceof Error ? err.message : String(err);
5966
6200
  const waitHours = this.config.budget.pause.waitHours;
5967
6201
  const until = computeDecisionDeadline(waitHours);
5968
- log17.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
6202
+ log18.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
5969
6203
  try {
5970
6204
  await this.client.addComment(card.id, formatResumeConflictComment({
5971
6205
  holderMessage,
@@ -5977,7 +6211,7 @@ ${userPrompt}`;
5977
6211
  agentSessionId: this.sessionId ?? undefined
5978
6212
  });
5979
6213
  } catch (commentErr) {
5980
- log17.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
6214
+ log18.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
5981
6215
  }
5982
6216
  if (this.runId) {
5983
6217
  const run = this.stateStore.getRun(this.runId);
@@ -5988,7 +6222,7 @@ ${userPrompt}`;
5988
6222
  awaitingDecisionUntil: until
5989
6223
  });
5990
6224
  } catch (storeErr) {
5991
- log17.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
6225
+ log18.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
5992
6226
  }
5993
6227
  }
5994
6228
  }
@@ -6000,7 +6234,7 @@ ${userPrompt}`;
6000
6234
  this.progressTracker = null;
6001
6235
  const waitHours = this.config.budget.pause.waitHours;
6002
6236
  const until = computeDecisionDeadline(waitHours);
6003
- log17.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
6237
+ log18.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
6004
6238
  const body = formatBudgetComment({
6005
6239
  trigger,
6006
6240
  numTurns: stats?.cost?.numTurns ?? 0,
@@ -6019,18 +6253,18 @@ ${userPrompt}`;
6019
6253
  });
6020
6254
  commentId = res?.comment?.id ?? null;
6021
6255
  } catch (err) {
6022
- log17.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
6256
+ log18.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
6023
6257
  }
6024
6258
  try {
6025
6259
  await this.client.updateAgentProgress(card.id, {
6026
- agentIdentifier: agentIdentifier(this.id),
6260
+ agentIdentifier: this.sessionIdentifier,
6027
6261
  agentName: `${AGENT_NAME} (Review)`,
6028
6262
  status: "blocked",
6029
6263
  currentTask: "Waiting for your decision on the turn budget",
6030
6264
  awaitingDecisionUntil: new Date(until).toISOString()
6031
6265
  });
6032
6266
  } catch (err) {
6033
- log17.warn(this.tag, `Failed to mark the session blocked: ${err}`);
6267
+ log18.warn(this.tag, `Failed to mark the session blocked: ${err}`);
6034
6268
  }
6035
6269
  if (this.runId) {
6036
6270
  try {
@@ -6042,14 +6276,14 @@ ${userPrompt}`;
6042
6276
  numTurns: stats?.cost?.numTurns ?? 0
6043
6277
  });
6044
6278
  } catch (err) {
6045
- log17.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
6279
+ log18.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
6046
6280
  }
6047
6281
  }
6048
6282
  }
6049
6283
  async pause() {
6050
6284
  if (!this.isActive || !this.process || this.process.killed)
6051
6285
  return;
6052
- log17.info(this.tag, `Pausing review on ${this.cardId}`);
6286
+ log18.info(this.tag, `Pausing review on ${this.cardId}`);
6053
6287
  signalGroup(this.process, "SIGSTOP");
6054
6288
  if (this.timeoutTimer) {
6055
6289
  clearTimeout(this.timeoutTimer);
@@ -6058,34 +6292,34 @@ ${userPrompt}`;
6058
6292
  if (this.cardId) {
6059
6293
  try {
6060
6294
  await this.client.updateAgentProgress(this.cardId, {
6061
- agentIdentifier: agentIdentifier(this.id),
6062
- agentName: AGENT_NAME,
6295
+ agentIdentifier: this.sessionIdentifier,
6296
+ agentName: `${AGENT_NAME} (Review)`,
6063
6297
  status: "paused"
6064
6298
  });
6065
6299
  } catch {
6066
- log17.warn(this.tag, "Failed to update agent session to paused");
6300
+ log18.warn(this.tag, "Failed to update agent session to paused");
6067
6301
  }
6068
6302
  }
6069
6303
  }
6070
6304
  async resume() {
6071
6305
  if (!this.isActive || !this.process || this.process.killed)
6072
6306
  return;
6073
- log17.info(this.tag, `Resuming review on ${this.cardId}`);
6307
+ log18.info(this.tag, `Resuming review on ${this.cardId}`);
6074
6308
  signalGroup(this.process, "SIGCONT");
6075
6309
  this.timeoutTimer = setTimeout(() => {
6076
- log17.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
6310
+ log18.warn(this.tag, `Timeout reached (${this.config.review.maxTimeout}ms), cancelling`);
6077
6311
  this.timedOut = true;
6078
6312
  this.cancel("timeout");
6079
6313
  }, this.config.review.maxTimeout);
6080
6314
  if (this.cardId) {
6081
6315
  try {
6082
6316
  await this.client.updateAgentProgress(this.cardId, {
6083
- agentIdentifier: agentIdentifier(this.id),
6084
- agentName: AGENT_NAME,
6317
+ agentIdentifier: this.sessionIdentifier,
6318
+ agentName: `${AGENT_NAME} (Review)`,
6085
6319
  status: "working"
6086
6320
  });
6087
6321
  } catch {
6088
- log17.warn(this.tag, "Failed to update agent session to working");
6322
+ log18.warn(this.tag, "Failed to update agent session to working");
6089
6323
  }
6090
6324
  }
6091
6325
  }
@@ -6094,7 +6328,7 @@ ${userPrompt}`;
6094
6328
  return;
6095
6329
  this.aborted = true;
6096
6330
  this.state = "cancelling";
6097
- log17.info(this.tag, `Cancelling review on ${this.cardId}`);
6331
+ log18.info(this.tag, `Cancelling review on ${this.cardId}`);
6098
6332
  const snapshotStats = this.lastSessionStats ?? this.progressTracker?.stats;
6099
6333
  if (this.progressTracker) {
6100
6334
  this.progressTracker?.stop();
@@ -6144,11 +6378,11 @@ ${userPrompt}`;
6144
6378
  "--",
6145
6379
  prompt
6146
6380
  ];
6147
- log17.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
6381
+ log18.info(this.tag, `Spawning review: claude ${args.slice(0, 5).join(" ")} ...`);
6148
6382
  const runLog = openRunLog(this.tag, this.runId, shortId);
6149
6383
  this.lastRunLogPath = runLog?.path ?? null;
6150
6384
  if (runLog) {
6151
- log17.info(this.tag, `Run log: ${runLog.path}`);
6385
+ log18.info(this.tag, `Run log: ${runLog.path}`);
6152
6386
  runLog.stream.write(`# run=${this.runId} card=#${shortId} pipeline=review started=${new Date().toISOString()}
6153
6387
  ` + `# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>
6154
6388
 
@@ -6166,7 +6400,7 @@ ${userPrompt}`;
6166
6400
  this.captureCliSessionId(parser.sessionId);
6167
6401
  });
6168
6402
  parser.on("parse_error", (msg) => {
6169
- log17.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
6403
+ log18.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
6170
6404
  runLog?.stream.write(`
6171
6405
  [parse_error] ${msg}
6172
6406
  `);
@@ -6245,16 +6479,16 @@ ${userPrompt}`;
6245
6479
  const evidence = await collectGateEvidence(registry, context);
6246
6480
  const evaluation = gateEvaluate(resolved.gate, evidence);
6247
6481
  await this.client.request("POST", `/cards/${encodeURIComponent(card.id)}/stage-gate-evidence`, toStageGateEvidenceInsert(context, evidence));
6248
- log17.info(this.tag, `Recorded review_passed gate evidence for #${card.short_id} stage "${resolved.stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
6482
+ log18.info(this.tag, `Recorded review_passed gate evidence for #${card.short_id} stage "${resolved.stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
6249
6483
  } catch (err) {
6250
- log17.warn(this.tag, `review gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6484
+ log18.warn(this.tag, `review gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6251
6485
  }
6252
6486
  }
6253
6487
  killDevServer() {
6254
6488
  if (this.devServerProcess && !this.devServerProcess.killed) {
6255
6489
  signalGroup(this.devServerProcess, "SIGTERM");
6256
6490
  this.devServerProcess = null;
6257
- log17.debug(this.tag, "Killed dev server group");
6491
+ log18.debug(this.tag, "Killed dev server group");
6258
6492
  }
6259
6493
  }
6260
6494
  cleanup() {
@@ -6272,7 +6506,7 @@ ${userPrompt}`;
6272
6506
  try {
6273
6507
  cleanupWorktree3(this.worktreePath);
6274
6508
  } catch {
6275
- log17.warn(this.tag, "Failed to cleanup review worktree");
6509
+ log18.warn(this.tag, "Failed to cleanup review worktree");
6276
6510
  }
6277
6511
  }
6278
6512
  this.process = null;
@@ -6305,7 +6539,7 @@ var init_review_worker = __esm(() => {
6305
6539
 
6306
6540
  // src/sleep-guard.ts
6307
6541
  import { spawn } from "node:child_process";
6308
- import { log as log18 } from "@gethmy/harness";
6542
+ import { log as log19 } from "@gethmy/harness";
6309
6543
 
6310
6544
  class SleepGuard {
6311
6545
  platform;
@@ -6333,7 +6567,7 @@ class SleepGuard {
6333
6567
  if (!this.child.killed)
6334
6568
  this.child.kill("SIGTERM");
6335
6569
  this.child = null;
6336
- log18.info(TAG17, "sleep assertion released");
6570
+ log19.info(TAG17, "sleep assertion released");
6337
6571
  }
6338
6572
  }
6339
6573
  start() {
@@ -6348,7 +6582,7 @@ class SleepGuard {
6348
6582
  spawned = true;
6349
6583
  });
6350
6584
  child.on("error", (err) => {
6351
- log18.warn(TAG17, `caffeinate unavailable: ${err.message}`);
6585
+ log19.warn(TAG17, `caffeinate unavailable: ${err.message}`);
6352
6586
  if (this.child === child)
6353
6587
  this.child = null;
6354
6588
  });
@@ -6361,9 +6595,9 @@ class SleepGuard {
6361
6595
  });
6362
6596
  child.unref();
6363
6597
  this.child = child;
6364
- log18.info(TAG17, "sleep assertion acquired (caffeinate -i)");
6598
+ log19.info(TAG17, "sleep assertion acquired (caffeinate -i)");
6365
6599
  } catch (err) {
6366
- log18.warn(TAG17, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
6600
+ log19.warn(TAG17, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
6367
6601
  }
6368
6602
  }
6369
6603
  }
@@ -6371,13 +6605,13 @@ var TAG17 = "sleep-guard";
6371
6605
  var init_sleep_guard = () => {};
6372
6606
 
6373
6607
  // src/unblock.ts
6374
- import { log as log19 } from "@gethmy/harness";
6608
+ import { log as log20 } from "@gethmy/harness";
6375
6609
  async function fetchBlocksLinks(client, cardId) {
6376
6610
  try {
6377
6611
  const { links } = await client.getCardLinks(cardId);
6378
6612
  return links.filter((l) => l.link_type === "blocks");
6379
6613
  } catch (err) {
6380
- log19.warn(TAG18, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6614
+ log20.warn(TAG18, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6381
6615
  return null;
6382
6616
  }
6383
6617
  }
@@ -6409,23 +6643,23 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
6409
6643
  const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
6410
6644
  if (successors.length === 0)
6411
6645
  return;
6412
- log19.info(TAG18, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
6646
+ log20.info(TAG18, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
6413
6647
  for (const link of successors) {
6414
6648
  const successorId = link.target_card.id;
6415
6649
  try {
6416
6650
  const { card } = await deps.client.getCard(successorId);
6417
6651
  if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
6418
- log19.info(TAG18, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
6652
+ log20.info(TAG18, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
6419
6653
  await deps.client.updateCard(successorId, {
6420
6654
  assignedAgentId: deps.agentId
6421
6655
  });
6422
6656
  } else {
6423
- log19.debug(TAG18, `successor #${card.short_id} assigned to different entity — skipping`);
6657
+ log20.debug(TAG18, `successor #${card.short_id} assigned to different entity — skipping`);
6424
6658
  continue;
6425
6659
  }
6426
6660
  await deps.enqueue(successorId);
6427
6661
  } catch (err) {
6428
- log19.warn(TAG18, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
6662
+ log20.warn(TAG18, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
6429
6663
  }
6430
6664
  }
6431
6665
  }
@@ -6433,7 +6667,7 @@ var TAG18 = "unblock";
6433
6667
  var init_unblock = () => {};
6434
6668
 
6435
6669
  // src/cli-agent-runner.ts
6436
- import { log as log20 } from "@gethmy/harness";
6670
+ import { log as log21 } from "@gethmy/harness";
6437
6671
  function truncateOutput(value) {
6438
6672
  return value === undefined ? undefined : value.slice(0, MAX_OUTPUT_LEN);
6439
6673
  }
@@ -6555,6 +6789,14 @@ class CliAgentRunner {
6555
6789
  this.enqueue({ kind: "loop_exhausted", source: "system", payload });
6556
6790
  this.startTimer();
6557
6791
  }
6792
+ recordLoopItemDispatched(payload) {
6793
+ this.enqueue({ kind: "loop_item_dispatched", source: "system", payload });
6794
+ this.startTimer();
6795
+ }
6796
+ recordLoopItemSettled(payload) {
6797
+ this.enqueue({ kind: "loop_item_settled", source: "system", payload });
6798
+ this.startTimer();
6799
+ }
6558
6800
  record(body) {
6559
6801
  this.enqueue(body);
6560
6802
  this.startTimer();
@@ -6583,7 +6825,7 @@ class CliAgentRunner {
6583
6825
  events: batch
6584
6826
  });
6585
6827
  } catch (err) {
6586
- log20.warn(TAG19, `Failed to flush run events: ${err}`);
6828
+ log21.warn(TAG19, `Failed to flush run events: ${err}`);
6587
6829
  this.buffer.unshift(...batch);
6588
6830
  if (this.buffer.length > MAX_BUFFER) {
6589
6831
  this.buffer.length = MAX_BUFFER;
@@ -6623,6 +6865,268 @@ function mapCost(cost) {
6623
6865
  var TAG19 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
6624
6866
  var init_cli_agent_runner = () => {};
6625
6867
 
6868
+ // src/fanout.ts
6869
+ import { log as log22 } from "@gethmy/harness";
6870
+ async function fetchLinks(client, cardId) {
6871
+ try {
6872
+ const { links } = await client.getCardLinks(cardId);
6873
+ return links;
6874
+ } catch (err) {
6875
+ log22.warn(TAG20, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6876
+ return null;
6877
+ }
6878
+ }
6879
+ async function isFanoutChildOf(card, stage, client) {
6880
+ const links = await fetchLinks(client, card.id);
6881
+ if (links === null) {
6882
+ log22.warn(TAG20, `#${card.short_id}: link read failed — treating as a fan-out child (fail closed, no recursive dispatch)`);
6883
+ return true;
6884
+ }
6885
+ const parents = links.filter((l) => l.direction === "outgoing" && l.link_type === "is_part_of");
6886
+ for (const link of parents) {
6887
+ try {
6888
+ const { card: parent } = await client.getCard(link.target_card.id);
6889
+ if (parent.current_stage === stage.id && parent.playbook_id === card.playbook_id) {
6890
+ return true;
6891
+ }
6892
+ } catch {}
6893
+ }
6894
+ return false;
6895
+ }
6896
+ function childOutcome(child, deps) {
6897
+ if (child.done)
6898
+ return "passed";
6899
+ if (child.playbook_id && child.current_stage === null)
6900
+ return "passed";
6901
+ if (deps.isGivenUp(child.id))
6902
+ return "failed";
6903
+ return "pending";
6904
+ }
6905
+ async function readChildren(parent, deps) {
6906
+ const links = await fetchLinks(deps.client, parent.id);
6907
+ if (links === null)
6908
+ return null;
6909
+ const childLinks = links.filter((l) => l.direction === "incoming" && l.link_type === "is_part_of");
6910
+ const children = [];
6911
+ for (const link of childLinks) {
6912
+ const stub = link.target_card;
6913
+ let outcome = "pending";
6914
+ let key = null;
6915
+ try {
6916
+ const { card: child } = await deps.client.getCard(stub.id);
6917
+ outcome = childOutcome(child, deps);
6918
+ key = parseFanoutKeyMarker(child.description);
6919
+ } catch {
6920
+ outcome = "pending";
6921
+ }
6922
+ children.push({
6923
+ cardId: stub.id,
6924
+ shortId: stub.short_id,
6925
+ title: stub.title,
6926
+ key,
6927
+ outcome
6928
+ });
6929
+ }
6930
+ return children;
6931
+ }
6932
+ async function resolveItems(card, stage, loop, deps) {
6933
+ const source = getLoopItemSource(loop);
6934
+ if (!source) {
6935
+ return {
6936
+ error: `Stage "${stage.name}" is a fan-out loop but its item source is missing or malformed, so nothing can be dispatched. Set an item source on the stage.`
6937
+ };
6938
+ }
6939
+ switch (source.kind) {
6940
+ case "subtasks":
6941
+ return { items: subtaskItems(card.subtasks ?? []) };
6942
+ case "checklist":
6943
+ return {
6944
+ items: checklistItemsFromDescription(card.description, source.field)
6945
+ };
6946
+ case "list_handoff": {
6947
+ const handoff = await readStageHandoff(card, source.from_stage, deps);
6948
+ return { items: handoffListItems(handoff) };
6949
+ }
6950
+ default: {
6951
+ const unknown = source;
6952
+ return {
6953
+ error: `Stage "${stage.name}" has an item source this daemon does not understand (${JSON.stringify(unknown)}). Upgrade the daemon or change the source.`
6954
+ };
6955
+ }
6956
+ }
6957
+ }
6958
+ async function readStageHandoff(card, fromStage, deps) {
6959
+ try {
6960
+ const res = await deps.client.request("GET", `/cards/${encodeURIComponent(card.id)}/comments?order=desc&comment_type=decision`);
6961
+ const comments = res.comments ?? [];
6962
+ for (const candidate of comments) {
6963
+ const handoff = extractLatestHandoff([candidate], deps.identity);
6964
+ if (handoff && handoff.stageId === fromStage)
6965
+ return handoff;
6966
+ }
6967
+ return null;
6968
+ } catch (err) {
6969
+ log22.warn(TAG20, `handoff read failed for #${card.short_id} stage "${fromStage}": ${err instanceof Error ? err.message : err}`);
6970
+ return null;
6971
+ }
6972
+ }
6973
+ async function runFanoutTick(card, stage, loop, deps) {
6974
+ const resolved = await resolveItems(card, stage, loop, deps);
6975
+ if ("error" in resolved)
6976
+ return { kind: "held", reason: resolved.error };
6977
+ const plan = planFanoutItems(resolved.items, loop);
6978
+ if (plan.items.length === 0) {
6979
+ const why = plan.invalid > 0 ? `its item source produced ${plan.invalid} entr${plan.invalid === 1 ? "y" : "ies"} with no usable text` : "its item source is empty";
6980
+ return {
6981
+ kind: "held",
6982
+ reason: `Stage "${stage.name}" is a fan-out loop but ${why}, so there is nothing to dispatch.`
6983
+ };
6984
+ }
6985
+ const children = await readChildren(card, deps);
6986
+ if (children === null) {
6987
+ return {
6988
+ kind: "held",
6989
+ reason: `Could not read the child cards for stage "${stage.name}" — holding rather than risking a duplicate dispatch.`
6990
+ };
6991
+ }
6992
+ const byKey = new Map;
6993
+ for (const child of children) {
6994
+ if (child.key && !byKey.has(child.key))
6995
+ byKey.set(child.key, child);
6996
+ }
6997
+ const itemFor = (child) => plan.items.find((i) => fanoutItemKey(i) === child.key);
6998
+ const policy = resolveOnItemFail(loop);
6999
+ for (const child of children) {
7000
+ if (child.outcome === "pending")
7001
+ continue;
7002
+ const first = await deps.stateStore.markFanoutSettled(card.id, stage.id, child.cardId).catch(() => false);
7003
+ if (!first)
7004
+ continue;
7005
+ deps.sink?.recordLoopItemSettled?.({
7006
+ stageId: stage.id,
7007
+ index: itemFor(child)?.index ?? 0,
7008
+ total: plan.items.length,
7009
+ label: child.title,
7010
+ childCardId: child.cardId,
7011
+ childShortId: child.shortId,
7012
+ result: child.outcome === "passed" ? "passed" : "failed",
7013
+ policy
7014
+ });
7015
+ }
7016
+ const aggregate = decideFanoutAggregation({
7017
+ loop,
7018
+ outcomes: children.map((c) => c.outcome)
7019
+ });
7020
+ if (aggregate === "halt") {
7021
+ const failed = children.filter((c) => c.outcome === "failed");
7022
+ const named = failed.map((c) => `#${c.shortId}`).join(", ");
7023
+ return {
7024
+ kind: "halted",
7025
+ reason: `Fan-out stage "${stage.name}" stopped: ${failed.length} item${failed.length === 1 ? "" : "s"} failed (${named}) and this loop is set to halt on a failed item. Settled items are left exactly as they are — decide whether to retry them or advance the parent by hand.`
7026
+ };
7027
+ }
7028
+ const inFlight = children.filter((c) => c.outcome === "pending").length;
7029
+ const settled = children.length - inFlight;
7030
+ const undispatched = plan.items.filter((i) => !byKey.has(fanoutItemKey(i)));
7031
+ if (undispatched.length > 0) {
7032
+ const slots = Math.max(0, resolveLoopConcurrency(loop) - inFlight);
7033
+ if (slots > 0) {
7034
+ const created = await dispatchWave(card, stage, plan, undispatched.slice(0, slots), deps);
7035
+ return {
7036
+ kind: "dispatched",
7037
+ created,
7038
+ inFlight: inFlight + created,
7039
+ total: plan.items.length
7040
+ };
7041
+ }
7042
+ return { kind: "waiting", settled, total: plan.items.length };
7043
+ }
7044
+ if (aggregate === "pending") {
7045
+ return { kind: "waiting", settled, total: plan.items.length };
7046
+ }
7047
+ return {
7048
+ kind: "complete",
7049
+ passed: children.filter((c) => c.outcome === "passed").length,
7050
+ failed: children.filter((c) => c.outcome === "failed").length
7051
+ };
7052
+ }
7053
+ async function dispatchWave(parent, stage, plan, items, deps) {
7054
+ if (items.length === 0)
7055
+ return 0;
7056
+ let spawned = [];
7057
+ try {
7058
+ const res = await deps.client.request("POST", `/cards/${encodeURIComponent(parent.id)}/fanout-children`, {
7059
+ stageId: stage.id,
7060
+ batchTotal: plan.items.length,
7061
+ items: items.map((i) => ({
7062
+ index: i.index,
7063
+ label: i.label,
7064
+ ...i.detail ? { detail: i.detail } : {},
7065
+ ...i.sourceId ? { sourceId: i.sourceId } : {}
7066
+ }))
7067
+ });
7068
+ spawned = res.children ?? [];
7069
+ } catch (err) {
7070
+ log22.warn(TAG20, `child spawn failed for #${parent.short_id} stage "${stage.id}": ${err instanceof Error ? err.message : err}`);
7071
+ return 0;
7072
+ }
7073
+ let created = 0;
7074
+ for (const child of spawned) {
7075
+ if (!child.created)
7076
+ continue;
7077
+ const item = items.find((i) => i.index === child.index);
7078
+ if (!item)
7079
+ continue;
7080
+ await seedChildHandoff(parent, stage, item, plan.items.length, child.id, deps);
7081
+ deps.sink?.recordLoopItemDispatched?.({
7082
+ stageId: stage.id,
7083
+ stageName: stage.name,
7084
+ index: item.index,
7085
+ total: plan.items.length,
7086
+ label: item.label,
7087
+ childCardId: child.id,
7088
+ childShortId: child.shortId ?? undefined,
7089
+ ...plan.truncated > 0 ? { truncated: plan.truncated } : {},
7090
+ ...plan.invalid > 0 ? { invalid: plan.invalid } : {}
7091
+ });
7092
+ created += 1;
7093
+ }
7094
+ if (created > 0 && plan.truncated > 0) {
7095
+ await deps.client.addComment(parent.id, `Fan-out stage "${stage.name}" capped this batch at ${plan.items.length} of ${plan.total} items (max_iterations). ${plan.truncated} item${plan.truncated === 1 ? " was" : "s were"} not dispatched. Raise the loop's max iterations to cover the rest.`, { commentType: "finding" }).catch(() => {});
7096
+ }
7097
+ return created;
7098
+ }
7099
+ async function seedChildHandoff(parent, stage, item, total, childId, deps) {
7100
+ const body = buildHandoffCommentBody({
7101
+ stageId: stage.id,
7102
+ stageName: stage.name,
7103
+ artifactType: stage.artifact_type,
7104
+ produced: `Fanned out from #${parent.short_id} "${parent.title}".`,
7105
+ decisions: [
7106
+ "Do only your own item — the other items are being handled on their own cards."
7107
+ ],
7108
+ nextStageNeeds: `Complete this one item: ${item.label}`,
7109
+ fanoutItem: {
7110
+ parentCardId: parent.id,
7111
+ stageId: stage.id,
7112
+ index: item.index,
7113
+ total,
7114
+ label: item.label,
7115
+ ...item.detail ? { detail: item.detail } : {},
7116
+ ...item.sourceId ? { sourceId: item.sourceId } : {}
7117
+ }
7118
+ });
7119
+ try {
7120
+ await deps.client.addComment(childId, body, { commentType: "decision" });
7121
+ } catch (err) {
7122
+ log22.warn(TAG20, `seed handoff failed for child ${childId}: ${err instanceof Error ? err.message : err}`);
7123
+ }
7124
+ }
7125
+ var TAG20 = "fanout";
7126
+ var init_fanout = __esm(() => {
7127
+ init_dist();
7128
+ });
7129
+
6626
7130
  // src/motor-driver.ts
6627
7131
  import { mkdtempSync, writeFileSync as writeFileSync2 } from "node:fs";
6628
7132
  import { createRequire as createRequire2 } from "node:module";
@@ -6804,7 +7308,7 @@ var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
6804
7308
  var init_motor_driver = () => {};
6805
7309
 
6806
7310
  // src/stage-advance.ts
6807
- import { gateConfigErrorReason, log as log21 } from "@gethmy/harness";
7311
+ import { gateConfigErrorReason, log as log23 } from "@gethmy/harness";
6808
7312
  function handoffText(stage) {
6809
7313
  if (stage.handoff && typeof stage.handoff === "object") {
6810
7314
  const summary = stage.handoff.summary ?? stage.handoff.description;
@@ -6842,7 +7346,7 @@ async function resolveStageColumnName(client, card, stage) {
6842
7346
  const match = board.columns.find((c) => c.id === target || c.name.toLowerCase() === target.toLowerCase());
6843
7347
  return match ? match.name : null;
6844
7348
  } catch (err) {
6845
- log21.warn(TAG20, `board fetch failed resolving stage column for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7349
+ log23.warn(TAG21, `board fetch failed resolving stage column for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6846
7350
  return null;
6847
7351
  }
6848
7352
  }
@@ -6881,7 +7385,7 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
6881
7385
  });
6882
7386
  } catch {}
6883
7387
  await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
6884
- log21.info(TAG20, `#${card.short_id} GateMisconfigured: ${reason}`);
7388
+ log23.info(TAG21, `#${card.short_id} GateMisconfigured: ${reason}`);
6885
7389
  return { kind: "held_misconfigured", reason };
6886
7390
  }
6887
7391
  function firstErrorMessage(evaluation) {
@@ -6912,7 +7416,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
6912
7416
  evidence,
6913
7417
  summary
6914
7418
  });
6915
- log21.info(TAG20, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
7419
+ log23.info(TAG21, `#${card.short_id} converge loop "${stage.name}": ${summary} → ${decision}`);
6916
7420
  if (decision === "exit") {
6917
7421
  await deps.stateStore.resetLoopIterations(card.id).catch(() => {});
6918
7422
  deps.sink?.recordLoopCompleted?.({
@@ -6956,7 +7460,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
6956
7460
  endStatus: "blocked",
6957
7461
  blockers: [reason]
6958
7462
  });
6959
- log21.info(TAG20, `#${card.short_id} LoopExhausted: ${reason}`);
7463
+ log23.info(TAG21, `#${card.short_id} LoopExhausted: ${reason}`);
6960
7464
  return { kind: "held_gate_unmet", reason };
6961
7465
  }
6962
7466
  await deps.stateStore.decrementAttempt(card.id).catch(() => {});
@@ -6970,7 +7474,7 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
6970
7474
  addLabels: [{ name: AGENT_LABEL }],
6971
7475
  ...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
6972
7476
  }, { store: deps.stateStore, runId: deps.runId });
6973
- log21.info(TAG20, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
7477
+ log23.info(TAG21, `#${card.short_id} converge loop "${stage.name}" — requeued to "${toColumn}" for iteration ${iteration + 1}/${maxIterations}`);
6974
7478
  return { kind: "requeued_gate_unmet", toColumn };
6975
7479
  }
6976
7480
  async function writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps) {
@@ -6989,7 +7493,7 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
6989
7493
  });
6990
7494
  await deps.client.addComment(card.id, body, { commentType: "decision" });
6991
7495
  } catch (err) {
6992
- log21.warn(TAG20, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7496
+ log23.warn(TAG21, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6993
7497
  }
6994
7498
  }
6995
7499
  async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps) {
@@ -7017,7 +7521,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
7017
7521
  reason: "Playbook complete — final stage gate passed."
7018
7522
  });
7019
7523
  deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
7020
- log21.info(TAG20, `#${card.short_id} terminal stage "${stage.name}" passed — playbook complete (done left to the column)`);
7524
+ log23.info(TAG21, `#${card.short_id} terminal stage "${stage.name}" passed — playbook complete (done left to the column)`);
7021
7525
  return { kind: "completed_terminal" };
7022
7526
  }
7023
7527
  if (next.kind === "out_of_range") {
@@ -7049,7 +7553,7 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
7049
7553
  ...isAgentRunnableOwner(next.stage.owner) ? { assignAgent: deps.agentId } : {}
7050
7554
  }, { store: deps.stateStore, runId: deps.runId });
7051
7555
  deps.stateStore.recordOutcome(card.id, "success").catch(() => {});
7052
- log21.info(TAG20, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
7556
+ log23.info(TAG21, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
7053
7557
  if (next.stage.owner === "human") {
7054
7558
  const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
7055
7559
  await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
@@ -7078,7 +7582,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
7078
7582
  endStatus: "blocked",
7079
7583
  blockers: [reason]
7080
7584
  });
7081
- log21.info(TAG20, `#${card.short_id} GateUnmetExhausted: ${reason}`);
7585
+ log23.info(TAG21, `#${card.short_id} GateUnmetExhausted: ${reason}`);
7082
7586
  return { kind: "held_gate_unmet", reason };
7083
7587
  }
7084
7588
  const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
@@ -7090,7 +7594,7 @@ async function handleGateUnmet(card, stage, summary, deps) {
7090
7594
  addLabels: [{ name: AGENT_LABEL }],
7091
7595
  ...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
7092
7596
  }, { store: deps.stateStore, runId: deps.runId });
7093
- log21.info(TAG20, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
7597
+ log23.info(TAG21, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
7094
7598
  return { kind: "requeued_gate_unmet", toColumn };
7095
7599
  }
7096
7600
  async function holdForHuman(client, card, reason, runId, stateStore, opts = {}) {
@@ -7111,13 +7615,13 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
7111
7615
  }
7112
7616
  }, { store: stateStore, runId });
7113
7617
  if (opts.endStatus === "blocked" && result.endSession?.ended === false) {
7114
- log21.warn(TAG20, `#${card.short_id} hold intended to end the session BLOCKED, but it was already ended (${result.endSession.reason ?? "unknown reason"}) — no agent_blocked push fired from this write.`);
7618
+ log23.warn(TAG21, `#${card.short_id} hold intended to end the session BLOCKED, but it was already ended (${result.endSession.reason ?? "unknown reason"}) — no agent_blocked push fired from this write.`);
7115
7619
  }
7116
7620
  } catch (err) {
7117
- log21.warn(TAG20, `hold transition failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7621
+ log23.warn(TAG21, `hold transition failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7118
7622
  }
7119
7623
  }
7120
- var TAG20 = "stage-advance", AGENT_LABEL = "agent";
7624
+ var TAG21 = "stage-advance", AGENT_LABEL = "agent";
7121
7625
  var init_stage_advance = __esm(() => {
7122
7626
  init_dist();
7123
7627
  init_transitions();
@@ -7134,7 +7638,7 @@ import {
7134
7638
  collectGateEvidence as collectGateEvidence2,
7135
7639
  createWorktree,
7136
7640
  describeApiError,
7137
- log as log22,
7641
+ log as log24,
7138
7642
  makeBranchName,
7139
7643
  normalizeGateSpec,
7140
7644
  pushBranch as pushBranch3,
@@ -7252,6 +7756,7 @@ class Worker {
7252
7756
  lastDrainedSeq = 0;
7253
7757
  grantedTurns = null;
7254
7758
  resumeMessage = null;
7759
+ sessionIdentifier = "";
7255
7760
  get effectiveMaxTurns() {
7256
7761
  return this.grantedTurns ?? this.config.claude.maxTurns;
7257
7762
  }
@@ -7269,6 +7774,7 @@ class Worker {
7269
7774
  this.onCardCompleted = onCardCompleted;
7270
7775
  this.onApiError = onApiError;
7271
7776
  this.id = id;
7777
+ this.sessionIdentifier = agentIdentifier(id);
7272
7778
  }
7273
7779
  startHeartbeat() {
7274
7780
  this.stopHeartbeat();
@@ -7306,11 +7812,11 @@ class Worker {
7306
7812
  sessionId: this.sessionId
7307
7813
  });
7308
7814
  } catch (err) {
7309
- log22.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
7815
+ log24.warn(this.tag, `state store updateRun failed: ${err instanceof Error ? err.message : err}`);
7310
7816
  }
7311
7817
  }
7312
7818
  get tag() {
7313
- return `${TAG21}:${this.id}`;
7819
+ return `${TAG22}:${this.id}`;
7314
7820
  }
7315
7821
  get isIdle() {
7316
7822
  return this.state === "idle";
@@ -7349,6 +7855,7 @@ class Worker {
7349
7855
  this.startedAt = Date.now();
7350
7856
  this.runId = newRunId();
7351
7857
  const resuming = this.stateStore.getResumableRunForCard(card.id);
7858
+ this.sessionIdentifier = agentIdentifier(resuming?.workerId ?? this.id);
7352
7859
  if (resuming) {
7353
7860
  this.runId = resuming.runId;
7354
7861
  this.worktreePath = resuming.worktreePath;
@@ -7363,7 +7870,7 @@ class Worker {
7363
7870
  resumeMessage: null
7364
7871
  });
7365
7872
  } catch (err) {
7366
- log22.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
7873
+ log24.warn(this.tag, `Failed to clear the consumed grant: ${err}`);
7367
7874
  }
7368
7875
  }
7369
7876
  try {
@@ -7371,15 +7878,15 @@ class Worker {
7371
7878
  if (!resuming) {
7372
7879
  this.branchName = makeBranchName(card.short_id, card.title, this.config.worktree.failedBranchPrefix);
7373
7880
  }
7374
- log22.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
7881
+ log24.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
7375
7882
  const attemptCount = await this.stateStore.incrementAttempt(card.id);
7376
7883
  const isRework = attemptCount > 1;
7377
7884
  const recordedBranch = extractBranchRef(card.description);
7378
7885
  const continuesPushedWork = isRework || recordsPushedWorkOn(card.description, this.branchName);
7379
7886
  if (continuesPushedWork && !isRework) {
7380
- log22.info(this.tag, `Card records completed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
7887
+ log24.info(this.tag, `Card records completed work on ${this.branchName} — continuing that branch instead of rebuilding from ${this.config.worktree.baseBranch}`);
7381
7888
  } else if (recordedBranch && recordedBranch !== this.branchName) {
7382
- log22.warn(this.tag, `Card records branch ${recordedBranch} but this run targets ${this.branchName} — starting fresh; the recorded branch is left untouched`);
7889
+ log24.warn(this.tag, `Card records branch ${recordedBranch} but this run targets ${this.branchName} — starting fresh; the recorded branch is left untouched`);
7383
7890
  }
7384
7891
  this.startHeartbeat();
7385
7892
  this.sizing = await this.sizeThisRun(card);
@@ -7407,7 +7914,7 @@ class Worker {
7407
7914
  let session;
7408
7915
  try {
7409
7916
  const started = await this.client.startAgentSession(card.id, {
7410
- agentIdentifier: agentIdentifier(this.id),
7917
+ agentIdentifier: this.sessionIdentifier,
7411
7918
  agentName: AGENT_NAME,
7412
7919
  agentId: this.identity.agentId,
7413
7920
  status: "working",
@@ -7421,7 +7928,7 @@ class Worker {
7421
7928
  } catch (err) {
7422
7929
  if (isSessionConflict(err)) {
7423
7930
  this.sessionConflict = true;
7424
- log22.info(this.tag, `Skipping #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
7931
+ log24.info(this.tag, `Skipping #${card.short_id}: ${err instanceof Error ? err.message : String(err)}`);
7425
7932
  await this.stateStore.decrementAttempt(card.id);
7426
7933
  return;
7427
7934
  }
@@ -7429,7 +7936,7 @@ class Worker {
7429
7936
  }
7430
7937
  const sid = session && typeof session === "object" && "id" in session ? session.id : null;
7431
7938
  if (!sid) {
7432
- log22.warn(TAG21, "startAgentSession returned no session id");
7939
+ log24.warn(TAG22, "startAgentSession returned no session id");
7433
7940
  }
7434
7941
  this.sessionId = sid;
7435
7942
  }
@@ -7447,7 +7954,7 @@ class Worker {
7447
7954
  if (!resuming) {
7448
7955
  const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
7449
7956
  if (!moved) {
7450
- log22.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
7957
+ log24.warn(this.tag, `Card #${card.short_id} was NOT moved to "In Progress" — check API logs`);
7451
7958
  }
7452
7959
  }
7453
7960
  if (this.aborted)
@@ -7463,6 +7970,10 @@ class Worker {
7463
7970
  await this.holdStageCard(card, stageCtx.reason, stageCtx.wait);
7464
7971
  return;
7465
7972
  }
7973
+ if (stageCtx.kind === "fanout") {
7974
+ await this.runFanoutStageCtx(card, stageCtx);
7975
+ return;
7976
+ }
7466
7977
  if (!resuming) {
7467
7978
  this.worktreePath = createWorktree(this.config.worktree.basePath, this.config.worktree.baseBranch, this.branchName, {
7468
7979
  continueExisting: stageCtx.kind === "run" || stageCtx.kind === "motor" || continuesPushedWork
@@ -7494,7 +8005,7 @@ class Worker {
7494
8005
  if (this.aborted)
7495
8006
  return;
7496
8007
  if (parked) {
7497
- log22.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
8008
+ log24.info(this.tag, `#${card.short_id} parked for plan approval — ending run`);
7498
8009
  return;
7499
8010
  }
7500
8011
  }
@@ -7505,7 +8016,7 @@ class Worker {
7505
8016
  if (stageCtx.kind === "run") {
7506
8017
  const loop = getStageLoop(stageCtx.stage);
7507
8018
  const isLoop = isConvergeLoop(loop);
7508
- const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop });
8019
+ const inherited = await this.loadInheritedHandoffSection(card.id, stageCtx.stage.id, { includeOwnStage: isLoop || stageCtx.isFanoutChild === true });
7509
8020
  prompt = [buildStagePreamble(stageCtx.stage), inherited, basePrompt].filter(Boolean).join(`
7510
8021
 
7511
8022
  `);
@@ -7539,14 +8050,14 @@ ${basePrompt}`;
7539
8050
  ${prompt}`;
7540
8051
  }
7541
8052
  await this.client.updateAgentProgress(card.id, {
7542
- agentIdentifier: agentIdentifier(this.id),
8053
+ agentIdentifier: this.sessionIdentifier,
7543
8054
  agentName: AGENT_NAME,
7544
8055
  status: "working",
7545
8056
  currentTask: resuming ? "Resuming Claude CLI" : stageCtx.kind === "run" ? `Running stage "${stageCtx.stage.name}"` : "Running Claude CLI",
7546
8057
  progressPercent: 10
7547
8058
  });
7548
8059
  this.timeoutTimer = setTimeout(() => {
7549
- log22.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
8060
+ log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
7550
8061
  this.timedOut = true;
7551
8062
  this.cancel("timeout");
7552
8063
  }, this.config.maxTimeout);
@@ -7571,9 +8082,9 @@ ${prompt}`;
7571
8082
  }
7572
8083
  this.state = "verifying";
7573
8084
  await this.recordPhase("verifying");
7574
- log22.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
8085
+ log24.info(this.tag, `Claude finished for #${card.short_id}, running verification & completion`);
7575
8086
  await this.client.updateAgentProgress(card.id, {
7576
- agentIdentifier: agentIdentifier(this.id),
8087
+ agentIdentifier: this.sessionIdentifier,
7577
8088
  agentName: AGENT_NAME,
7578
8089
  status: "working",
7579
8090
  currentTask: "Verifying implementation",
@@ -7588,7 +8099,7 @@ ${prompt}`;
7588
8099
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
7589
8100
  return stageEndDisposition(stageGateEvaluation, stageRun.stage, stageRun.index, stageRun.def);
7590
8101
  } : undefined;
7591
- const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns);
8102
+ const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns);
7592
8103
  if (completed === "park") {
7593
8104
  await this.parkForDecision(card, "max_turns");
7594
8105
  return;
@@ -7623,7 +8134,7 @@ ${prompt}`;
7623
8134
  }
7624
8135
  this.state = "error";
7625
8136
  const msg = err instanceof Error ? err.message : String(err);
7626
- log22.error(this.tag, `Error on #${card.short_id}: ${msg}`);
8137
+ log24.error(this.tag, `Error on #${card.short_id}: ${msg}`);
7627
8138
  const rawStderr = err?.stderr;
7628
8139
  const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
7629
8140
  const sdkKind = err?.errorKind;
@@ -7646,7 +8157,7 @@ ${prompt}`;
7646
8157
  try {
7647
8158
  await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
7648
8159
  } catch {
7649
- log22.warn(this.tag, "Failed to cleanup worktree before requeue");
8160
+ log24.warn(this.tag, "Failed to cleanup worktree before requeue");
7650
8161
  }
7651
8162
  this.worktreePath = null;
7652
8163
  }
@@ -7663,7 +8174,7 @@ ${prompt}`;
7663
8174
  }
7664
8175
  });
7665
8176
  } catch (tErr) {
7666
- log22.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
8177
+ log24.error(this.tag, `error transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7667
8178
  }
7668
8179
  if (this.runId) {
7669
8180
  try {
@@ -7699,7 +8210,7 @@ ${prompt}`;
7699
8210
  try {
7700
8211
  await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
7701
8212
  } catch {
7702
- log22.warn(this.tag, "Failed to cleanup worktree before requeue");
8213
+ log24.warn(this.tag, "Failed to cleanup worktree before requeue");
7703
8214
  }
7704
8215
  this.worktreePath = null;
7705
8216
  }
@@ -7714,7 +8225,7 @@ ${prompt}`;
7714
8225
  }
7715
8226
  });
7716
8227
  } catch (tErr) {
7717
- log22.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
8228
+ log24.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7718
8229
  }
7719
8230
  try {
7720
8231
  await this.stateStore.endRun(this.runId, "failed", {
@@ -7728,15 +8239,15 @@ ${prompt}`;
7728
8239
  try {
7729
8240
  await this.client.updateCard(card.id, { assignedAgentId: null });
7730
8241
  } catch (err) {
7731
- log22.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
8242
+ log24.warn(this.tag, `failed to release card after stop: ${err instanceof Error ? err.message : err}`);
7732
8243
  }
7733
8244
  try {
7734
8245
  await runTransition(this.client, card, { removeLabels: ["agent"] });
7735
8246
  } catch (tErr) {
7736
- log22.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
8247
+ log24.warn(this.tag, `stop label cleanup failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7737
8248
  }
7738
8249
  } else {
7739
- log22.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
8250
+ log24.info(this.tag, `cancel arrived after completion on #${card.short_id} — keeping assignment so review picks it up (#585)`);
7740
8251
  }
7741
8252
  try {
7742
8253
  await this.stateStore.endRun(this.runId, "paused", {
@@ -7808,23 +8319,23 @@ ${prompt}`;
7808
8319
  };
7809
8320
  const { pick, reason } = selectAutoPlaybook(subject, playbooks ?? []);
7810
8321
  if (!pick) {
7811
- log22.info(this.tag, `No playbook auto-bound to #${card.short_id}: ${reason}`);
8322
+ log24.info(this.tag, `No playbook auto-bound to #${card.short_id}: ${reason}`);
7812
8323
  return card;
7813
8324
  }
7814
8325
  const applyResult = await this.client.request("POST", `/cards/${card.id}/apply-playbook`, {
7815
8326
  playbookId: pick.id
7816
8327
  });
7817
- log22.info(this.tag, `Auto-bound #${card.short_id} to playbook "${pick.name}": ${reason}`);
8328
+ log24.info(this.tag, `Auto-bound #${card.short_id} to playbook "${pick.name}": ${reason}`);
7818
8329
  try {
7819
8330
  await this.client.addComment(card.id, `Bound playbook "${pick.name}" automatically — ${reason} Apply a different playbook from the card's stage rail to override, or turn the rule off in the playbook editor.`);
7820
8331
  } catch (commentErr) {
7821
- log22.warn(this.tag, `Auto-bind comment failed for #${card.short_id}: ${commentErr instanceof Error ? commentErr.message : String(commentErr)}`);
8332
+ log24.warn(this.tag, `Auto-bind comment failed for #${card.short_id}: ${commentErr instanceof Error ? commentErr.message : String(commentErr)}`);
7822
8333
  }
7823
8334
  try {
7824
8335
  const { card: fresh } = await this.client.getCard(card.id);
7825
8336
  return fresh;
7826
8337
  } catch (fetchErr) {
7827
- log22.warn(this.tag, `Auto-bind re-fetch failed for #${card.short_id} after binding playbook "${pick.name}" — using the apply-playbook response's fields for this pickup: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
8338
+ log24.warn(this.tag, `Auto-bind re-fetch failed for #${card.short_id} after binding playbook "${pick.name}" — using the apply-playbook response's fields for this pickup: ${fetchErr instanceof Error ? fetchErr.message : String(fetchErr)}`);
7828
8339
  return {
7829
8340
  ...card,
7830
8341
  playbook_id: applyResult.card.playbook_id,
@@ -7833,7 +8344,7 @@ ${prompt}`;
7833
8344
  };
7834
8345
  }
7835
8346
  } catch (err) {
7836
- log22.warn(this.tag, `Auto-bind playbook check failed for #${card.short_id}, continuing unbound: ${err instanceof Error ? err.message : String(err)}`);
8347
+ log24.warn(this.tag, `Auto-bind playbook check failed for #${card.short_id}, continuing unbound: ${err instanceof Error ? err.message : String(err)}`);
7837
8348
  return card;
7838
8349
  }
7839
8350
  }
@@ -7889,6 +8400,20 @@ ${prompt}`;
7889
8400
  }
7890
8401
  return { kind: "motor", stage, index: resolution.index, def, role };
7891
8402
  }
8403
+ const stageLoop = getStageLoop(stage);
8404
+ let isFanoutChild = false;
8405
+ if (isFanoutLoop(stageLoop) && stageLoop) {
8406
+ isFanoutChild = await isFanoutChildOf(card, stage, this.client);
8407
+ if (!isFanoutChild) {
8408
+ return {
8409
+ kind: "fanout",
8410
+ stage,
8411
+ index: resolution.index,
8412
+ def,
8413
+ loop: stageLoop
8414
+ };
8415
+ }
8416
+ }
7892
8417
  const allowedTools = entryActionAllowlist(stage.entry_action);
7893
8418
  if (!allowedTools) {
7894
8419
  return {
@@ -7905,11 +8430,89 @@ ${prompt}`;
7905
8430
  allowedTools,
7906
8431
  index: resolution.index,
7907
8432
  priorStage,
7908
- def
8433
+ def,
8434
+ ...isFanoutChild ? { isFanoutChild: true } : {}
7909
8435
  };
7910
8436
  }
8437
+ async runFanoutStageCtx(card, ctx) {
8438
+ this.held = true;
8439
+ this.cliRunner?.recordStageEntered({
8440
+ stageId: ctx.stage.id,
8441
+ stageName: ctx.stage.name,
8442
+ owner: ctx.stage.owner
8443
+ });
8444
+ let outcome;
8445
+ try {
8446
+ outcome = await runFanoutTick(card, ctx.stage, ctx.loop, {
8447
+ client: this.client,
8448
+ stateStore: this.stateStore,
8449
+ identity: {
8450
+ userId: this.identity.userId,
8451
+ agentId: this.identity.agentId
8452
+ },
8453
+ sink: this.cliRunner,
8454
+ isGivenUp: (cardId) => (this.stateStore.getCard(cardId)?.attempts ?? 0) >= this.config.budget.maxAttemptsPerCard
8455
+ });
8456
+ } catch (err) {
8457
+ const detail = err instanceof Error ? err.message : String(err);
8458
+ log24.warn(this.tag, `fan-out tick failed on #${card.short_id}: ${detail}`);
8459
+ await this.holdStageCard(card, `Fan-out stage "${ctx.stage.name}" could not run: ${detail}`);
8460
+ return;
8461
+ }
8462
+ switch (outcome.kind) {
8463
+ case "dispatched":
8464
+ case "waiting": {
8465
+ const total = outcome.kind === "dispatched" ? outcome.total : outcome.total;
8466
+ const note = outcome.kind === "dispatched" ? `Fan-out stage "${ctx.stage.name}": dispatched ${outcome.created} of ${total} item(s); ${outcome.inFlight} in flight.` : `Fan-out stage "${ctx.stage.name}": ${outcome.settled} of ${total} item(s) settled; waiting on the rest.`;
8467
+ log24.info(this.tag, `#${card.short_id} ${note}`);
8468
+ await this.client.updateAgentProgress(card.id, {
8469
+ agentIdentifier: "claude-code-stage",
8470
+ agentName: "Harmony Agent",
8471
+ status: "waiting",
8472
+ currentTask: note
8473
+ }).catch(() => {});
8474
+ await this.holdStageCard(card, note);
8475
+ return;
8476
+ }
8477
+ case "complete": {
8478
+ await this.stateStore.resetFanoutSettled(card.id).catch(() => {});
8479
+ const summary = `Fan-out stage "${ctx.stage.name}" complete: ${outcome.passed} item(s) passed${outcome.failed > 0 ? `, ${outcome.failed} failed (continuing per on_item_fail)` : ""}.`;
8480
+ this.cliRunner?.recordLoopCompleted({
8481
+ stageId: ctx.stage.id,
8482
+ iterations: outcome.passed + outcome.failed,
8483
+ maxIterations: Math.max(1, Math.floor(ctx.loop.max_iterations) || 1),
8484
+ reason: summary
8485
+ });
8486
+ const exitEval = {
8487
+ passed: true,
8488
+ findings: [{ level: "info", message: summary }],
8489
+ structured: {}
8490
+ };
8491
+ const advance = await this.advanceFromGateEvaluation(card, ctx.stage, ctx.index, ctx.def, exitEval);
8492
+ if (advance.kind === "advanced" || advance.kind === "completed_terminal") {
8493
+ this.held = false;
8494
+ }
8495
+ log24.info(this.tag, `#${card.short_id} ${summary} → ${advance.kind}`);
8496
+ return;
8497
+ }
8498
+ case "halted": {
8499
+ await this.client.updateAgentProgress(card.id, {
8500
+ agentIdentifier: "claude-code-stage",
8501
+ agentName: "Harmony Agent",
8502
+ status: "waiting",
8503
+ currentTask: outcome.reason
8504
+ }).catch(() => {});
8505
+ await this.holdStageCard(card, outcome.reason, true);
8506
+ return;
8507
+ }
8508
+ case "held": {
8509
+ await this.holdStageCard(card, outcome.reason, true);
8510
+ return;
8511
+ }
8512
+ }
8513
+ }
7911
8514
  async holdStageCard(card, reason, wait = false) {
7912
- log22.info(this.tag, `Holding #${card.short_id}: ${reason}`);
8515
+ log24.info(this.tag, `Holding #${card.short_id}: ${reason}`);
7913
8516
  await this.stateStore.decrementAttempt(card.id);
7914
8517
  try {
7915
8518
  await this.client.addComment(card.id, reason, { commentType: "blocker" });
@@ -7925,7 +8528,7 @@ ${prompt}`;
7925
8528
  }
7926
8529
  });
7927
8530
  } catch (tErr) {
7928
- log22.warn(this.tag, `hold transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
8531
+ log24.warn(this.tag, `hold transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
7929
8532
  }
7930
8533
  if (this.runId) {
7931
8534
  try {
@@ -7943,7 +8546,7 @@ ${prompt}`;
7943
8546
  const holderMessage = err instanceof Error ? err.message : String(err);
7944
8547
  const waitHours = this.config.budget.pause.waitHours;
7945
8548
  const until = computeDecisionDeadline(waitHours);
7946
- log22.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
8549
+ log24.warn(this.tag, `#${card.short_id} stays parked — the resume could not reclaim its session: ${holderMessage}`);
7947
8550
  try {
7948
8551
  await this.client.addComment(card.id, formatResumeConflictComment({
7949
8552
  holderMessage,
@@ -7955,7 +8558,7 @@ ${prompt}`;
7955
8558
  agentSessionId: this.sessionId ?? undefined
7956
8559
  });
7957
8560
  } catch (commentErr) {
7958
- log22.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
8561
+ log24.warn(this.tag, `Failed to post the resume-conflict note for #${card.short_id}: ${commentErr}`);
7959
8562
  }
7960
8563
  if (this.runId) {
7961
8564
  const run = this.stateStore.getRun(this.runId);
@@ -7966,7 +8569,7 @@ ${prompt}`;
7966
8569
  awaitingDecisionUntil: until
7967
8570
  });
7968
8571
  } catch (storeErr) {
7969
- log22.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
8572
+ log24.error(this.tag, `#${card.short_id} could not be re-parked after a resume conflict — the daemon has no hold on it: ${storeErr}`);
7970
8573
  }
7971
8574
  }
7972
8575
  }
@@ -7977,7 +8580,7 @@ ${prompt}`;
7977
8580
  this.progressTracker = null;
7978
8581
  const waitHours = this.config.budget.pause.waitHours;
7979
8582
  const until = computeDecisionDeadline(waitHours);
7980
- log22.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
8583
+ log24.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
7981
8584
  const body = formatBudgetComment({
7982
8585
  trigger,
7983
8586
  numTurns: stats?.cost?.numTurns ?? 0,
@@ -7996,18 +8599,18 @@ ${prompt}`;
7996
8599
  });
7997
8600
  commentId = res?.comment?.id ?? null;
7998
8601
  } catch (err) {
7999
- log22.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
8602
+ log24.warn(this.tag, `Failed to post the budget-pause comment: ${err}`);
8000
8603
  }
8001
8604
  try {
8002
8605
  await this.client.updateAgentProgress(card.id, {
8003
- agentIdentifier: agentIdentifier(this.id),
8606
+ agentIdentifier: this.sessionIdentifier,
8004
8607
  agentName: AGENT_NAME,
8005
8608
  status: "blocked",
8006
8609
  currentTask: "Waiting for your decision on the turn budget",
8007
8610
  awaitingDecisionUntil: new Date(until).toISOString()
8008
8611
  });
8009
8612
  } catch (err) {
8010
- log22.warn(this.tag, `Failed to mark the session blocked: ${err}`);
8613
+ log24.warn(this.tag, `Failed to mark the session blocked: ${err}`);
8011
8614
  }
8012
8615
  if (this.runId) {
8013
8616
  try {
@@ -8019,7 +8622,7 @@ ${prompt}`;
8019
8622
  numTurns: stats?.cost?.numTurns ?? 0
8020
8623
  });
8021
8624
  } catch (err) {
8022
- log22.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
8625
+ log24.error(this.tag, `#${card.short_id} parkRun failed after the blocker comment + blocked session were already written — the daemon has no local record of this park: ${err}`);
8023
8626
  }
8024
8627
  }
8025
8628
  }
@@ -8042,7 +8645,7 @@ ${prompt}`;
8042
8645
  });
8043
8646
  const motorTask = `Running stage "${ctx.stage.name}" under the harness motor`;
8044
8647
  await this.client.updateAgentProgress(card.id, {
8045
- agentIdentifier: agentIdentifier(this.id),
8648
+ agentIdentifier: this.sessionIdentifier,
8046
8649
  agentName: AGENT_NAME,
8047
8650
  status: "working",
8048
8651
  currentTask: motorTask,
@@ -8050,27 +8653,27 @@ ${prompt}`;
8050
8653
  });
8051
8654
  const gate = normalizeGateSpec(ctx.stage.gate);
8052
8655
  const metricsPath = gate?.kind === "custom" ? writeMetricsFile(this.config.playbooks.metrics ?? {}) : null;
8053
- log22.info(this.tag, `Running stage "${ctx.stage.name}" (role ${ctx.role}) for #${card.short_id} under the harness motor`);
8656
+ log24.info(this.tag, `Running stage "${ctx.stage.name}" (role ${ctx.role}) for #${card.short_id} under the harness motor`);
8054
8657
  const motorAbort = new AbortController;
8055
8658
  this.motorAbort = motorAbort;
8056
8659
  this.timeoutTimer = setTimeout(() => {
8057
- log22.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms) during the motor stage run, cancelling`);
8660
+ log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms) during the motor stage run, cancelling`);
8058
8661
  this.timedOut = true;
8059
8662
  this.cancel("timeout");
8060
8663
  }, this.config.maxTimeout);
8061
- const motorTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, "exploring");
8664
+ const motorTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, "exploring");
8062
8665
  if (this.cliRunner)
8063
8666
  motorTracker.setRunEventSink(this.cliRunner);
8064
8667
  let motorTrackerLive = false;
8065
8668
  let motorRunSettled = false;
8066
8669
  const onMotorLine = (line) => {
8067
8670
  if (line.type !== "agent_event") {
8068
- log22.info(this.tag, `motor: ${line.type}`);
8671
+ log24.info(this.tag, `motor: ${line.type}`);
8069
8672
  return;
8070
8673
  }
8071
8674
  if (motorRunSettled)
8072
8675
  return;
8073
- log22.debug(this.tag, `motor: agent_event ${line.event.kind}`);
8676
+ log24.debug(this.tag, `motor: agent_event ${line.event.kind}`);
8074
8677
  if (line.event.kind === "tool_started" && STAGE_DAEMON_OWNED_TOOLS.includes(line.event.payload.toolName)) {
8075
8678
  return;
8076
8679
  }
@@ -8084,13 +8687,13 @@ ${prompt}`;
8084
8687
  if (motorTrackerLive && !motorTracker.isStopped)
8085
8688
  return;
8086
8689
  this.client.updateAgentProgress(card.id, {
8087
- agentIdentifier: agentIdentifier(this.id),
8690
+ agentIdentifier: this.sessionIdentifier,
8088
8691
  agentName: AGENT_NAME,
8089
8692
  status: "working",
8090
8693
  currentTask: motorTask,
8091
8694
  progressPercent: MOTOR_RUN_PROGRESS_PERCENT
8092
8695
  }).catch((err) => {
8093
- log22.warn(this.tag, `motor session heartbeat failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8696
+ log24.warn(this.tag, `motor session heartbeat failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8094
8697
  });
8095
8698
  }, MOTOR_SESSION_HEARTBEAT_MS);
8096
8699
  heartbeat.unref?.();
@@ -8167,9 +8770,7 @@ ${prompt}`;
8167
8770
  }
8168
8771
  }
8169
8772
  }
8170
- async finishMotorStageRun(card, ctx, disposition = {
8171
- status: "completed"
8172
- }) {
8773
+ async finishMotorStageRun(card, ctx, disposition = { status: "completed" }) {
8173
8774
  const worktreePath = this.worktreePath;
8174
8775
  if (worktreePath) {
8175
8776
  commitUncommittedChanges(worktreePath, card);
@@ -8177,36 +8778,17 @@ ${prompt}`;
8177
8778
  try {
8178
8779
  pushBranch3(this.branchName, worktreePath);
8179
8780
  } catch (err) {
8180
- log22.error(this.tag, `push after the motor stage "${ctx.stage.name}" failed for ${this.branchName}: ${err instanceof Error ? err.message : err}`);
8781
+ log24.error(this.tag, `push after the motor stage "${ctx.stage.name}" failed for ${this.branchName}: ${err instanceof Error ? err.message : err}`);
8181
8782
  }
8182
8783
  }
8183
8784
  }
8184
8785
  const completionColumn = this.config.completion.moveToColumn;
8185
8786
  if (completionColumn) {
8186
- await moveCardToColumn(this.client, card, completionColumn);
8187
- try {
8188
- await releaseAssignedAgent(this.client, card.id);
8189
- } catch (err) {
8190
- log22.warn(this.tag, `assignment release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8191
- }
8192
- if (this.onCardCompleted) {
8193
- try {
8194
- await this.onCardCompleted(card);
8195
- } catch (err) {
8196
- log22.warn(this.tag, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8197
- }
8198
- }
8787
+ await transferCardToCompletion({ client: this.client, tag: this.tag }, card, completionColumn, this.onCardCompleted);
8199
8788
  } else {
8200
- log22.warn(this.tag, `completion.moveToColumn is empty — #${card.short_id} stays in its current column after the motor stage run`);
8201
- }
8202
- try {
8203
- await this.client.endAgentSession(card.id, {
8204
- ...disposition,
8205
- progressPercent: 100
8206
- });
8207
- } catch (err) {
8208
- log22.error(this.tag, `endAgentSession after the motor stage run failed on #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8789
+ log24.warn(this.tag, `completion.moveToColumn is empty — #${card.short_id} stays in its current column after the motor stage run`);
8209
8790
  }
8791
+ await endRunSession({ client: this.client, tag: this.tag }, card, disposition, {}, "log");
8210
8792
  await this.closeoutMotorWorktree(card);
8211
8793
  }
8212
8794
  async closeoutMotorWorktree(card) {
@@ -8216,7 +8798,7 @@ ${prompt}`;
8216
8798
  try {
8217
8799
  await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined);
8218
8800
  } catch {
8219
- log22.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
8801
+ log24.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
8220
8802
  }
8221
8803
  this.worktreePath = null;
8222
8804
  }
@@ -8230,7 +8812,7 @@ ${prompt}`;
8230
8812
  });
8231
8813
  return handoff ? renderInheritedHandoffSection(handoff) : "";
8232
8814
  } catch (err) {
8233
- log22.warn(this.tag, `inherited-handoff fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
8815
+ log24.warn(this.tag, `inherited-handoff fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
8234
8816
  return "";
8235
8817
  }
8236
8818
  }
@@ -8247,9 +8829,9 @@ ${prompt}`;
8247
8829
  nextStageNeeds: "Pick up from the produced artifact above; treat the recorded decisions as settled."
8248
8830
  });
8249
8831
  await this.client.addComment(card.id, body, { commentType: "decision" });
8250
- log22.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
8832
+ log24.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
8251
8833
  } catch (err) {
8252
- log22.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8834
+ log24.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8253
8835
  }
8254
8836
  }
8255
8837
  async collectStageGateEvidence(card, stage, worktreePath, subtasks) {
@@ -8261,12 +8843,12 @@ ${prompt}`;
8261
8843
  return null;
8262
8844
  }
8263
8845
  if (gate.pendingEngine === true) {
8264
- log22.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
8846
+ log24.info(this.tag, `Stage "${stage.name}" gate "${gate.kind}" is advisory — skipping enforcement`);
8265
8847
  return null;
8266
8848
  }
8267
8849
  const review = gate.kind === "review_passed" ? parseReviewOutput(this.lastRunText) : undefined;
8268
8850
  if (review) {
8269
- log22.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
8851
+ log24.info(this.tag, `Review-gated stage "${stage.name}" verdict: ${review.verdict} (${review.findings.length} finding(s))`);
8270
8852
  }
8271
8853
  const registry = buildGateCollectorRegistry2({
8272
8854
  build: {
@@ -8295,10 +8877,10 @@ ${prompt}`;
8295
8877
  const evaluation = gateEvaluate(gate, evidence);
8296
8878
  const insert = toStageGateEvidenceInsert(context, evidence);
8297
8879
  await this.client.request("POST", `/cards/${encodeURIComponent(card.id)}/stage-gate-evidence`, insert);
8298
- log22.info(this.tag, `Recorded ${gate.kind} gate evidence for #${card.short_id} stage "${stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
8880
+ log24.info(this.tag, `Recorded ${gate.kind} gate evidence for #${card.short_id} stage "${stage.name}": result=${evidence.result} passed=${evaluation.passed}`);
8299
8881
  return evaluation;
8300
8882
  } catch (err) {
8301
- log22.warn(this.tag, `stage-gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8883
+ log24.warn(this.tag, `stage-gate evidence collection failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8302
8884
  return null;
8303
8885
  }
8304
8886
  }
@@ -8314,7 +8896,7 @@ ${prompt}`;
8314
8896
  runId: this.runId ?? undefined
8315
8897
  });
8316
8898
  } catch (err) {
8317
- log22.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8899
+ log24.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
8318
8900
  return { kind: "no_advance" };
8319
8901
  }
8320
8902
  }
@@ -8324,7 +8906,7 @@ ${prompt}`;
8324
8906
  this.modelChoice = choice;
8325
8907
  const { model, escalated, source } = choice;
8326
8908
  if (source !== "policy" || escalated) {
8327
- log22.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${this.sizing?.tier ?? "none"})`);
8909
+ log24.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${this.sizing?.tier ?? "none"})`);
8328
8910
  }
8329
8911
  return model;
8330
8912
  }
@@ -8338,7 +8920,7 @@ ${prompt}`;
8338
8920
  encoding: "utf-8"
8339
8921
  }).trim();
8340
8922
  } catch (err) {
8341
- log22.warn(this.tag, `Sizing #${card.short_id}: could not resolve the repo root (${err instanceof Error ? err.message : String(err)}) — using the policy fallback`);
8923
+ log24.warn(this.tag, `Sizing #${card.short_id}: could not resolve the repo root (${err instanceof Error ? err.message : String(err)}) — using the policy fallback`);
8342
8924
  return null;
8343
8925
  }
8344
8926
  const sized = await sizeRun({
@@ -8350,7 +8932,7 @@ ${prompt}`;
8350
8932
  description: card.description,
8351
8933
  model
8352
8934
  });
8353
- log22.info(this.tag, sized ? `Sized #${card.short_id}: complexity ${sized.complexity}/10 -> ${sized.tier}` : `Sizing #${card.short_id} produced no verdict — using the policy fallback`);
8935
+ log24.info(this.tag, sized ? `Sized #${card.short_id}: complexity ${sized.complexity}/10 -> ${sized.tier}` : `Sizing #${card.short_id} produced no verdict — using the policy fallback`);
8354
8936
  return sized;
8355
8937
  }
8356
8938
  recordRunSized() {
@@ -8392,44 +8974,44 @@ ${prompt}`;
8392
8974
  commentType: "blocker"
8393
8975
  });
8394
8976
  giveUpCommentId = res?.comment?.id ?? null;
8395
- log22.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
8977
+ log24.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
8396
8978
  } catch (err) {
8397
- log22.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
8979
+ log24.warn(this.tag, `failed to post give-up comment for ${cardId}: ${err instanceof Error ? err.message : err}`);
8398
8980
  }
8399
8981
  if (this.config.budget.pause.enabled) {
8400
8982
  const waitHours = this.config.budget.pause.waitHours;
8401
8983
  const until = computeDecisionDeadline(waitHours);
8402
8984
  try {
8403
8985
  await this.client.updateAgentProgress(cardId, {
8404
- agentIdentifier: agentIdentifier(this.id),
8986
+ agentIdentifier: this.sessionIdentifier,
8405
8987
  agentName: AGENT_NAME,
8406
8988
  status: "blocked",
8407
8989
  currentTask: "Waiting for your decision on the attempt budget",
8408
8990
  awaitingDecisionUntil: new Date(until).toISOString()
8409
8991
  });
8410
8992
  } catch (err) {
8411
- log22.warn(this.tag, `Failed to mark the attempt cap pending for ${cardId}: ${err}`);
8993
+ log24.warn(this.tag, `Failed to mark the attempt cap pending for ${cardId}: ${err}`);
8412
8994
  }
8413
8995
  try {
8414
8996
  await this.stateStore.markAwaitingDecision(cardId, {
8415
8997
  until,
8416
8998
  blockerCommentId: giveUpCommentId,
8417
- agentIdentifier: agentIdentifier(this.id)
8999
+ agentIdentifier: this.sessionIdentifier
8418
9000
  });
8419
9001
  } catch (err) {
8420
- log22.warn(this.tag, `Failed to record the attempt-cap decision locally for ${cardId}: ${err}`);
9002
+ log24.warn(this.tag, `Failed to record the attempt-cap decision locally for ${cardId}: ${err}`);
8421
9003
  }
8422
9004
  }
8423
9005
  }
8424
9006
  }
8425
9007
  } catch (err) {
8426
- log22.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
9008
+ log24.warn(this.tag, `recordOutcome(${outcome}) failed: ${err instanceof Error ? err.message : err}`);
8427
9009
  }
8428
9010
  }
8429
9011
  async pause() {
8430
9012
  if (!this.isActive || !this.process || this.process.killed)
8431
9013
  return;
8432
- log22.info(this.tag, `Pausing work on ${this.cardId}`);
9014
+ log24.info(this.tag, `Pausing work on ${this.cardId}`);
8433
9015
  signalGroup2(this.process, "SIGSTOP");
8434
9016
  if (this.timeoutTimer) {
8435
9017
  clearTimeout(this.timeoutTimer);
@@ -8438,34 +9020,34 @@ ${prompt}`;
8438
9020
  if (this.cardId) {
8439
9021
  try {
8440
9022
  await this.client.updateAgentProgress(this.cardId, {
8441
- agentIdentifier: agentIdentifier(this.id),
9023
+ agentIdentifier: this.sessionIdentifier,
8442
9024
  agentName: AGENT_NAME,
8443
9025
  status: "paused"
8444
9026
  });
8445
9027
  } catch {
8446
- log22.warn(this.tag, "Failed to update agent session to paused");
9028
+ log24.warn(this.tag, "Failed to update agent session to paused");
8447
9029
  }
8448
9030
  }
8449
9031
  }
8450
9032
  async resume() {
8451
9033
  if (!this.isActive || !this.process || this.process.killed)
8452
9034
  return;
8453
- log22.info(this.tag, `Resuming work on ${this.cardId}`);
9035
+ log24.info(this.tag, `Resuming work on ${this.cardId}`);
8454
9036
  signalGroup2(this.process, "SIGCONT");
8455
9037
  this.timeoutTimer = setTimeout(() => {
8456
- log22.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
9038
+ log24.warn(this.tag, `Timeout reached (${this.config.maxTimeout}ms), cancelling`);
8457
9039
  this.timedOut = true;
8458
9040
  this.cancel("timeout");
8459
9041
  }, this.config.maxTimeout);
8460
9042
  if (this.cardId) {
8461
9043
  try {
8462
9044
  await this.client.updateAgentProgress(this.cardId, {
8463
- agentIdentifier: agentIdentifier(this.id),
9045
+ agentIdentifier: this.sessionIdentifier,
8464
9046
  agentName: AGENT_NAME,
8465
9047
  status: "working"
8466
9048
  });
8467
9049
  } catch {
8468
- log22.warn(this.tag, "Failed to update agent session to working");
9050
+ log24.warn(this.tag, "Failed to update agent session to working");
8469
9051
  }
8470
9052
  }
8471
9053
  }
@@ -8474,7 +9056,7 @@ ${prompt}`;
8474
9056
  return;
8475
9057
  this.aborted = true;
8476
9058
  this.state = "cancelling";
8477
- log22.info(this.tag, `Cancelling work on ${this.cardId}`);
9059
+ log24.info(this.tag, `Cancelling work on ${this.cardId}`);
8478
9060
  this.motorAbort?.abort();
8479
9061
  if (this.sdkRunner) {
8480
9062
  await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
@@ -8492,16 +9074,16 @@ ${prompt}`;
8492
9074
  ...buildTokenPayload(stats)
8493
9075
  });
8494
9076
  } catch (err) {
8495
- log22.warn(this.tag, `endAgentSession after cancel failed: ${err instanceof Error ? err.message : err}`);
9077
+ log24.warn(this.tag, `endAgentSession after cancel failed: ${err instanceof Error ? err.message : err}`);
8496
9078
  }
8497
9079
  }
8498
9080
  }
8499
9081
  async runPlanningPhase(enriched) {
8500
9082
  const planning = this.config.planning;
8501
9083
  const { card } = enriched;
8502
- log22.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
9084
+ log24.info(this.tag, `Planning pass for #${card.short_id} (mode=${planning.mode}, model=${planning.model})`);
8503
9085
  await this.client.updateAgentProgress(card.id, {
8504
- agentIdentifier: agentIdentifier(this.id),
9086
+ agentIdentifier: this.sessionIdentifier,
8505
9087
  agentName: AGENT_NAME,
8506
9088
  status: "working",
8507
9089
  currentTask: "Planning approach (read-only)",
@@ -8512,7 +9094,7 @@ ${prompt}`;
8512
9094
  let planTimedOut = false;
8513
9095
  const planTimeout = setTimeout(() => {
8514
9096
  planTimedOut = true;
8515
- log22.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
9097
+ log24.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
8516
9098
  if (this.sdkRunner) {
8517
9099
  this.sdkRunner.stop("timeout").catch(() => {});
8518
9100
  } else if (this.process && !this.process.killed) {
@@ -8530,7 +9112,7 @@ ${prompt}`;
8530
9112
  initialPhase: "planning"
8531
9113
  });
8532
9114
  } catch (err) {
8533
- log22.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
9115
+ log24.warn(this.tag, `Planning pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
8534
9116
  return false;
8535
9117
  } finally {
8536
9118
  clearTimeout(planTimeout);
@@ -8548,7 +9130,7 @@ ${prompt}`;
8548
9130
  }
8549
9131
  const planText = stats?.lastAssistantText ?? "";
8550
9132
  if (!planText.trim()) {
8551
- log22.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
9133
+ log24.warn(this.tag, `Planning pass for #${card.short_id} produced no text — implementing directly`);
8552
9134
  return false;
8553
9135
  }
8554
9136
  const artifact = extractPlanArtifact(planText, card.title);
@@ -8569,9 +9151,9 @@ ${prompt}`;
8569
9151
  });
8570
9152
  planId = createdId;
8571
9153
  }
8572
- log22.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
9154
+ log24.info(this.tag, `Stored plan ${planId ?? "(unlinked)"} for #${card.short_id} (${artifact.tasks.length} tasks)`);
8573
9155
  } catch (err) {
8574
- log22.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
9156
+ log24.warn(this.tag, `Failed to store/link plan (non-fatal): ${err instanceof Error ? err.message : err}`);
8575
9157
  }
8576
9158
  if (planning.mode === "gated" && planId) {
8577
9159
  try {
@@ -8590,11 +9172,11 @@ ${prompt}`;
8590
9172
  ...buildTokenPayload(stats)
8591
9173
  }
8592
9174
  }, { store: this.stateStore, runId: this.runId ?? undefined });
8593
- log22.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
9175
+ log24.info(this.tag, `#${card.short_id} parked in "${planning.awaitingApprovalColumn}" for plan approval`);
8594
9176
  this.lastSessionStats = undefined;
8595
9177
  return true;
8596
9178
  } catch (err) {
8597
- log22.warn(this.tag, `Gated park failed for #${card.short_id} (non-fatal, implementing directly): ${err instanceof TransitionError ? err.detail : err instanceof Error ? err.message : err}`);
9179
+ log24.warn(this.tag, `Gated park failed for #${card.short_id} (non-fatal, implementing directly): ${err instanceof TransitionError ? err.detail : err instanceof Error ? err.message : err}`);
8598
9180
  }
8599
9181
  }
8600
9182
  if (planId && planning.postComment) {
@@ -8604,7 +9186,7 @@ ${prompt}`;
8604
9186
  agentSessionId: this.sessionId ?? undefined
8605
9187
  });
8606
9188
  } catch (err) {
8607
- log22.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
9189
+ log24.warn(this.tag, `Failed to post advisory plan comment (non-fatal): ${err instanceof Error ? err.message : err}`);
8608
9190
  }
8609
9191
  }
8610
9192
  return false;
@@ -8614,12 +9196,12 @@ ${prompt}`;
8614
9196
  const { card } = enriched;
8615
9197
  const existing = await this.loadPinnedContract(card.id);
8616
9198
  if (existing) {
8617
- log22.info(this.tag, `Contract already pinned for #${card.short_id} (${existing.assertions.length} assertions) — reusing`);
9199
+ log24.info(this.tag, `Contract already pinned for #${card.short_id} (${existing.assertions.length} assertions) — reusing`);
8618
9200
  return;
8619
9201
  }
8620
- log22.info(this.tag, `Contract pass for #${card.short_id} (model=${contractCfg.model})`);
9202
+ log24.info(this.tag, `Contract pass for #${card.short_id} (model=${contractCfg.model})`);
8621
9203
  await this.client.updateAgentProgress(card.id, {
8622
- agentIdentifier: agentIdentifier(this.id),
9204
+ agentIdentifier: this.sessionIdentifier,
8623
9205
  agentName: AGENT_NAME,
8624
9206
  status: "working",
8625
9207
  currentTask: "Writing acceptance contract (read-only)",
@@ -8630,7 +9212,7 @@ ${prompt}`;
8630
9212
  let contractTimedOut = false;
8631
9213
  const contractTimeout = setTimeout(() => {
8632
9214
  contractTimedOut = true;
8633
- log22.warn(this.tag, "Contract pass exceeded timeout — abandoning, implementing directly");
9215
+ log24.warn(this.tag, "Contract pass exceeded timeout — abandoning, implementing directly");
8634
9216
  if (this.sdkRunner) {
8635
9217
  this.sdkRunner.stop("timeout").catch(() => {});
8636
9218
  } else if (this.process && !this.process.killed) {
@@ -8648,7 +9230,7 @@ ${prompt}`;
8648
9230
  initialPhase: "planning"
8649
9231
  });
8650
9232
  } catch (err) {
8651
- log22.warn(this.tag, `Contract pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
9233
+ log24.warn(this.tag, `Contract pass failed (non-fatal): ${err instanceof Error ? err.message : err}`);
8652
9234
  return;
8653
9235
  } finally {
8654
9236
  clearTimeout(contractTimeout);
@@ -8666,12 +9248,12 @@ ${prompt}`;
8666
9248
  }
8667
9249
  const contractText = stats?.lastAssistantText ?? "";
8668
9250
  if (!contractText.trim()) {
8669
- log22.warn(this.tag, `Contract pass for #${card.short_id} produced no text — implementing directly`);
9251
+ log24.warn(this.tag, `Contract pass for #${card.short_id} produced no text — implementing directly`);
8670
9252
  return;
8671
9253
  }
8672
9254
  const contract = extractContract(contractText, card);
8673
9255
  if (contract.assertions.length < contractCfg.minAssertions) {
8674
- log22.warn(this.tag, `Contract for #${card.short_id} had ${contract.assertions.length} assertion(s) (< ${contractCfg.minAssertions}) — not pinning, implementing directly`);
9256
+ log24.warn(this.tag, `Contract for #${card.short_id} had ${contract.assertions.length} assertion(s) (< ${contractCfg.minAssertions}) — not pinning, implementing directly`);
8675
9257
  return;
8676
9258
  }
8677
9259
  try {
@@ -8679,9 +9261,9 @@ ${prompt}`;
8679
9261
  commentType: "decision",
8680
9262
  agentSessionId: this.sessionId ?? undefined
8681
9263
  });
8682
- log22.info(this.tag, `Pinned acceptance contract for #${card.short_id} (${contract.assertions.length} assertions)`);
9264
+ log24.info(this.tag, `Pinned acceptance contract for #${card.short_id} (${contract.assertions.length} assertions)`);
8683
9265
  } catch (err) {
8684
- log22.warn(this.tag, `Failed to pin contract (non-fatal): ${err instanceof Error ? err.message : err}`);
9266
+ log24.warn(this.tag, `Failed to pin contract (non-fatal): ${err instanceof Error ? err.message : err}`);
8685
9267
  }
8686
9268
  }
8687
9269
  async loadPinnedContract(cardId) {
@@ -8691,7 +9273,7 @@ ${prompt}`;
8691
9273
  return null;
8692
9274
  return extractPinnedContract(comments, this.identity);
8693
9275
  } catch (err) {
8694
- log22.warn(this.tag, `pinned-contract fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
9276
+ log24.warn(this.tag, `pinned-contract fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
8695
9277
  return null;
8696
9278
  }
8697
9279
  }
@@ -8704,13 +9286,13 @@ ${prompt}`;
8704
9286
  const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
8705
9287
  messages = res.messages ?? [];
8706
9288
  } catch (err) {
8707
- log22.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
9289
+ log24.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
8708
9290
  return;
8709
9291
  }
8710
9292
  if (messages.length === 0)
8711
9293
  return;
8712
9294
  this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
8713
- log22.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
9295
+ log24.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
8714
9296
  this.state = "running";
8715
9297
  await this.recordPhase("running");
8716
9298
  try {
@@ -8721,7 +9303,7 @@ ${prompt}`;
8721
9303
  ...this.activeRunSpawnOpts ?? {}
8722
9304
  });
8723
9305
  } catch (err) {
8724
- log22.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
9306
+ log24.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
8725
9307
  return;
8726
9308
  }
8727
9309
  }
@@ -8752,10 +9334,10 @@ ${prompt}`;
8752
9334
  "--",
8753
9335
  prompt
8754
9336
  ];
8755
- log22.info(this.tag, `Spawning: claude ${args.slice(0, 4).join(" ")} ...`);
9337
+ log24.info(this.tag, `Spawning: claude ${args.slice(0, 4).join(" ")} ...`);
8756
9338
  const runLog = openRunLog(this.tag, this.runId, card.short_id);
8757
9339
  if (runLog) {
8758
- log22.info(this.tag, `Run log: ${runLog.path}`);
9340
+ log24.info(this.tag, `Run log: ${runLog.path}`);
8759
9341
  runLog.stream.write(`# run=${this.runId} card=#${card.short_id} started=${new Date().toISOString()}
8760
9342
  ` + `# args: ${args.slice(0, -2).join(" ")} -- <prompt:${prompt.length} chars>
8761
9343
 
@@ -8766,7 +9348,7 @@ ${prompt}`;
8766
9348
  stdio: ["ignore", "pipe", "pipe"]
8767
9349
  });
8768
9350
  const parser = new StreamParser;
8769
- this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
9351
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, initialPhase);
8770
9352
  this.progressTracker.setRequestedModel(model);
8771
9353
  this.progressTracker.attach(parser);
8772
9354
  this.cliRunner?.attach(parser);
@@ -8786,7 +9368,7 @@ ${prompt}`;
8786
9368
  this.captureCliSessionId(parser.sessionId);
8787
9369
  });
8788
9370
  parser.on("parse_error", (msg) => {
8789
- log22.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
9371
+ log24.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
8790
9372
  runLog?.stream.write(`
8791
9373
  [parse_error] ${msg}
8792
9374
  `);
@@ -8853,16 +9435,16 @@ ${prompt}`;
8853
9435
  const disallowedTools = opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined;
8854
9436
  const initialPhase = opts.initialPhase ?? "exploring";
8855
9437
  const sdkCfg = this.config.sdk;
8856
- log22.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
9438
+ log24.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
8857
9439
  const runLog = openRunLog(this.tag, this.runId, card.short_id);
8858
9440
  if (runLog) {
8859
- log22.info(this.tag, `Run log: ${runLog.path}`);
9441
+ log24.info(this.tag, `Run log: ${runLog.path}`);
8860
9442
  runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
8861
9443
  ` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
8862
9444
 
8863
9445
  `);
8864
9446
  }
8865
- this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
9447
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.sessionIdentifier, subtasks, initialPhase);
8866
9448
  this.progressTracker.setRequestedModel(model);
8867
9449
  if (this.cliRunner) {
8868
9450
  this.progressTracker.setRunEventSink(this.cliRunner);
@@ -8980,7 +9562,7 @@ ${prompt}`;
8980
9562
  try {
8981
9563
  await teardownWorktree2(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
8982
9564
  } catch {
8983
- log22.warn(this.tag, "Failed to cleanup worktree");
9565
+ log24.warn(this.tag, "Failed to cleanup worktree");
8984
9566
  }
8985
9567
  }
8986
9568
  this.process = null;
@@ -8995,7 +9577,7 @@ ${prompt}`;
8995
9577
  this.runTurns = 0;
8996
9578
  }
8997
9579
  }
8998
- var TAG21 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, MOTOR_SESSION_HEARTBEAT_MS = 60000, MOTOR_RUN_PROGRESS_PERCENT = 10, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT, MAX_STAGE_NAME_CHARS = 80, MAX_HANDOFF_CHARS = 400;
9580
+ var TAG22 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, MOTOR_SESSION_HEARTBEAT_MS = 60000, MOTOR_RUN_PROGRESS_PERCENT = 10, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT, MAX_STAGE_NAME_CHARS = 80, MAX_HANDOFF_CHARS = 400;
8999
9581
  var init_worker = __esm(() => {
9000
9582
  init_dist();
9001
9583
  init_board_helpers();
@@ -9003,12 +9585,14 @@ var init_worker = __esm(() => {
9003
9585
  init_cli_agent_runner();
9004
9586
  init_completion();
9005
9587
  init_contract_phase();
9588
+ init_fanout();
9006
9589
  init_motor_driver();
9007
9590
  init_plan_phase();
9008
9591
  init_progress_tracker();
9009
9592
  init_prompt();
9010
9593
  init_review_completion();
9011
9594
  init_review_knowledge();
9595
+ init_run_closeout();
9012
9596
  init_run_log();
9013
9597
  init_stage_advance();
9014
9598
  init_state_store();
@@ -9022,7 +9606,7 @@ var init_worker = __esm(() => {
9022
9606
  import {
9023
9607
  cooldownMsFor,
9024
9608
  describeApiError as describeApiError2,
9025
- log as log23
9609
+ log as log25
9026
9610
  } from "@gethmy/harness";
9027
9611
  async function routeBudgetDecision(d, run, actions, cardId) {
9028
9612
  if (!run) {
@@ -9098,41 +9682,41 @@ class Pool {
9098
9682
  }
9099
9683
  async enqueue(card, column, labels, subtasks, mode = "implement") {
9100
9684
  if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
9101
- log23.debug(TAG22, `Card ${card.id} already queued, active, or reserved, skipping`);
9685
+ log25.debug(TAG23, `Card ${card.id} already queued, active, or reserved, skipping`);
9102
9686
  return;
9103
9687
  }
9104
9688
  this.reservations.add(card.id);
9105
9689
  try {
9106
9690
  if (mode === "implement") {
9107
9691
  if (this.authPaused) {
9108
- log23.debug(TAG22, `#${card.short_id} held — agent paused (auth error)`);
9692
+ log25.debug(TAG23, `#${card.short_id} held — agent paused (auth error)`);
9109
9693
  await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
9110
9694
  return;
9111
9695
  }
9112
9696
  const cooldownMs = this.apiCooldownRemainingMs();
9113
9697
  if (cooldownMs > 0) {
9114
- log23.debug(TAG22, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9698
+ log25.debug(TAG23, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
9115
9699
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
9116
9700
  return;
9117
9701
  }
9118
9702
  const decision = this.budget.check(card.id);
9119
9703
  if (!decision.allow) {
9120
9704
  if (decision.reason === "daily_budget") {
9121
- log23.warn(TAG22, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9705
+ log25.warn(TAG23, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
9122
9706
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
9123
9707
  } else {
9124
- log23.debug(TAG22, `#${card.short_id} gave up: ${decision.detail}`);
9708
+ log25.debug(TAG23, `#${card.short_id} gave up: ${decision.detail}`);
9125
9709
  }
9126
9710
  return;
9127
9711
  }
9128
9712
  const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
9129
9713
  if (blockers === null) {
9130
- log23.warn(TAG22, `#${card.short_id} blocker check failed — deferring to next tick`);
9714
+ log25.warn(TAG23, `#${card.short_id} blocker check failed — deferring to next tick`);
9131
9715
  return;
9132
9716
  }
9133
9717
  if (blockers.length > 0) {
9134
9718
  const list = blockers.map((b) => `#${b.shortId}`).join(", ");
9135
- log23.info(TAG22, `#${card.short_id} blocked by ${list} — waiting`);
9719
+ log25.info(TAG23, `#${card.short_id} blocked by ${list} — waiting`);
9136
9720
  await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
9137
9721
  return;
9138
9722
  }
@@ -9164,7 +9748,7 @@ class Pool {
9164
9748
  });
9165
9749
  this.lastWaitingEmit.set(cardId, currentTask);
9166
9750
  } catch (err) {
9167
- log23.debug(TAG22, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
9751
+ log25.debug(TAG23, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
9168
9752
  }
9169
9753
  }
9170
9754
  noteApiError(err) {
@@ -9172,7 +9756,7 @@ class Pool {
9172
9756
  return;
9173
9757
  if (err.kind === "auth") {
9174
9758
  if (!this.authPaused) {
9175
- log23.error(TAG22, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
9759
+ log25.error(TAG23, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
9176
9760
  }
9177
9761
  this.authPaused = true;
9178
9762
  return;
@@ -9181,7 +9765,7 @@ class Pool {
9181
9765
  const until = Date.now() + cooldownMs;
9182
9766
  if (until > this.apiCooldownUntil) {
9183
9767
  this.apiCooldownUntil = until;
9184
- log23.warn(TAG22, `${describeApiError2(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
9768
+ log25.warn(TAG23, `${describeApiError2(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
9185
9769
  }
9186
9770
  }
9187
9771
  apiCooldownRemainingMs() {
@@ -9195,13 +9779,13 @@ class Pool {
9195
9779
  const removed = queue.remove(cardId);
9196
9780
  if (removed) {
9197
9781
  this.cardDataCache.delete(cardId);
9198
- log23.info(TAG22, `Removed #${removed.shortId} from ${removed.mode} queue`);
9782
+ log25.info(TAG23, `Removed #${removed.shortId} from ${removed.mode} queue`);
9199
9783
  return;
9200
9784
  }
9201
9785
  }
9202
9786
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
9203
9787
  if (worker) {
9204
- log23.info(TAG22, `Cancelling worker ${worker.id} for card ${cardId}`);
9788
+ log25.info(TAG23, `Cancelling worker ${worker.id} for card ${cardId}`);
9205
9789
  await worker.cancel("unassigned");
9206
9790
  }
9207
9791
  }
@@ -9238,10 +9822,10 @@ class Pool {
9238
9822
  }
9239
9823
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
9240
9824
  if (!worker) {
9241
- log23.debug(TAG22, `No active worker for card ${cardId}, ignoring ${command}`);
9825
+ log25.debug(TAG23, `No active worker for card ${cardId}, ignoring ${command}`);
9242
9826
  return;
9243
9827
  }
9244
- log23.info(TAG22, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
9828
+ log25.info(TAG23, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
9245
9829
  switch (command) {
9246
9830
  case "pause":
9247
9831
  await worker.pause();
@@ -9289,7 +9873,7 @@ class Pool {
9289
9873
  };
9290
9874
  }
9291
9875
  async shutdown() {
9292
- log23.info(TAG22, "Shutting down pool...");
9876
+ log25.info(TAG23, "Shutting down pool...");
9293
9877
  this.shuttingDown = true;
9294
9878
  const active = [
9295
9879
  ...this.implWorkers.filter((w) => w.isActive),
@@ -9297,7 +9881,7 @@ class Pool {
9297
9881
  ];
9298
9882
  await Promise.all(active.map((w) => w.cancel("shutdown")));
9299
9883
  this.sleepGuard.stop();
9300
- log23.info(TAG22, "Pool shutdown complete");
9884
+ log25.info(TAG23, "Pool shutdown complete");
9301
9885
  }
9302
9886
  async drainBudgetDecisions(cardId) {
9303
9887
  const targets = cardId ? [cardId] : [
@@ -9328,7 +9912,7 @@ class Pool {
9328
9912
  try {
9329
9913
  ({ decisions } = await this.client.getBudgetDecisions(cardId, new Date(sinceMs).toISOString()));
9330
9914
  } catch (err) {
9331
- log23.warn(TAG22, `getBudgetDecisions failed for ${cardId}: ${err}`);
9915
+ log25.warn(TAG23, `getBudgetDecisions failed for ${cardId}: ${err}`);
9332
9916
  return;
9333
9917
  }
9334
9918
  if (decisions.length > 0) {
@@ -9363,7 +9947,7 @@ class Pool {
9363
9947
  ...run.blockerCommentId ? { replyToId: run.blockerCommentId } : {}
9364
9948
  });
9365
9949
  } catch (err) {
9366
- log23.warn(TAG22, `Failed to post the expired-park closing comment for ${run.cardId}: ${err}`);
9950
+ log25.warn(TAG23, `Failed to post the expired-park closing comment for ${run.cardId}: ${err}`);
9367
9951
  }
9368
9952
  try {
9369
9953
  const { card } = await this.client.getCard(run.cardId);
@@ -9374,14 +9958,14 @@ class Pool {
9374
9958
  });
9375
9959
  }
9376
9960
  } catch (err) {
9377
- log23.error(TAG22, `Failed to move #${run.cardShortId} after an expired park: ${err}`);
9961
+ log25.error(TAG23, `Failed to move #${run.cardShortId} after an expired park: ${err}`);
9378
9962
  }
9379
9963
  try {
9380
9964
  await this.stateStore.endRun(run.runId, "failed", {
9381
9965
  errorMessage: "budget decision expired"
9382
9966
  });
9383
9967
  } catch (err) {
9384
- log23.warn(TAG22, `Failed to release the expired park for ${run.cardId}: ${err}`);
9968
+ log25.warn(TAG23, `Failed to release the expired park for ${run.cardId}: ${err}`);
9385
9969
  }
9386
9970
  }
9387
9971
  async releaseExpiredAttemptCap(cardId, blockerCommentId) {
@@ -9391,7 +9975,7 @@ class Pool {
9391
9975
  ...blockerCommentId ? { replyToId: blockerCommentId } : {}
9392
9976
  });
9393
9977
  } catch (err) {
9394
- log23.warn(TAG22, `Failed to post the expired attempt-cap note for ${cardId}: ${err}`);
9978
+ log25.warn(TAG23, `Failed to post the expired attempt-cap note for ${cardId}: ${err}`);
9395
9979
  }
9396
9980
  try {
9397
9981
  await this.client.endAgentSession(cardId, {
@@ -9400,14 +9984,14 @@ class Pool {
9400
9984
  failureSummary: "The attempt-budget decision expired with no answer. Reassign the card to grant a fresh attempt."
9401
9985
  });
9402
9986
  } catch (err) {
9403
- log23.warn(TAG22, `Failed to end the expired attempt-cap session for ${cardId}: ${err}`);
9987
+ log25.warn(TAG23, `Failed to end the expired attempt-cap session for ${cardId}: ${err}`);
9404
9988
  }
9405
9989
  await this.stateStore.clearAwaitingDecision(cardId);
9406
9990
  }
9407
9991
  async adoptGrantedRun(run) {
9408
9992
  if (this.isCardKnown(run.cardId))
9409
9993
  return;
9410
- log23.warn(TAG22, `#${run.cardShortId}: granted continue never reached a worker — re-enqueueing (${run.pipeline})`);
9994
+ log25.warn(TAG23, `#${run.cardShortId}: granted continue never reached a worker — re-enqueueing (${run.pipeline})`);
9411
9995
  await this.enqueueCard(run.cardId, run.pipeline);
9412
9996
  }
9413
9997
  sessionIdentityFor(run) {
@@ -9439,7 +10023,7 @@ class Pool {
9439
10023
  awaitingDecisionUntil: null
9440
10024
  });
9441
10025
  } catch (err) {
9442
- log23.warn(TAG22, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
10026
+ log25.warn(TAG23, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
9443
10027
  }
9444
10028
  if (run.blockerCommentId) {
9445
10029
  try {
@@ -9447,7 +10031,7 @@ class Pool {
9447
10031
  resolve: true
9448
10032
  });
9449
10033
  } catch (err) {
9450
- log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
10034
+ log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
9451
10035
  }
9452
10036
  }
9453
10037
  await this.enqueueCard(run.cardId, run.pipeline);
@@ -9459,7 +10043,7 @@ class Pool {
9459
10043
  resolve: true
9460
10044
  });
9461
10045
  } catch (err) {
9462
- log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
10046
+ log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
9463
10047
  }
9464
10048
  }
9465
10049
  try {
@@ -9468,7 +10052,7 @@ class Pool {
9468
10052
  awaitingDecisionUntil: null
9469
10053
  });
9470
10054
  } catch (err) {
9471
- log23.warn(TAG22, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
10055
+ log25.warn(TAG23, `Failed to clear the decision deadline for ${run.cardId}: ${err}`);
9472
10056
  }
9473
10057
  try {
9474
10058
  const { card } = await this.client.getCard(run.cardId);
@@ -9482,14 +10066,14 @@ class Pool {
9482
10066
  }
9483
10067
  });
9484
10068
  } catch (err) {
9485
- log23.error(TAG22, `Failed to hand #${run.cardShortId} back after a stop: ${err}`);
10069
+ log25.error(TAG23, `Failed to hand #${run.cardShortId} back after a stop: ${err}`);
9486
10070
  }
9487
10071
  try {
9488
10072
  await this.stateStore.endRun(run.runId, "failed", {
9489
10073
  errorMessage: "budget_decision_stop"
9490
10074
  });
9491
10075
  } catch (err) {
9492
- log23.warn(TAG22, `Failed to end the local run record for ${run.cardId}: ${err}`);
10076
+ log25.warn(TAG23, `Failed to end the local run record for ${run.cardId}: ${err}`);
9493
10077
  }
9494
10078
  }
9495
10079
  async grantAttempt(cardId) {
@@ -9502,7 +10086,7 @@ class Pool {
9502
10086
  awaitingDecisionUntil: null
9503
10087
  });
9504
10088
  } catch (err) {
9505
- log23.warn(TAG22, `Failed to clear the attempt-cap decision deadline for ${cardId}: ${err}`);
10089
+ log25.warn(TAG23, `Failed to clear the attempt-cap decision deadline for ${cardId}: ${err}`);
9506
10090
  }
9507
10091
  await this.enqueueCard(cardId, "implement");
9508
10092
  }
@@ -9513,7 +10097,7 @@ class Pool {
9513
10097
  try {
9514
10098
  await this.client.updateComment(blockerCommentId, { resolve: true });
9515
10099
  } catch (err) {
9516
- log23.warn(TAG22, `Failed to resolve the blocker comment: ${err}`);
10100
+ log25.warn(TAG23, `Failed to resolve the blocker comment: ${err}`);
9517
10101
  }
9518
10102
  }
9519
10103
  try {
@@ -9522,7 +10106,7 @@ class Pool {
9522
10106
  awaitingDecisionUntil: null
9523
10107
  });
9524
10108
  } catch (err) {
9525
- log23.warn(TAG22, `Failed to clear the decision deadline for ${cardId}: ${err}`);
10109
+ log25.warn(TAG23, `Failed to clear the decision deadline for ${cardId}: ${err}`);
9526
10110
  }
9527
10111
  try {
9528
10112
  await this.client.endAgentSession(cardId, {
@@ -9531,7 +10115,7 @@ class Pool {
9531
10115
  failureSummary: "Stopped by a human decision on the attempt budget."
9532
10116
  });
9533
10117
  } catch (err) {
9534
- log23.warn(TAG22, `Failed to end the attempt-cap session for ${cardId}: ${err}`);
10118
+ log25.warn(TAG23, `Failed to end the attempt-cap session for ${cardId}: ${err}`);
9535
10119
  }
9536
10120
  await this.stateStore.clearAwaitingDecision(cardId);
9537
10121
  }
@@ -9550,7 +10134,7 @@ class Pool {
9550
10134
  const columns = board.columns ?? [];
9551
10135
  const column = columns.find((c) => c.id === card.column_id);
9552
10136
  if (!column) {
9553
- log23.warn(TAG22, `#${card.short_id}: column not found — cannot re-enqueue`);
10137
+ log25.warn(TAG23, `#${card.short_id}: column not found — cannot re-enqueue`);
9554
10138
  return;
9555
10139
  }
9556
10140
  const labelMap = buildLabelMap(board.labels ?? []);
@@ -9558,7 +10142,7 @@ class Pool {
9558
10142
  const subtasks = card.subtasks ?? [];
9559
10143
  await this.enqueue(card, column, cardLabels, subtasks, mode);
9560
10144
  } catch (err) {
9561
- log23.error(TAG22, `Failed to re-enqueue ${cardId} after a budget decision: ${err}`);
10145
+ log25.error(TAG23, `Failed to re-enqueue ${cardId} after a budget decision: ${err}`);
9562
10146
  }
9563
10147
  }
9564
10148
  reservations = new Set;
@@ -9568,7 +10152,7 @@ class Pool {
9568
10152
  return false;
9569
10153
  const idle = workers.find((w) => w.isIdle);
9570
10154
  if (!idle) {
9571
- log23.debug(TAG22, `No idle ${label} workers (queue: ${queue.length})`);
10155
+ log25.debug(TAG23, `No idle ${label} workers (queue: ${queue.length})`);
9572
10156
  return false;
9573
10157
  }
9574
10158
  const next = queue.dequeue();
@@ -9576,18 +10160,18 @@ class Pool {
9576
10160
  return false;
9577
10161
  const data = this.cardDataCache.get(next.cardId);
9578
10162
  if (!data) {
9579
- log23.warn(TAG22, `No cached data for card ${next.cardId}, skipping`);
10163
+ log25.warn(TAG23, `No cached data for card ${next.cardId}, skipping`);
9580
10164
  return false;
9581
10165
  }
9582
10166
  this.cardDataCache.delete(next.cardId);
9583
10167
  this.lastWaitingEmit.delete(next.cardId);
9584
- log23.info(TAG22, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
10168
+ log25.info(TAG23, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
9585
10169
  this.sleepGuard.acquire();
9586
10170
  idle.run(data.card, data.column, data.labels, data.subtasks);
9587
10171
  return true;
9588
10172
  }
9589
10173
  }
9590
- var TAG22 = "pool";
10174
+ var TAG23 = "pool";
9591
10175
  var init_pool = __esm(() => {
9592
10176
  init_board_helpers();
9593
10177
  init_budget_pause();
@@ -9617,7 +10201,7 @@ import {
9617
10201
  } from "node:fs";
9618
10202
  import { homedir as homedir4 } from "node:os";
9619
10203
  import { dirname as dirname4, join as join5 } from "node:path";
9620
- import { log as log24 } from "@gethmy/harness";
10204
+ import { log as log26 } from "@gethmy/harness";
9621
10205
  function defaultRegistryPath() {
9622
10206
  return join5(homedir4(), ".harmony-mcp", "agent-ports.json");
9623
10207
  }
@@ -9631,7 +10215,7 @@ function load(path) {
9631
10215
  return parsed;
9632
10216
  return {};
9633
10217
  } catch (err) {
9634
- log24.warn(TAG23, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
10218
+ log26.warn(TAG24, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
9635
10219
  return {};
9636
10220
  }
9637
10221
  }
@@ -9649,7 +10233,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
9649
10233
  registry[projectId] = { ...entry, updatedAt: Date.now() };
9650
10234
  save(path, registry);
9651
10235
  } catch (err) {
9652
- log24.warn(TAG23, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
10236
+ log26.warn(TAG24, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
9653
10237
  }
9654
10238
  }
9655
10239
  function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
@@ -9665,14 +10249,14 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
9665
10249
  delete registry[projectId];
9666
10250
  save(path, registry);
9667
10251
  } catch (err) {
9668
- log24.warn(TAG23, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
10252
+ log26.warn(TAG24, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
9669
10253
  }
9670
10254
  }
9671
- var TAG23 = "port-registry";
10255
+ var TAG24 = "port-registry";
9672
10256
  var init_port_registry = () => {};
9673
10257
 
9674
10258
  // src/recovery.ts
9675
- import { log as log25, teardownWorktree as teardownWorktree3 } from "@gethmy/harness";
10259
+ import { log as log27, teardownWorktree as teardownWorktree3 } from "@gethmy/harness";
9676
10260
  function isProcessAlive(pid, currentPid) {
9677
10261
  if (pid === currentPid)
9678
10262
  return true;
@@ -9688,7 +10272,7 @@ async function fetchCardSafely(client, cardId) {
9688
10272
  const { card } = await client.getCard(cardId);
9689
10273
  return card;
9690
10274
  } catch (err) {
9691
- log25.warn(TAG24, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
10275
+ log27.warn(TAG25, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
9692
10276
  return null;
9693
10277
  }
9694
10278
  }
@@ -9698,7 +10282,7 @@ async function recoverOrphans(store, client, config) {
9698
10282
  return [];
9699
10283
  }
9700
10284
  const outcomes = [];
9701
- log25.info(TAG24, `recovering ${active.length} orphan run(s) from prior daemon`);
10285
+ log27.info(TAG25, `recovering ${active.length} orphan run(s) from prior daemon`);
9702
10286
  for (const run of active) {
9703
10287
  const outcome = {
9704
10288
  runId: run.runId,
@@ -9710,16 +10294,16 @@ async function recoverOrphans(store, client, config) {
9710
10294
  };
9711
10295
  outcomes.push(outcome);
9712
10296
  if (isBudgetHeldRun(run)) {
9713
- log25.info(TAG24, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
10297
+ log27.info(TAG25, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
9714
10298
  outcome.actions.push("skipped: held for a human budget decision");
9715
10299
  continue;
9716
10300
  }
9717
10301
  if (isProcessAlive(run.daemonPid, process.pid)) {
9718
- log25.warn(TAG24, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
10302
+ log27.warn(TAG25, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
9719
10303
  outcome.actions.push("skipped: daemon pid still alive");
9720
10304
  continue;
9721
10305
  }
9722
- log25.info(TAG24, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
10306
+ log27.info(TAG25, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
9723
10307
  await recoverRun(run, store, client, config, outcome, {
9724
10308
  rollbackAttempt: true
9725
10309
  });
@@ -9739,7 +10323,7 @@ async function recoverRun(run, store, client, config, outcome, opts = {}) {
9739
10323
  } catch (err) {
9740
10324
  const msg = err instanceof Error ? err.message : String(err);
9741
10325
  outcome.errors.push(`endAgentSession: ${msg}`);
9742
- log25.warn(TAG24, `endAgentSession failed for ${run.cardId}: ${msg}`);
10326
+ log27.warn(TAG25, `endAgentSession failed for ${run.cardId}: ${msg}`);
9743
10327
  }
9744
10328
  const card = await fetchCardSafely(client, run.cardId);
9745
10329
  if (card) {
@@ -9791,27 +10375,27 @@ async function recoverRun(run, store, client, config, outcome, opts = {}) {
9791
10375
  outcome.errors.push(`decrementAttempt: ${msg}`);
9792
10376
  }
9793
10377
  }
9794
- log25.info(TAG24, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
10378
+ log27.info(TAG25, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
9795
10379
  }
9796
- var TAG24 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
10380
+ var TAG25 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
9797
10381
  var init_recovery = __esm(() => {
9798
10382
  init_board_helpers();
9799
10383
  init_state_store();
9800
10384
  });
9801
10385
 
9802
10386
  // src/claim.ts
9803
- import { log as log26 } from "@gethmy/harness";
10387
+ import { log as log28 } from "@gethmy/harness";
9804
10388
  async function claimReviewCard(client, cardId, agentId) {
9805
10389
  try {
9806
10390
  const { claimed } = await client.claimCard(cardId, agentId);
9807
- log26.debug(TAG25, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
10391
+ log28.debug(TAG26, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
9808
10392
  return claimed;
9809
10393
  } catch (err) {
9810
- log26.error(TAG25, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
10394
+ log28.error(TAG26, `claim ${cardId} failed: ${err instanceof Error ? err.message : err}`);
9811
10395
  return false;
9812
10396
  }
9813
10397
  }
9814
- var TAG25 = "claim";
10398
+ var TAG26 = "claim";
9815
10399
  var init_claim = () => {};
9816
10400
 
9817
10401
  // src/strand-recovery.ts
@@ -9819,7 +10403,7 @@ var exports_strand_recovery = {};
9819
10403
  __export(exports_strand_recovery, {
9820
10404
  reclaimPreReviewStrands: () => reclaimPreReviewStrands
9821
10405
  });
9822
- import { log as log27, resolvePrUrl as resolvePrUrl2 } from "@gethmy/harness";
10406
+ import { log as log29, resolvePrUrl as resolvePrUrl2 } from "@gethmy/harness";
9823
10407
  async function reclaimPreReviewStrands(opts) {
9824
10408
  const {
9825
10409
  client,
@@ -9863,22 +10447,22 @@ async function reclaimPreReviewStrands(opts) {
9863
10447
  continue;
9864
10448
  const won = await claimReviewCard(client, card.id, agentId);
9865
10449
  if (!won) {
9866
- log27.debug(TAG26, `#${card.short_id} — lost the review claim race, skipping`);
10450
+ log29.debug(TAG27, `#${card.short_id} — lost the review claim race, skipping`);
9867
10451
  continue;
9868
10452
  }
9869
- log27.warn(TAG26, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
10453
+ log29.warn(TAG27, `#${card.short_id} claimed for review (branch pushed, no PR, unowned)`);
9870
10454
  reclaimed.push(card.id);
9871
10455
  if (opts.onClaimed) {
9872
10456
  try {
9873
10457
  await opts.onClaimed(card);
9874
10458
  } catch (err) {
9875
- log27.error(TAG26, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
10459
+ log29.error(TAG27, `onClaimed for #${card.short_id} failed: ${err instanceof Error ? err.message : err}`);
9876
10460
  }
9877
10461
  }
9878
10462
  }
9879
10463
  return reclaimed;
9880
10464
  }
9881
- var TAG26 = "strand-recovery";
10465
+ var TAG27 = "strand-recovery";
9882
10466
  var init_strand_recovery = __esm(() => {
9883
10467
  init_board_helpers();
9884
10468
  init_claim();
@@ -9887,7 +10471,7 @@ var init_strand_recovery = __esm(() => {
9887
10471
  });
9888
10472
 
9889
10473
  // src/reconcile.ts
9890
- import { detectGitProvider as detectGitProvider5, log as log28 } from "@gethmy/harness";
10474
+ import { detectGitProvider as detectGitProvider5, log as log30 } from "@gethmy/harness";
9891
10475
 
9892
10476
  class Reconciler {
9893
10477
  client;
@@ -9930,7 +10514,7 @@ class Reconciler {
9930
10514
  clearInterval(this.timer);
9931
10515
  this.timer = null;
9932
10516
  }
9933
- log28.info(TAG27, "Heartbeat stopped");
10517
+ log30.info(TAG28, "Heartbeat stopped");
9934
10518
  }
9935
10519
  async recoverStaleRuns() {
9936
10520
  if (!this.stateStore || !this.agentConfig)
@@ -9941,7 +10525,7 @@ class Reconciler {
9941
10525
  const pool = this.pool;
9942
10526
  for (const run of active) {
9943
10527
  if (isBudgetHeldRun(run)) {
9944
- log28.info(TAG27, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
10528
+ log30.info(TAG28, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
9945
10529
  continue;
9946
10530
  }
9947
10531
  const foreignDaemon = run.daemonPid !== process.pid;
@@ -9951,7 +10535,7 @@ class Reconciler {
9951
10535
  if (!daemonDead && !(heartbeatStale && ourZombie))
9952
10536
  continue;
9953
10537
  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`;
9954
- log28.warn(TAG27, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
10538
+ log30.warn(TAG28, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
9955
10539
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
9956
10540
  runId: run.runId,
9957
10541
  cardId: run.cardId,
@@ -9978,11 +10562,11 @@ class Reconciler {
9978
10562
  const stalledAt = Date.parse(card.updated_at ?? "");
9979
10563
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
9980
10564
  continue;
9981
- log28.warn(TAG27, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
10565
+ log30.warn(TAG28, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
9982
10566
  try {
9983
10567
  await this.client.moveCard(card.id, pickupCol.id);
9984
10568
  } catch (err) {
9985
- log28.error(TAG27, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
10569
+ log30.error(TAG28, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9986
10570
  }
9987
10571
  }
9988
10572
  }
@@ -10014,7 +10598,7 @@ class Reconciler {
10014
10598
  return;
10015
10599
  const cardLabels = resolveCardLabels(card, labelMap);
10016
10600
  const subtasks = card.subtasks ?? [];
10017
- log28.info(TAG27, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
10601
+ log30.info(TAG28, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
10018
10602
  await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
10019
10603
  }
10020
10604
  });
@@ -10038,11 +10622,11 @@ class Reconciler {
10038
10622
  const parkedAt = Date.parse(card.updated_at ?? "");
10039
10623
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
10040
10624
  continue;
10041
- log28.warn(TAG27, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
10625
+ log30.warn(TAG28, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
10042
10626
  try {
10043
10627
  await this.client.moveCard(card.id, pickupCol.id);
10044
10628
  } catch (err) {
10045
- log28.error(TAG27, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
10629
+ log30.error(TAG28, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
10046
10630
  }
10047
10631
  }
10048
10632
  }
@@ -10086,21 +10670,21 @@ class Reconciler {
10086
10670
  const subtasks = card.subtasks ?? [];
10087
10671
  const mode = route.mode;
10088
10672
  if (route.stage) {
10089
- log28.info(TAG27, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
10673
+ log30.info(TAG28, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
10090
10674
  }
10091
10675
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
10092
- log28.debug(TAG27, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
10676
+ log30.debug(TAG28, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
10093
10677
  continue;
10094
10678
  }
10095
10679
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
10096
- log28.debug(TAG27, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
10680
+ log30.debug(TAG28, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
10097
10681
  continue;
10098
10682
  }
10099
10683
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
10100
- log28.debug(TAG27, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
10684
+ log30.debug(TAG28, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
10101
10685
  continue;
10102
10686
  }
10103
- log28.info(TAG27, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
10687
+ log30.info(TAG28, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
10104
10688
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
10105
10689
  }
10106
10690
  }
@@ -10110,24 +10694,24 @@ class Reconciler {
10110
10694
  try {
10111
10695
  await this.pool.drainBudgetDecisions();
10112
10696
  } catch (err) {
10113
- log28.error(TAG27, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
10697
+ log30.error(TAG28, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
10114
10698
  }
10115
10699
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
10116
10700
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
10117
10701
  for (const knownId of knownCardIds) {
10118
10702
  if (!allAgentCardIds.has(knownId)) {
10119
- log28.info(TAG27, `Missed unassign: ${knownId} — removing`);
10703
+ log30.info(TAG28, `Missed unassign: ${knownId} — removing`);
10120
10704
  await this.pool.removeCard(knownId);
10121
10705
  }
10122
10706
  }
10123
10707
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
10124
- log28.debug(TAG27, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
10708
+ log30.debug(TAG28, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
10125
10709
  } catch (err) {
10126
- log28.error(TAG27, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
10710
+ log30.error(TAG28, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
10127
10711
  }
10128
10712
  }
10129
10713
  }
10130
- var TAG27 = "reconcile";
10714
+ var TAG28 = "reconcile";
10131
10715
  var init_reconcile = __esm(() => {
10132
10716
  init_board_helpers();
10133
10717
  init_recovery();
@@ -10142,7 +10726,7 @@ var exports_startup_banner = {};
10142
10726
  __export(exports_startup_banner, {
10143
10727
  createStartupBanner: () => createStartupBanner
10144
10728
  });
10145
- import { isPretty, log as log29 } from "@gethmy/harness";
10729
+ import { isPretty, log as log31 } from "@gethmy/harness";
10146
10730
  function createStartupBanner(config, version) {
10147
10731
  return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
10148
10732
  }
@@ -10167,7 +10751,7 @@ function prettyBanner(config, version) {
10167
10751
  checks.push({ kind: "ok", message });
10168
10752
  },
10169
10753
  warn(message) {
10170
- log29.warn(TAG28, message);
10754
+ log31.warn(TAG29, message);
10171
10755
  checks.push({ kind: "warn", message: message.split(`
10172
10756
  `, 1)[0] });
10173
10757
  },
@@ -10192,25 +10776,25 @@ function prettyBanner(config, version) {
10192
10776
  };
10193
10777
  }
10194
10778
  function jsonBanner(config, version) {
10195
- log29.info(TAG28, `Harmony Agent Daemon v${version} starting...`);
10196
- log29.info(TAG28, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
10779
+ log31.info(TAG29, `Harmony Agent Daemon v${version} starting...`);
10780
+ log31.info(TAG29, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
10197
10781
  if (config.agent.review.enabled) {
10198
- log29.info(TAG28, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
10782
+ log31.info(TAG29, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
10199
10783
  }
10200
10784
  let failed = false;
10201
10785
  return {
10202
10786
  setProjectName(_name) {},
10203
10787
  setGitProvider(provider) {
10204
- log29.info(TAG28, `Git provider: ${provider}`);
10788
+ log31.info(TAG29, `Git provider: ${provider}`);
10205
10789
  },
10206
10790
  setHttpPort(port) {
10207
- log29.info(TAG28, `HTTP server on port ${port}`);
10791
+ log31.info(TAG29, `HTTP server on port ${port}`);
10208
10792
  },
10209
10793
  check(message) {
10210
- log29.info(TAG28, message);
10794
+ log31.info(TAG29, message);
10211
10795
  },
10212
10796
  warn(message) {
10213
- log29.warn(TAG28, message);
10797
+ log31.warn(TAG29, message);
10214
10798
  },
10215
10799
  fail() {
10216
10800
  failed = true;
@@ -10218,7 +10802,7 @@ function jsonBanner(config, version) {
10218
10802
  async ready(message) {
10219
10803
  if (failed)
10220
10804
  return;
10221
- log29.info(TAG28, message);
10805
+ log31.info(TAG29, message);
10222
10806
  }
10223
10807
  };
10224
10808
  }
@@ -10299,7 +10883,7 @@ function cyan(s) {
10299
10883
  function yellow(s) {
10300
10884
  return `${ANSI.yellow}${s}${ANSI.reset}`;
10301
10885
  }
10302
- var TAG28 = "daemon", RULE_WIDTH = 70, ANSI;
10886
+ var TAG29 = "daemon", RULE_WIDTH = 70, ANSI;
10303
10887
  var init_startup_banner = __esm(() => {
10304
10888
  ANSI = {
10305
10889
  reset: "\x1B[0m",
@@ -10402,7 +10986,7 @@ var init_stream_parser_selftest = __esm(() => {
10402
10986
 
10403
10987
  // src/watcher.ts
10404
10988
  import { randomUUID as randomUUID2 } from "node:crypto";
10405
- import { isPretty as isPretty2, log as log30 } from "@gethmy/harness";
10989
+ import { isPretty as isPretty2, log as log32 } from "@gethmy/harness";
10406
10990
  import { createClient } from "@supabase/supabase-js";
10407
10991
 
10408
10992
  class Watcher {
@@ -10453,7 +11037,7 @@ class Watcher {
10453
11037
  }
10454
11038
  async start() {
10455
11039
  if (!isPretty2()) {
10456
- log30.info(TAG29, "Connecting to Supabase realtime (broadcast)...");
11040
+ log32.info(TAG30, "Connecting to Supabase realtime (broadcast)...");
10457
11041
  }
10458
11042
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
10459
11043
  this.subscribeBroadcast();
@@ -10466,7 +11050,7 @@ class Watcher {
10466
11050
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
10467
11051
  this.presenceChannel = presenceChannel;
10468
11052
  presenceChannel.on("presence", { event: "sync" }, () => {
10469
- log30.debug(TAG29, "Presence sync");
11053
+ log32.debug(TAG30, "Presence sync");
10470
11054
  }).subscribe(async (status) => {
10471
11055
  if (gen !== this.presenceGen)
10472
11056
  return;
@@ -10490,13 +11074,13 @@ class Watcher {
10490
11074
  if (trackStatus !== "ok") {
10491
11075
  this.presenceTracked = false;
10492
11076
  if (!this.stopping) {
10493
- log30.warn(TAG29, `Presence track returned "${trackStatus}" — scheduling reconnect`);
11077
+ log32.warn(TAG30, `Presence track returned "${trackStatus}" — scheduling reconnect`);
10494
11078
  this.schedulePresenceReconnect();
10495
11079
  }
10496
11080
  return;
10497
11081
  }
10498
11082
  if (!isPretty2() || !this.suppressStartupLogs) {
10499
- log30.info(TAG29, "Presence tracked on board-presence channel");
11083
+ log32.info(TAG30, "Presence tracked on board-presence channel");
10500
11084
  }
10501
11085
  this.presenceTracked = true;
10502
11086
  this.presenceReconnectAttempts = 0;
@@ -10504,7 +11088,7 @@ class Watcher {
10504
11088
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
10505
11089
  this.presenceTracked = false;
10506
11090
  if (!this.stopping) {
10507
- log30.warn(TAG29, `Presence subscription ${status} — scheduling reconnect`);
11091
+ log32.warn(TAG30, `Presence subscription ${status} — scheduling reconnect`);
10508
11092
  this.schedulePresenceReconnect();
10509
11093
  }
10510
11094
  }
@@ -10523,7 +11107,7 @@ class Watcher {
10523
11107
  async reconnectPresence() {
10524
11108
  if (this.stopping || !this.supabase)
10525
11109
  return;
10526
- log30.warn(TAG29, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
11110
+ log32.warn(TAG30, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
10527
11111
  if (this.presenceChannel) {
10528
11112
  const old = this.presenceChannel;
10529
11113
  this.presenceChannel = null;
@@ -10541,13 +11125,13 @@ class Watcher {
10541
11125
  return;
10542
11126
  const gen = ++this.broadcastGen;
10543
11127
  this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
10544
- log30.debug(TAG29, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
11128
+ log32.debug(TAG30, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
10545
11129
  this.onCardBroadcast({
10546
11130
  event: "card_update",
10547
11131
  payload: msg.payload ?? {}
10548
11132
  });
10549
11133
  }).on("broadcast", { event: "card_created" }, (msg) => {
10550
- log30.debug(TAG29, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
11134
+ log32.debug(TAG30, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
10551
11135
  this.onCardBroadcast({
10552
11136
  event: "card_created",
10553
11137
  payload: msg.payload ?? {}
@@ -10557,7 +11141,7 @@ class Watcher {
10557
11141
  const cardId = payload.card_id;
10558
11142
  const command = payload.command;
10559
11143
  if (cardId && command) {
10560
- log30.info(TAG29, `Broadcast: agent_command ${command} for ${cardId}`);
11144
+ log32.info(TAG30, `Broadcast: agent_command ${command} for ${cardId}`);
10561
11145
  this.onAgentCommand?.({ cardId, command });
10562
11146
  }
10563
11147
  }).subscribe((status) => {
@@ -10567,13 +11151,13 @@ class Watcher {
10567
11151
  this.connected = true;
10568
11152
  this.reconnectAttempts = 0;
10569
11153
  if (!isPretty2() || !this.suppressStartupLogs) {
10570
- log30.info(TAG29, "Broadcast subscription active");
11154
+ log32.info(TAG30, "Broadcast subscription active");
10571
11155
  }
10572
11156
  this.maybeResolveReady();
10573
11157
  } else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
10574
11158
  this.connected = false;
10575
11159
  if (!this.stopping) {
10576
- log30.warn(TAG29, `Broadcast subscription ${status} — scheduling reconnect`);
11160
+ log32.warn(TAG30, `Broadcast subscription ${status} — scheduling reconnect`);
10577
11161
  this.scheduleReconnect();
10578
11162
  }
10579
11163
  }
@@ -10592,7 +11176,7 @@ class Watcher {
10592
11176
  async reconnectBroadcast() {
10593
11177
  if (this.stopping || !this.supabase)
10594
11178
  return;
10595
- log30.warn(TAG29, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
11179
+ log32.warn(TAG30, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
10596
11180
  if (this.channel) {
10597
11181
  const old = this.channel;
10598
11182
  this.channel = null;
@@ -10629,10 +11213,10 @@ class Watcher {
10629
11213
  }
10630
11214
  this.connected = false;
10631
11215
  this.presenceTracked = false;
10632
- log30.info(TAG29, "Broadcast subscription stopped");
11216
+ log32.info(TAG30, "Broadcast subscription stopped");
10633
11217
  }
10634
11218
  }
10635
- var TAG29 = "watcher";
11219
+ var TAG30 = "watcher";
10636
11220
  var init_watcher = () => {};
10637
11221
 
10638
11222
  // src/worktree-gc.ts
@@ -10644,9 +11228,9 @@ __export(exports_worktree_gc, {
10644
11228
  WorktreeGc: () => WorktreeGc
10645
11229
  });
10646
11230
  import { execFileSync as execFileSync6 } from "node:child_process";
10647
- import { readdirSync, statSync as statSync2 } from "node:fs";
11231
+ import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
10648
11232
  import { resolve as resolve2 } from "node:path";
10649
- import { cleanupWorktree as cleanupWorktree4, log as log31 } from "@gethmy/harness";
11233
+ import { cleanupWorktree as cleanupWorktree4, log as log33 } from "@gethmy/harness";
10650
11234
  function isTransientGitNetworkError(message) {
10651
11235
  return TRANSIENT_GIT_NETWORK_ERROR.test(message);
10652
11236
  }
@@ -10675,17 +11259,58 @@ function runWorktreeGc(basePath, store, opts = {}) {
10675
11259
  return result;
10676
11260
  }
10677
11261
  const activePaths = new Set(store.getActiveRuns().map((r) => r.worktreePath).filter((p) => !!p));
11262
+ const candidates = [];
10678
11263
  for (const entry of entries) {
10679
11264
  const full = resolve2(baseAbs, entry);
11265
+ let isDirectory;
11266
+ try {
11267
+ isDirectory = statSync2(full).isDirectory();
11268
+ } catch (err) {
11269
+ result.errors.push({
11270
+ path: full,
11271
+ error: err instanceof Error ? err.message : String(err)
11272
+ });
11273
+ continue;
11274
+ }
11275
+ if (!isDirectory) {
11276
+ result.checked++;
11277
+ result.skipped.push(full);
11278
+ continue;
11279
+ }
11280
+ if (existsSync4(resolve2(full, ".git"))) {
11281
+ candidates.push(full);
11282
+ continue;
11283
+ }
11284
+ let children;
11285
+ try {
11286
+ children = readdirSync(full);
11287
+ } catch (err) {
11288
+ result.errors.push({
11289
+ path: full,
11290
+ error: err instanceof Error ? err.message : String(err)
11291
+ });
11292
+ continue;
11293
+ }
11294
+ const childDirs = children.filter((child) => {
11295
+ try {
11296
+ return statSync2(resolve2(full, child)).isDirectory();
11297
+ } catch {
11298
+ return false;
11299
+ }
11300
+ });
11301
+ if (childDirs.length === 0) {
11302
+ candidates.push(full);
11303
+ continue;
11304
+ }
11305
+ for (const child of childDirs) {
11306
+ candidates.push(resolve2(full, child));
11307
+ }
11308
+ }
11309
+ for (const full of candidates) {
10680
11310
  result.checked++;
10681
11311
  let mtimeMs;
10682
11312
  try {
10683
- const stat = statSync2(full);
10684
- if (!stat.isDirectory()) {
10685
- result.skipped.push(full);
10686
- continue;
10687
- }
10688
- mtimeMs = stat.mtimeMs;
11313
+ mtimeMs = statSync2(full).mtimeMs;
10689
11314
  } catch (err) {
10690
11315
  result.errors.push({
10691
11316
  path: full,
@@ -10718,10 +11343,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
10718
11343
  });
10719
11344
  } catch {}
10720
11345
  if (result.removed.length > 0) {
10721
- log31.info(TAG30, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
11346
+ log33.info(TAG31, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
10722
11347
  }
10723
11348
  if (result.errors.length > 0) {
10724
- log31.warn(TAG30, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
11349
+ log33.warn(TAG31, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
10725
11350
  }
10726
11351
  return result;
10727
11352
  }
@@ -10751,7 +11376,7 @@ function pruneFailedRemoteBranches(opts) {
10751
11376
  } catch (err) {
10752
11377
  const detail = gitErrorDetail2(err);
10753
11378
  if (isTransientGitNetworkError(detail)) {
10754
- log31.debug(TAG30, `Remote branch GC skipped — remote unreachable: ${detail}`);
11379
+ log33.debug(TAG31, `Remote branch GC skipped — remote unreachable: ${detail}`);
10755
11380
  return result;
10756
11381
  }
10757
11382
  result.errors.push({ ref: "fetch", error: detail });
@@ -10790,7 +11415,7 @@ function pruneFailedRemoteBranches(opts) {
10790
11415
  continue;
10791
11416
  }
10792
11417
  if (clock() > sweepDeadline) {
10793
- log31.debug(TAG30, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
11418
+ log33.debug(TAG31, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
10794
11419
  break;
10795
11420
  }
10796
11421
  try {
@@ -10803,17 +11428,17 @@ function pruneFailedRemoteBranches(opts) {
10803
11428
  } catch (err) {
10804
11429
  const detail = gitErrorDetail2(err);
10805
11430
  if (isTransientGitNetworkError(detail)) {
10806
- log31.debug(TAG30, `Remote branch GC interrupted — remote unreachable: ${detail}`);
11431
+ log33.debug(TAG31, `Remote branch GC interrupted — remote unreachable: ${detail}`);
10807
11432
  break;
10808
11433
  }
10809
11434
  result.errors.push({ ref, error: detail });
10810
11435
  }
10811
11436
  }
10812
11437
  if (result.removed.length > 0) {
10813
- log31.info(TAG30, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
11438
+ log33.info(TAG31, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
10814
11439
  }
10815
11440
  if (result.errors.length > 0) {
10816
- log31.warn(TAG30, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
11441
+ log33.warn(TAG31, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
10817
11442
  }
10818
11443
  return result;
10819
11444
  }
@@ -10844,13 +11469,13 @@ class WorktreeGc {
10844
11469
  try {
10845
11470
  runWorktreeGc(this.basePath, this.store);
10846
11471
  } catch (err) {
10847
- log31.warn(TAG30, `GC tick failed: ${err instanceof Error ? err.message : err}`);
11472
+ log33.warn(TAG31, `GC tick failed: ${err instanceof Error ? err.message : err}`);
10848
11473
  }
10849
11474
  if (this.remoteOpts) {
10850
11475
  try {
10851
11476
  pruneFailedRemoteBranches(this.remoteOpts);
10852
11477
  } catch (err) {
10853
- log31.warn(TAG30, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
11478
+ log33.warn(TAG31, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
10854
11479
  }
10855
11480
  }
10856
11481
  }
@@ -10864,7 +11489,7 @@ function getRepoRoot2() {
10864
11489
  return null;
10865
11490
  }
10866
11491
  }
10867
- var TAG30 = "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;
11492
+ var TAG31 = "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;
10868
11493
  var init_worktree_gc = __esm(() => {
10869
11494
  GIT_NETWORK_EXEC = {
10870
11495
  timeout: GIT_NETWORK_TIMEOUT_MS,
@@ -10901,7 +11526,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
10901
11526
  import { createRequire as createRequire3 } from "node:module";
10902
11527
  import {
10903
11528
  detectGitProvider as detectGitProvider6,
10904
- log as log32,
11529
+ log as log34,
10905
11530
  validateGitProviderCli
10906
11531
  } from "@gethmy/harness";
10907
11532
  async function validatePrerequisites(config, banner) {
@@ -10975,7 +11600,7 @@ async function main() {
10975
11600
  } catch (err) {
10976
11601
  if (err instanceof ConfigValidationError) {
10977
11602
  banner.fail();
10978
- log32.error(TAG31, err.message);
11603
+ log34.error(TAG32, err.message);
10979
11604
  process.exit(1);
10980
11605
  }
10981
11606
  throw err;
@@ -10985,7 +11610,7 @@ async function main() {
10985
11610
  } catch (err) {
10986
11611
  if (err instanceof ConfigValidationError) {
10987
11612
  banner.fail();
10988
- log32.error(TAG31, err.message);
11613
+ log34.error(TAG32, err.message);
10989
11614
  process.exit(1);
10990
11615
  }
10991
11616
  throw err;
@@ -11107,7 +11732,7 @@ async function main() {
11107
11732
  if (shuttingDown)
11108
11733
  return;
11109
11734
  shuttingDown = true;
11110
- log32.info(TAG31, `Received ${signal}, shutting down gracefully...`);
11735
+ log34.info(TAG32, `Received ${signal}, shutting down gracefully...`);
11111
11736
  reconciler.stop();
11112
11737
  mergeMonitor?.stop();
11113
11738
  worktreeGc.stop();
@@ -11118,18 +11743,18 @@ async function main() {
11118
11743
  }
11119
11744
  await watcher.stop();
11120
11745
  await pool.shutdown();
11121
- log32.info(TAG31, "Daemon stopped.");
11746
+ log34.info(TAG32, "Daemon stopped.");
11122
11747
  process.exit(exitCode);
11123
11748
  };
11124
11749
  process.on("SIGINT", () => shutdown("SIGINT"));
11125
11750
  process.on("SIGTERM", () => shutdown("SIGTERM"));
11126
11751
  process.on("uncaughtException", (err) => {
11127
- log32.error(TAG31, `Uncaught exception: ${err.message}`);
11752
+ log34.error(TAG32, `Uncaught exception: ${err.message}`);
11128
11753
  exitCode = 1;
11129
11754
  shutdown("uncaughtException");
11130
11755
  });
11131
11756
  process.on("unhandledRejection", (reason) => {
11132
- log32.error(TAG31, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
11757
+ log34.error(TAG32, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
11133
11758
  exitCode = 1;
11134
11759
  shutdown("unhandledRejection");
11135
11760
  });
@@ -11188,29 +11813,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
11188
11813
  if (assignedAgentId === undefined)
11189
11814
  return;
11190
11815
  if (assignedAgentId === agentId) {
11191
- log32.info(TAG31, `Broadcast: card ${cardId} assigned to agent`);
11816
+ log34.info(TAG32, `Broadcast: card ${cardId} assigned to agent`);
11192
11817
  try {
11193
11818
  await pool.resetAttemptsForReassign(cardId);
11194
11819
  await tryEnqueueCard(cardId, client, pool, config, agentId);
11195
11820
  } catch (err) {
11196
- log32.error(TAG31, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
11821
+ log34.error(TAG32, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
11197
11822
  }
11198
11823
  } else if (pool.isCardKnown(cardId)) {
11199
- log32.info(TAG31, `Broadcast: card ${cardId} unassigned from agent`);
11824
+ log34.info(TAG32, `Broadcast: card ${cardId} unassigned from agent`);
11200
11825
  await pool.removeCard(cardId);
11201
11826
  }
11202
11827
  }
11203
11828
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
11204
11829
  const { card } = await client.getCard(cardId);
11205
11830
  if (card.assigned_agent_id !== agentId) {
11206
- log32.debug(TAG31, `Card ${cardId} no longer assigned to agent — skipping`);
11831
+ log34.debug(TAG32, `Card ${cardId} no longer assigned to agent — skipping`);
11207
11832
  return;
11208
11833
  }
11209
11834
  const board = await client.getBoard(config.projectId, { summary: true });
11210
11835
  const columns = board.columns;
11211
11836
  const column = columns.find((c) => c.id === card.column_id);
11212
11837
  if (!column) {
11213
- log32.warn(TAG31, `Column not found for card ${cardId}`);
11838
+ log34.warn(TAG32, `Column not found for card ${cardId}`);
11214
11839
  return;
11215
11840
  }
11216
11841
  const route = classifyPickup(card, column.name, {
@@ -11219,31 +11844,31 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
11219
11844
  playbooks: config.agent.playbooks
11220
11845
  });
11221
11846
  if (!route) {
11222
- log32.info(TAG31, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
11847
+ log34.info(TAG32, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
11223
11848
  return;
11224
11849
  }
11225
11850
  if (route.stage) {
11226
- log32.info(TAG31, `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`);
11851
+ log34.info(TAG32, `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`);
11227
11852
  }
11228
11853
  const mode = route.mode;
11229
11854
  const labelMap = buildLabelMap(board.labels ?? []);
11230
11855
  const cardLabels = resolveCardLabels(card, labelMap);
11231
11856
  const subtasks = card.subtasks ?? [];
11232
11857
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
11233
- log32.debug(TAG31, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
11858
+ log34.debug(TAG32, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
11234
11859
  return;
11235
11860
  }
11236
11861
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
11237
- log32.debug(TAG31, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
11862
+ log34.debug(TAG32, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
11238
11863
  return;
11239
11864
  }
11240
11865
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
11241
- log32.info(TAG31, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
11866
+ log34.info(TAG32, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
11242
11867
  return;
11243
11868
  }
11244
11869
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
11245
11870
  }
11246
- var TAG31 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
11871
+ var TAG32 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
11247
11872
  var init_src = __esm(() => {
11248
11873
  init_base_branch();
11249
11874
  init_board_helpers();