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