@gethmy/agent 1.33.0 → 1.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +459 -220
  2. package/dist/index.js +459 -220
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -1131,14 +1131,13 @@ function isDaemonAuthoredComment(comment, identity) {
1131
1131
  return true;
1132
1132
  }
1133
1133
  // ../harmony-shared/dist/agentStaleness.js
1134
- var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS, SWEPT_SESSION_WRITE_GRACE_MS, ACTIVE_STATUSES, NOTICE_DRIVER = "notice";
1134
+ var AGENT_HEARTBEAT_LIVENESS_MS, AGENT_MILESTONE_LIVENESS_MS, AGENT_SWEEP_DAEMON_MS, AGENT_SWEEP_INTERACTIVE_MS, AGENT_SWEEP_PAUSED_MS, ACTIVE_STATUSES, NOTICE_DRIVER = "notice";
1135
1135
  var init_agentStaleness = __esm(() => {
1136
1136
  AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
1137
1137
  AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
1138
1138
  AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
1139
1139
  AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
1140
1140
  AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
1141
- SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
1142
1141
  ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
1143
1142
  });
1144
1143
  // ../harmony-shared/dist/branchRef.js
@@ -4718,6 +4717,9 @@ function formatBudgetComment(i) {
4718
4717
  if (i.lastAction)
4719
4718
  lines.push(`Last action: ${i.lastAction}.`);
4720
4719
  }
4720
+ if (i.hasCommits === false) {
4721
+ lines.push("No commits yet — nothing on the branch past where this run started.");
4722
+ }
4721
4723
  lines.push(i.branchName ? `The work is parked, not lost: branch \`${i.branchName}\`, worktree kept.` : "The work is parked, not lost.");
4722
4724
  lines.push(i.trigger === "account_limit" ? "Raise the limit, then run `harmony-agent resume` — that releases every card parked on it, this one included." : i.trigger === "max_attempts" ? `Continue to start a fresh attempt, or stop and I'll hand the card back.` : `Continue to pick up from where I stopped with a fresh turn budget, or stop and I'll hand the card back.`);
4723
4725
  const cap = i.cardTurnCap;
@@ -5323,7 +5325,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5323
5325
  } else {
5324
5326
  await endRunSession({ client, tag: TAG15 }, card, { status: "completed" }, buildTokenPayload(sessionStats), "throw");
5325
5327
  }
5326
- await teardownWorktree(client, card.id, worktreePath, branchName);
5328
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5327
5329
  return true;
5328
5330
  }
5329
5331
  log16.warn(TAG15, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
@@ -5340,7 +5342,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5340
5342
  failureSummary,
5341
5343
  ...buildTokenPayload(sessionStats)
5342
5344
  });
5343
- await teardownWorktree(client, card.id, worktreePath, branchName);
5345
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5344
5346
  return false;
5345
5347
  }
5346
5348
  log16.info(TAG15, `Pushing branch ${branchName} (pre-verify)...`);
@@ -5435,7 +5437,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5435
5437
  recoveryBranch: branchName,
5436
5438
  ...buildTokenPayload(sessionStats)
5437
5439
  });
5438
- await teardownWorktree(client, card.id, worktreePath, branchName);
5440
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5439
5441
  return false;
5440
5442
  }
5441
5443
  log16.info(TAG15, `Verification passed for #${card.short_id}`);
@@ -5482,7 +5484,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5482
5484
  agentSessionId: agentSessionId ?? null
5483
5485
  });
5484
5486
  }
5485
- await teardownWorktree(client, card.id, worktreePath, branchName);
5487
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5486
5488
  log16.info(TAG15, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
5487
5489
  return true;
5488
5490
  }
@@ -5548,7 +5550,7 @@ function commitUncommittedChanges(worktreePath, card) {
5548
5550
  return false;
5549
5551
  }
5550
5552
  }
5551
- function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args, cwd) => execFileSync5("git", [...GIT_NO_HOOKS6, ...args], {
5553
+ function measureRunCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args, cwd) => execFileSync5("git", [...GIT_NO_HOOKS6, ...args], {
5552
5554
  cwd,
5553
5555
  encoding: "utf-8"
5554
5556
  })) {
@@ -5556,16 +5558,22 @@ function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args,
5556
5558
  try {
5557
5559
  gitImpl(["merge-base", "--is-ancestor", baselineSha, "HEAD"], worktreePath);
5558
5560
  } catch {
5559
- return false;
5561
+ return "unknown";
5560
5562
  }
5561
5563
  }
5562
5564
  const range = baselineSha ? `${baselineSha}..HEAD` : `origin/${baseBranch}..HEAD`;
5565
+ let count;
5563
5566
  try {
5564
- const count = gitImpl(["rev-list", "--count", range], worktreePath).trim();
5565
- return parseInt(count, 10) > 0;
5567
+ count = parseInt(gitImpl(["rev-list", "--count", range], worktreePath).trim(), 10);
5566
5568
  } catch {
5567
- return false;
5569
+ return "unknown";
5568
5570
  }
5571
+ if (!Number.isFinite(count))
5572
+ return "unknown";
5573
+ return count > 0 ? "committed" : "none";
5574
+ }
5575
+ function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl) {
5576
+ return measureRunCommits(worktreePath, baseBranch, baselineSha, gitImpl) === "committed";
5569
5577
  }
5570
5578
  function stripDaemonBlocks(description) {
5571
5579
  const indices = [SUMMARY_MARKER, BRANCH_PROVENANCE_MARKER].map((marker) => description.indexOf(marker)).filter((index) => index >= 0);
@@ -6464,9 +6472,12 @@ function stripReviewSummary(description) {
6464
6472
  return description;
6465
6473
  return description.slice(0, idx).trimEnd();
6466
6474
  }
6467
- async function postReviewComment(client, card, commentType, body) {
6475
+ async function postReviewComment(client, card, commentType, body, agentSessionId) {
6468
6476
  try {
6469
- await client.addComment(card.id, body, { commentType });
6477
+ await client.addComment(card.id, body, {
6478
+ commentType,
6479
+ agentSessionId: agentSessionId ?? undefined
6480
+ });
6470
6481
  } catch (err) {
6471
6482
  log19.error(TAG18, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6472
6483
  }
@@ -6532,7 +6543,7 @@ ${runLogTail}
6532
6543
  ].filter(Boolean).join(`
6533
6544
 
6534
6545
  `);
6535
- await postReviewComment(client, card, "blocker", body);
6546
+ await postReviewComment(client, card, "blocker", body, agentSessionId);
6536
6547
  }
6537
6548
  await client.endAgentSession(card.id, {
6538
6549
  status: "paused",
@@ -6611,7 +6622,7 @@ ${runLogTail}
6611
6622
  ].filter(Boolean).join(`
6612
6623
 
6613
6624
  `);
6614
- await postReviewComment(client, card, "decision", body);
6625
+ await postReviewComment(client, card, "decision", body, agentSessionId);
6615
6626
  }
6616
6627
  await client.endAgentSession(card.id, {
6617
6628
  status: "completed",
@@ -6639,7 +6650,7 @@ ${runLogTail}
6639
6650
  ].filter(Boolean).join(`
6640
6651
 
6641
6652
  `);
6642
- await postReviewComment(client, card, "blocker", body);
6653
+ await postReviewComment(client, card, "blocker", body, agentSessionId);
6643
6654
  await client.endAgentSession(card.id, {
6644
6655
  status: "completed",
6645
6656
  ...buildTokenPayload(sessionStats)
@@ -6675,7 +6686,7 @@ ${runLogTail}
6675
6686
  }));
6676
6687
  if (linkedFindings.length > 0) {
6677
6688
  for (const body2 of buildFindingComments(linkedFindings)) {
6678
- await postReviewComment(client, card, "finding", body2);
6689
+ await postReviewComment(client, card, "finding", body2, agentSessionId);
6679
6690
  }
6680
6691
  }
6681
6692
  await Promise.all((handbackVerdict.proceed ? minorFindings : []).map(async (finding) => {
@@ -6704,7 +6715,7 @@ ${runLogTail}
6704
6715
  ].filter(Boolean).join(`
6705
6716
 
6706
6717
  `);
6707
- await postReviewComment(client, card, "summary", body);
6718
+ await postReviewComment(client, card, "summary", body, agentSessionId);
6708
6719
  }
6709
6720
  if (handbackVerdict.proceed && config.planning.enabled && card.plan_id) {
6710
6721
  try {
@@ -7727,19 +7738,20 @@ import {
7727
7738
  buildGateCollectorRegistry,
7728
7739
  cleanupWorktree as cleanupWorktree4,
7729
7740
  collectGateEvidence,
7730
- containedEnv as containedEnv2,
7731
7741
  DevServerReadinessError,
7742
+ devServerLaunch,
7732
7743
  formatDiffSummary,
7733
7744
  GIT_NO_HOOKS as GIT_NO_HOOKS7,
7734
7745
  implementRunContainmentCliArgs,
7735
7746
  log as log24,
7736
7747
  probeDevServer,
7748
+ removeSandboxContainer,
7737
7749
  resolveStageGate,
7738
7750
  secretEnvKeysToStrip,
7739
7751
  signalGroup,
7740
7752
  spawnInGroup as spawnInGroup2,
7741
- spawnRunArgs,
7742
7753
  terminateGroup,
7754
+ verificationSandbox as verificationSandbox2,
7743
7755
  waitForDevServer
7744
7756
  } from "@gethmy/harness";
7745
7757
 
@@ -7758,6 +7770,7 @@ class ReviewWorker {
7758
7770
  startedAt = null;
7759
7771
  process = null;
7760
7772
  devServerProcess = null;
7773
+ devServerContainer = null;
7761
7774
  timeoutTimer = null;
7762
7775
  heartbeatTimer = null;
7763
7776
  progressTracker = null;
@@ -7868,7 +7881,10 @@ class ReviewWorker {
7868
7881
  if (crossedTurnCap(before, after, cap)) {
7869
7882
  log24.warn(TAG22, `#${this.stateStore.getRun(this.runId ?? "")?.cardShortId ?? cardId} reached its per-card turn cap on a review leg — ${after} of ${cap} turns. The implement pipeline will not pick it up again until it is reassigned or the cap is raised.`);
7870
7883
  const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
7871
- await this.client.addComment(cardId, buildTurnCapComment(after, cap, legs), { commentType: "blocker" });
7884
+ await this.client.addComment(cardId, buildTurnCapComment(after, cap, legs), {
7885
+ commentType: "blocker",
7886
+ agentSessionId: this.sessionId ?? undefined
7887
+ });
7872
7888
  }
7873
7889
  } catch {}
7874
7890
  }
@@ -7979,11 +7995,19 @@ class ReviewWorker {
7979
7995
  const port = this.reviewPort;
7980
7996
  const cwd = this.worktreePath;
7981
7997
  log24.info(this.tag, `Starting dev server on port ${port}...`);
7982
- const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
7983
- this.devServerProcess = spawnInGroup2(devCmd, devArgs, {
7998
+ const launch = devServerLaunch({
7999
+ worktreePath: cwd,
8000
+ port,
8001
+ sandbox: verificationSandbox2(this.config)
8002
+ });
8003
+ this.devServerContainer = launch.containerName ?? null;
8004
+ if (launch.containerName) {
8005
+ await removeSandboxContainer(launch.containerName);
8006
+ }
8007
+ this.devServerProcess = spawnInGroup2(launch.cmd, launch.args, {
7984
8008
  cwd,
7985
8009
  stdio: ["ignore", "pipe", "pipe"],
7986
- env: containedEnv2()
8010
+ stripEnvKeys: secretEnvKeysToStrip()
7987
8011
  });
7988
8012
  let devServerSpawnError = null;
7989
8013
  this.devServerProcess.once("error", (err) => {
@@ -7997,7 +8021,7 @@ class ReviewWorker {
7997
8021
  progressPercent: 10
7998
8022
  });
7999
8023
  if (devServerSpawnError) {
8000
- throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
8024
+ throw new DevServerReadinessError(`dev server failed to start (${launch.cmd}): ${devServerSpawnError.message}`);
8001
8025
  }
8002
8026
  await waitForDevServer(this.devServerProcess, 30000);
8003
8027
  await probeDevServer(port);
@@ -8257,7 +8281,8 @@ ${userPrompt}`;
8257
8281
  let commentId = null;
8258
8282
  try {
8259
8283
  const res = await this.client.addComment(card.id, body, {
8260
- commentType: "blocker"
8284
+ commentType: "blocker",
8285
+ agentSessionId: this.sessionId ?? undefined
8261
8286
  });
8262
8287
  commentId = res?.comment?.id ?? null;
8263
8288
  } catch (err) {
@@ -8382,7 +8407,8 @@ ${userPrompt}`;
8382
8407
  ...implementRunContainmentCliArgs({
8383
8408
  worktree: this.worktreePath,
8384
8409
  readOnly: true,
8385
- extraDisallowedTools: reviewDenylist ? reviewDenylist.split(",").map((t) => t.trim()).filter(Boolean) : undefined
8410
+ extraDisallowedTools: reviewDenylist ? reviewDenylist.split(",").map((t) => t.trim()).filter(Boolean) : undefined,
8411
+ run: this.cardId && this.sessionId ? { cardId: this.cardId, agentSessionId: this.sessionId } : undefined
8386
8412
  }),
8387
8413
  "--",
8388
8414
  prompt
@@ -8500,6 +8526,11 @@ ${userPrompt}`;
8500
8526
  this.devServerProcess = null;
8501
8527
  log24.debug(this.tag, "Killed dev server group");
8502
8528
  }
8529
+ if (this.devServerContainer) {
8530
+ const name = this.devServerContainer;
8531
+ this.devServerContainer = null;
8532
+ removeSandboxContainer(name);
8533
+ }
8503
8534
  }
8504
8535
  cleanup() {
8505
8536
  if (this.timeoutTimer) {
@@ -9105,7 +9136,10 @@ async function dispatchWave(parent, stage, plan, items, deps) {
9105
9136
  created += 1;
9106
9137
  }
9107
9138
  if (created > 0 && plan.truncated > 0) {
9108
- 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(() => {});
9139
+ 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.`, {
9140
+ commentType: "finding",
9141
+ agentSessionId: deps.agentSessionId ?? undefined
9142
+ }).catch(() => {});
9109
9143
  }
9110
9144
  return created;
9111
9145
  }
@@ -9188,14 +9222,17 @@ function runMotorStage(args, deps) {
9188
9222
  args.repoPath,
9189
9223
  "--session",
9190
9224
  args.sessionId,
9191
- ...args.metricsPath ? ["--metrics", args.metricsPath] : []
9225
+ ...args.metricsPath ? ["--metrics", args.metricsPath] : [],
9226
+ ...args.sandboxImage?.trim() ? ["--sandbox-image", args.sandboxImage.trim()] : []
9192
9227
  ];
9193
9228
  return new Promise((resolve2) => {
9194
9229
  const child = spawnFn("node", argv, {
9195
9230
  env: {
9196
9231
  ...process.env,
9197
9232
  HARMONY_API_URL: motorApiBase(deps.env.apiUrl),
9198
- HARMONY_API_KEY: deps.env.apiKey
9233
+ HARMONY_API_KEY: deps.env.apiKey,
9234
+ HARMONY_AGENT_CARD_ID: args.cardId,
9235
+ HARMONY_AGENT_SESSION_ID: args.sessionId
9199
9236
  },
9200
9237
  stdio: ["ignore", "pipe", "pipe"]
9201
9238
  });
@@ -9320,6 +9357,26 @@ function runMotorStage(args, deps) {
9320
9357
  var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
9321
9358
  var init_motor_driver = () => {};
9322
9359
 
9360
+ // src/no-commit-notice.ts
9361
+ function noCommitNoticeThreshold(turnBudget, fraction = NO_COMMIT_NOTICE_FRACTION) {
9362
+ if (!Number.isFinite(turnBudget) || turnBudget <= 0)
9363
+ return null;
9364
+ if (!Number.isFinite(fraction) || fraction <= 0)
9365
+ return null;
9366
+ return Math.ceil(turnBudget * fraction);
9367
+ }
9368
+ function shouldCheckForCommits(gate) {
9369
+ if (gate.settled)
9370
+ return false;
9371
+ if (!gate.expectsCommits)
9372
+ return false;
9373
+ const threshold = noCommitNoticeThreshold(gate.turnBudget, gate.fraction);
9374
+ if (threshold === null)
9375
+ return false;
9376
+ return gate.toolCalls >= threshold;
9377
+ }
9378
+ var NO_COMMIT_NOTICE_FRACTION = 0.75;
9379
+
9323
9380
  // src/stage-advance.ts
9324
9381
  import { gateConfigErrorReason, log as log29 } from "@gethmy/harness";
9325
9382
  function endDispositionFor(outcome) {
@@ -9390,7 +9447,7 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
9390
9447
  currentTask: reason
9391
9448
  });
9392
9449
  } catch {}
9393
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9450
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { agentSessionId: deps.agentSessionId });
9394
9451
  log29.info(TAG27, `#${card.short_id} GateMisconfigured: ${reason}`);
9395
9452
  return { kind: "held_misconfigured", reason, endDisposition };
9396
9453
  }
@@ -9461,7 +9518,12 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
9461
9518
  currentTask: reason
9462
9519
  });
9463
9520
  } catch {}
9464
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9521
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9522
+ keepAttempts: true,
9523
+ endStatus: "blocked",
9524
+ blockers: [reason],
9525
+ agentSessionId: deps.agentSessionId
9526
+ });
9465
9527
  log29.info(TAG27, `#${card.short_id} LoopExhausted: ${reason}`);
9466
9528
  return { kind: "held_gate_unmet", reason, endDisposition };
9467
9529
  }
@@ -9477,7 +9539,10 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
9477
9539
  await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
9478
9540
  const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
9479
9541
  try {
9480
- await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
9542
+ await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, {
9543
+ commentType: "progress",
9544
+ agentSessionId: deps.agentSessionId ?? undefined
9545
+ });
9481
9546
  } catch {}
9482
9547
  await runTransition(deps.client, guard.card, {
9483
9548
  move: { columnName: toColumn },
@@ -9501,7 +9566,10 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
9501
9566
  decisions: [],
9502
9567
  nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
9503
9568
  });
9504
- await deps.client.addComment(card.id, body, { commentType: "decision" });
9569
+ await deps.client.addComment(card.id, body, {
9570
+ commentType: "decision",
9571
+ agentSessionId: deps.agentSessionId ?? undefined
9572
+ });
9505
9573
  } catch (err) {
9506
9574
  log29.warn(TAG27, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9507
9575
  }
@@ -9536,13 +9604,13 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9536
9604
  }
9537
9605
  if (next.kind === "out_of_range") {
9538
9606
  const reason = `Stage advancement aborted: stage index ${stageIndex} is out of range for the pinned playbook version — holding for a human.`;
9539
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9607
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { agentSessionId: deps.agentSessionId });
9540
9608
  return { kind: "held_misconfigured", reason, endDisposition };
9541
9609
  }
9542
9610
  const toColumn = await resolveStageColumnName(deps.client, card, next.stage);
9543
9611
  if (!toColumn) {
9544
9612
  const reason = `Stage "${stage.name}" passed but the next stage "${next.stage.name}" has no resolvable board column — holding for a human (never moving to an undefined column).`;
9545
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore);
9613
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { agentSessionId: deps.agentSessionId });
9546
9614
  return { kind: "held_misconfigured", reason, endDisposition };
9547
9615
  }
9548
9616
  await persistStagePointer(deps.client, card, {
@@ -9566,7 +9634,12 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9566
9634
  log29.info(TAG27, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
9567
9635
  if (next.stage.owner === "human") {
9568
9636
  const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
9569
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9637
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9638
+ keepAttempts: true,
9639
+ endStatus: "blocked",
9640
+ blockers: [reason],
9641
+ agentSessionId: deps.agentSessionId
9642
+ });
9570
9643
  return {
9571
9644
  kind: "advanced",
9572
9645
  toStageId: next.stage.id,
@@ -9589,7 +9662,12 @@ async function handleGateUnmet(card, stage, summary, deps) {
9589
9662
  currentTask: reason
9590
9663
  });
9591
9664
  } catch {}
9592
- const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, { keepAttempts: true, endStatus: "blocked", blockers: [reason] });
9665
+ const endDisposition = await holdForHuman(deps.client, card, reason, deps.runId, deps.stateStore, {
9666
+ keepAttempts: true,
9667
+ endStatus: "blocked",
9668
+ blockers: [reason],
9669
+ agentSessionId: deps.agentSessionId
9670
+ });
9593
9671
  log29.info(TAG27, `#${card.short_id} GateUnmetExhausted: ${reason}`);
9594
9672
  return { kind: "held_gate_unmet", reason, endDisposition };
9595
9673
  }
@@ -9603,7 +9681,10 @@ async function handleGateUnmet(card, stage, summary, deps) {
9603
9681
  }
9604
9682
  const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
9605
9683
  try {
9606
- await deps.client.addComment(card.id, `Stage gate unmet — re-running "${stage.name}". ${summary}.`, { commentType: "progress" });
9684
+ await deps.client.addComment(card.id, `Stage gate unmet — re-running "${stage.name}". ${summary}.`, {
9685
+ commentType: "progress",
9686
+ agentSessionId: deps.agentSessionId ?? undefined
9687
+ });
9607
9688
  } catch {}
9608
9689
  await runTransition(deps.client, guard.card, {
9609
9690
  move: { columnName: toColumn },
@@ -9641,7 +9722,10 @@ async function guardStageReclaim(card, what, deps) {
9641
9722
  }
9642
9723
  log29.info(TAG27, `#${card.short_id} ${what} refused — ${detail} (${reason})${released ? "; released the daemon's claim so the stage router stops here" : ""}`);
9643
9724
  try {
9644
- await deps.client.addComment(card.id, `Stopped the ${what} — ${detail}. The card is not the daemon's to reclaim, so this stage will not re-run until it is assigned back.`, { commentType: "decision" });
9725
+ await deps.client.addComment(card.id, `Stopped the ${what} — ${detail}. The card is not the daemon's to reclaim, so this stage will not re-run until it is assigned back.`, {
9726
+ commentType: "decision",
9727
+ agentSessionId: deps.agentSessionId ?? undefined
9728
+ });
9645
9729
  } catch {}
9646
9730
  return { proceed: false, reason, released };
9647
9731
  }
@@ -9650,7 +9734,10 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
9650
9734
  await stateStore.decrementAttempt(card.id).catch(() => {});
9651
9735
  }
9652
9736
  try {
9653
- await client.addComment(card.id, reason, { commentType: "blocker" });
9737
+ await client.addComment(card.id, reason, {
9738
+ commentType: "blocker",
9739
+ agentSessionId: opts.agentSessionId ?? undefined
9740
+ });
9654
9741
  } catch {}
9655
9742
  try {
9656
9743
  await runTransition(client, card, { removeLabels: [AGENT_LABEL] }, { store: stateStore, runId });
@@ -9701,7 +9788,7 @@ import {
9701
9788
  spawnInGroup as spawnInGroup4,
9702
9789
  teardownWorktree as teardownWorktree2,
9703
9790
  terminateGroup as terminateGroup3,
9704
- verificationSandbox as verificationSandbox2,
9791
+ verificationSandbox as verificationSandbox3,
9705
9792
  WorktreeBaseError
9706
9793
  } from "@gethmy/harness";
9707
9794
  function sdkDraftLogLine(ev) {
@@ -9818,6 +9905,9 @@ class Worker {
9818
9905
  runTurns = 0;
9819
9906
  chargedCents = 0;
9820
9907
  chargedTurns = 0;
9908
+ runToolCalls = 0;
9909
+ noCommitNoticeSettled = false;
9910
+ runExpectsCommits = true;
9821
9911
  runLegIndex = 0;
9822
9912
  lastRunText = "";
9823
9913
  constructor(id, config, client, identity, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
@@ -9921,7 +10011,7 @@ class Worker {
9921
10011
  log30.warn(TAG28, `#${shortId ?? cardId} reached its per-card turn cap — ${after} of ${cap} turns. It will not be picked up again until it is reassigned or the cap is raised.`);
9922
10012
  try {
9923
10013
  const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
9924
- await this.client.addComment(cardId, buildTurnCapComment(after, cap, legs), { commentType: "blocker" });
10014
+ await this.client.addComment(cardId, buildTurnCapComment(after, cap, legs), { commentType: "blocker", agentSessionId: this.sessionId ?? undefined });
9925
10015
  } catch (err) {
9926
10016
  log30.warn(TAG28, `Could not post the turn-cap notice for ${cardId}: ${err instanceof Error ? err.message : err}`);
9927
10017
  }
@@ -9939,6 +10029,9 @@ class Worker {
9939
10029
  this.runTurns = 0;
9940
10030
  this.chargedCents = 0;
9941
10031
  this.chargedTurns = 0;
10032
+ this.runToolCalls = 0;
10033
+ this.noCommitNoticeSettled = false;
10034
+ this.runExpectsCommits = true;
9942
10035
  this.runLegIndex = 0;
9943
10036
  this.lastRunText = "";
9944
10037
  this.cliSessionId = null;
@@ -10149,10 +10242,12 @@ class Worker {
10149
10242
  this.cancel("timeout");
10150
10243
  }, this.config.maxTimeout);
10151
10244
  this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
10245
+ this.runExpectsCommits = stageCtx.kind === "run" ? stageRunExpectsCommit(stageCtx.stage, stageCtx.allowedTools) : true;
10152
10246
  await this.spawnClaude(prompt, card, subtasks, {
10153
10247
  model: implementModel,
10154
10248
  maxTurns: this.grantedTurns ?? undefined,
10155
10249
  resumeSessionId: this.cliSessionId ?? undefined,
10250
+ noCommitNotice: true,
10156
10251
  ...this.activeRunSpawnOpts ?? {}
10157
10252
  });
10158
10253
  if (this.aborted)
@@ -10185,7 +10280,7 @@ class Worker {
10185
10280
  const onBeforeWorktreeCleanup = stageRun ? async (worktreePath) => {
10186
10281
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
10187
10282
  } : undefined;
10188
- const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.identity.agentId, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns, stageRun ? stageRunExpectsCommit(stageRun.stage, stageRun.allowedTools) : true);
10283
+ const completed = await runCompletion(this.client, card, this.branchName, this.worktreePath, this.config, this.id, this.sessionIdentifier, this.identity.agentId, this.lastSessionStats, this.workspaceId, this.sessionId, this.stateStore, this.onCardCompleted, onBeforeWorktreeCleanup, this.runBaselineSha, this.effectiveMaxTurns, this.runExpectsCommits);
10189
10284
  if (completed === "park") {
10190
10285
  await this.parkForDecision(card, "max_turns");
10191
10286
  return;
@@ -10262,7 +10357,7 @@ class Worker {
10262
10357
  }
10263
10358
  if (this.worktreePath) {
10264
10359
  try {
10265
- await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
10360
+ await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined, this.sessionId);
10266
10361
  } catch {
10267
10362
  log30.warn(this.tag, "Failed to cleanup worktree before requeue");
10268
10363
  }
@@ -10323,7 +10418,7 @@ class Worker {
10323
10418
  } else if (this.runId && this.timedOut) {
10324
10419
  if (this.worktreePath) {
10325
10420
  try {
10326
- await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined);
10421
+ await teardownWorktree2(this.client, card.id, this.worktreePath, this.branchName ?? undefined, this.sessionId);
10327
10422
  } catch {
10328
10423
  log30.warn(this.tag, "Failed to cleanup worktree before requeue");
10329
10424
  }
@@ -10571,7 +10666,8 @@ class Worker {
10571
10666
  agentId: this.identity.agentId
10572
10667
  },
10573
10668
  sink: this.cliRunner,
10574
- isGivenUp: (cardId) => (this.stateStore.getCard(cardId)?.attempts ?? 0) >= this.config.budget.maxAttemptsPerCard
10669
+ isGivenUp: (cardId) => (this.stateStore.getCard(cardId)?.attempts ?? 0) >= this.config.budget.maxAttemptsPerCard,
10670
+ agentSessionId: this.sessionId
10575
10671
  });
10576
10672
  } catch (err) {
10577
10673
  const detail = err instanceof Error ? err.message : String(err);
@@ -10635,7 +10731,10 @@ class Worker {
10635
10731
  log30.info(this.tag, `Holding #${card.short_id}: ${reason}`);
10636
10732
  await this.stateStore.decrementAttempt(card.id);
10637
10733
  try {
10638
- await this.client.addComment(card.id, reason, { commentType: "blocker" });
10734
+ await this.client.addComment(card.id, reason, {
10735
+ commentType: "blocker",
10736
+ agentSessionId: this.sessionId ?? undefined
10737
+ });
10639
10738
  } catch {}
10640
10739
  try {
10641
10740
  await runTransition(this.client, card, {
@@ -10701,9 +10800,16 @@ class Worker {
10701
10800
  const waitHours = this.config.budget.pause.waitHours;
10702
10801
  const until = computeDecisionDeadline(waitHours);
10703
10802
  log30.warn(this.tag, `#${card.short_id} parked (${trigger}) — awaiting a human decision for ${waitHours}h`);
10803
+ const measured = this.worktreePath && this.runExpectsCommits ? measureRunCommits(this.worktreePath, this.config.worktree.baseBranch, this.runBaselineSha) : "unknown";
10804
+ const hasCommits = measured === "unknown" ? undefined : measured === "committed";
10805
+ if (hasCommits === false && !this.noCommitNoticeSettled) {
10806
+ this.noCommitNoticeSettled = true;
10807
+ this.recordNoCommitsYet("park", stats?.cost?.numTurns ?? 0);
10808
+ }
10704
10809
  const body = formatBudgetComment({
10705
10810
  trigger,
10706
10811
  numTurns: stats?.cost?.numTurns ?? 0,
10812
+ hasCommits,
10707
10813
  maxTurns: this.effectiveMaxTurns,
10708
10814
  toolCalls: stats?.toolCalls ?? 0,
10709
10815
  durationMs: stats?.cost?.durationMs ?? 0,
@@ -10717,7 +10823,8 @@ class Worker {
10717
10823
  let commentId = null;
10718
10824
  try {
10719
10825
  const res = await this.client.addComment(card.id, body, {
10720
- commentType: "blocker"
10826
+ commentType: "blocker",
10827
+ agentSessionId: this.sessionId ?? undefined
10721
10828
  });
10722
10829
  commentId = res?.comment?.id ?? null;
10723
10830
  } catch (err) {
@@ -10826,7 +10933,8 @@ class Worker {
10826
10933
  workspaceId: this.workspaceId,
10827
10934
  repoPath: worktreePath,
10828
10935
  sessionId,
10829
- metricsPath
10936
+ metricsPath,
10937
+ sandboxImage: this.config.verification.sandboxImage
10830
10938
  }, {
10831
10939
  env: {
10832
10940
  apiUrl: this.client.getApiUrl(),
@@ -10925,7 +11033,7 @@ class Worker {
10925
11033
  if (!worktreePath)
10926
11034
  return;
10927
11035
  try {
10928
- await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined);
11036
+ await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined, this.sessionId);
10929
11037
  } catch {
10930
11038
  log30.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
10931
11039
  }
@@ -11000,7 +11108,10 @@ ${prompt}`;
11000
11108
  decisions: [],
11001
11109
  nextStageNeeds: "Pick up from the produced artifact above; treat the recorded decisions as settled."
11002
11110
  });
11003
- await this.client.addComment(card.id, body, { commentType: "decision" });
11111
+ await this.client.addComment(card.id, body, {
11112
+ commentType: "decision",
11113
+ agentSessionId: this.sessionId ?? undefined
11114
+ });
11004
11115
  log30.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
11005
11116
  } catch (err) {
11006
11117
  log30.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
@@ -11027,7 +11138,7 @@ ${prompt}`;
11027
11138
  worktreePath,
11028
11139
  buildTimeout: this.config.verification.timeout,
11029
11140
  lintTimeout: this.config.verification.timeout,
11030
- sandbox: verificationSandbox2(this.config)
11141
+ sandbox: verificationSandbox3(this.config)
11031
11142
  },
11032
11143
  artifact: {
11033
11144
  worktreePath,
@@ -11067,7 +11178,8 @@ ${prompt}`;
11067
11178
  maxAttempts: this.config.budget.maxAttemptsPerCard,
11068
11179
  fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
11069
11180
  sink: this.cliRunner,
11070
- runId: this.runId ?? undefined
11181
+ runId: this.runId ?? undefined,
11182
+ agentSessionId: this.sessionId
11071
11183
  });
11072
11184
  } catch (err) {
11073
11185
  log30.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
@@ -11129,6 +11241,44 @@ ${prompt}`;
11129
11241
  }
11130
11242
  });
11131
11243
  }
11244
+ countToolCallForNoCommitNotice() {
11245
+ this.runToolCalls++;
11246
+ if (!shouldCheckForCommits({
11247
+ toolCalls: this.runToolCalls,
11248
+ turnBudget: this.effectiveMaxTurns,
11249
+ expectsCommits: this.runExpectsCommits,
11250
+ settled: this.noCommitNoticeSettled
11251
+ })) {
11252
+ return;
11253
+ }
11254
+ this.noCommitNoticeSettled = true;
11255
+ if (!this.worktreePath)
11256
+ return;
11257
+ const measured = measureRunCommits(this.worktreePath, this.config.worktree.baseBranch, this.runBaselineSha);
11258
+ if (measured !== "none") {
11259
+ if (measured === "unknown") {
11260
+ log30.debug(this.tag, `no-commit check could not attribute this run's commits — staying quiet on ${this.branchName ?? "this branch"}`);
11261
+ }
11262
+ return;
11263
+ }
11264
+ this.recordNoCommitsYet("turn_budget");
11265
+ }
11266
+ recordNoCommitsYet(trigger, numTurns) {
11267
+ const turnBudget = this.effectiveMaxTurns;
11268
+ log30.warn(this.tag, `No commits yet on ${this.branchName ?? "this run's branch"} — ${this.runToolCalls} tool calls into a ${turnBudget}-turn budget (${trigger})`);
11269
+ this.cliRunner?.record({
11270
+ kind: "no_commits_yet",
11271
+ source: "system",
11272
+ payload: {
11273
+ trigger,
11274
+ toolCalls: this.runToolCalls,
11275
+ turnBudget,
11276
+ thresholdFraction: NO_COMMIT_NOTICE_FRACTION,
11277
+ elapsedMs: this.startedAt ? Date.now() - this.startedAt : 0,
11278
+ ...numTurns !== undefined ? { numTurns } : {}
11279
+ }
11280
+ });
11281
+ }
11132
11282
  recordRunSized() {
11133
11283
  if (!this.modelChoice)
11134
11284
  return;
@@ -11161,7 +11311,8 @@ ${prompt}`;
11161
11311
  try {
11162
11312
  const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3), this.config.budget.pause.enabled);
11163
11313
  const res = await this.client.addComment(cardId, body, {
11164
- commentType: "blocker"
11314
+ commentType: "blocker",
11315
+ agentSessionId: this.sessionId ?? undefined
11165
11316
  });
11166
11317
  giveUpCommentId = res?.comment?.id ?? null;
11167
11318
  log30.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
@@ -11476,6 +11627,7 @@ ${prompt}`;
11476
11627
  model: this.selectImplementModel(card),
11477
11628
  maxTurns: STEERING_MAX_TURNS,
11478
11629
  resumeSessionId: this.cliSessionId,
11630
+ noCommitNotice: true,
11479
11631
  ...this.activeRunSpawnOpts ?? {}
11480
11632
  });
11481
11633
  } catch (err) {
@@ -11508,7 +11660,8 @@ ${prompt}`;
11508
11660
  ...this.config.claude.additionalArgs,
11509
11661
  ...implementRunContainmentCliArgs2({
11510
11662
  worktree: this.worktreePath,
11511
- extraDisallowedTools: opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined
11663
+ extraDisallowedTools: opts.disallowedTools ? opts.disallowedTools.split(",").map((t) => t.trim()).filter(Boolean) : undefined,
11664
+ run: this.sessionId ? { cardId: card.id, agentSessionId: this.sessionId } : undefined
11512
11665
  }),
11513
11666
  "--",
11514
11667
  prompt
@@ -11547,6 +11700,11 @@ ${prompt}`;
11547
11700
  this.lastRunText += content;
11548
11701
  this.captureCliSessionId(parser.sessionId);
11549
11702
  });
11703
+ if (opts.noCommitNotice) {
11704
+ parser.on("tool_start", () => {
11705
+ this.countToolCallForNoCommitNotice();
11706
+ });
11707
+ }
11550
11708
  parser.on("parse_error", (msg) => {
11551
11709
  log30.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
11552
11710
  runLog?.stream.write(`
@@ -11636,7 +11794,8 @@ ${prompt}`;
11636
11794
  maxBudgetUsd: sdkCfg?.maxBudgetUsd,
11637
11795
  ...implementRunContainment({
11638
11796
  worktree: this.worktreePath,
11639
- extraDisallowedTools: disallowedTools
11797
+ extraDisallowedTools: disallowedTools,
11798
+ run: this.sessionId ? { cardId: card.id, agentSessionId: this.sessionId } : undefined
11640
11799
  }),
11641
11800
  onSpawn: (child) => {
11642
11801
  this.process = child;
@@ -11657,6 +11816,9 @@ ${prompt}`;
11657
11816
  try {
11658
11817
  for await (const ev of stream) {
11659
11818
  this.progressTracker?.ingest(ev);
11819
+ if (opts.noCommitNotice && ev.kind === "tool_started") {
11820
+ this.countToolCallForNoCommitNotice();
11821
+ }
11660
11822
  if (ev.kind === "assistant_text") {
11661
11823
  this.lastRunText += `${ev.payload.text}
11662
11824
  `;
@@ -11740,7 +11902,7 @@ ${prompt}`;
11740
11902
  }
11741
11903
  if (this.worktreePath && this.state !== "parked" && (this.state === "error" || this.timedOut || this.aborted)) {
11742
11904
  try {
11743
- await teardownWorktree2(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined);
11905
+ await teardownWorktree2(this.client, this.cardId, this.worktreePath, this.branchName ?? undefined, this.sessionId);
11744
11906
  } catch {
11745
11907
  log30.warn(this.tag, "Failed to cleanup worktree");
11746
11908
  }
@@ -12256,7 +12418,10 @@ class Pool {
12256
12418
  try {
12257
12419
  await this.client.addComment(run.cardId, `**That Continue cannot be granted.** ${turns.detail}, which is this card's ceiling (\`budget.maxTurnsPerCard\`), so the implement pipeline would refuse it again the moment it was dispatched.
12258
12420
 
12259
- I am handing the card back rather than holding it parked. Reassign it — that clears the count and starts it fresh — or raise the cap in the daemon config if this card genuinely needs the room. The branch is pushed, so nothing committed is lost.`, { commentType: "blocker" });
12421
+ I am handing the card back rather than holding it parked. Reassign it — that clears the count and starts it fresh — or raise the cap in the daemon config if this card genuinely needs the room. The branch is pushed, so nothing committed is lost.`, {
12422
+ commentType: "blocker",
12423
+ agentSessionId: run.sessionId ?? undefined
12424
+ });
12260
12425
  } catch (err) {
12261
12426
  log31.warn(TAG29, `Could not explain the refused Continue on ${run.cardId}: ${err instanceof Error ? err.message : err}`);
12262
12427
  }
@@ -13243,8 +13408,139 @@ var init_sweep = __esm(() => {
13243
13408
  init_unblock();
13244
13409
  });
13245
13410
 
13411
+ // src/sweep-guard.ts
13412
+ import { log as log37 } from "@gethmy/harness";
13413
+
13414
+ class SweepGuard {
13415
+ config;
13416
+ store;
13417
+ checkDailyBudget;
13418
+ constructor(config, store, checkDailyBudget) {
13419
+ this.config = config;
13420
+ this.store = store;
13421
+ this.checkDailyBudget = checkDailyBudget;
13422
+ }
13423
+ check() {
13424
+ const sweep = this.store.getSweep();
13425
+ if (sweep.haltReason) {
13426
+ return {
13427
+ claiming: false,
13428
+ reason: sweep.haltReason,
13429
+ detail: this.describeStop(sweep.haltReason),
13430
+ latched: true
13431
+ };
13432
+ }
13433
+ const cap = this.config.maxCardsPerSweep;
13434
+ if (cap >= 0 && sweep.claimed >= cap) {
13435
+ return {
13436
+ claiming: false,
13437
+ reason: "card_cap",
13438
+ detail: this.describeStop("card_cap"),
13439
+ latched: true
13440
+ };
13441
+ }
13442
+ const daily = this.checkDailyBudget?.();
13443
+ if (daily && !daily.allow) {
13444
+ return {
13445
+ claiming: false,
13446
+ latched: false,
13447
+ reason: "daily_budget",
13448
+ detail: this.describeStop("daily_budget", daily.detail)
13449
+ };
13450
+ }
13451
+ return { claiming: true };
13452
+ }
13453
+ async recordClaim(cardId) {
13454
+ await this.store.recordSweepClaim(cardId);
13455
+ const verdict = this.check();
13456
+ if (!verdict.claiming && verdict.latched)
13457
+ await this.halt(verdict.reason);
13458
+ return verdict;
13459
+ }
13460
+ lastClaimedCardId() {
13461
+ return this.store.getSweep().lastClaimedCardId ?? null;
13462
+ }
13463
+ async halt(reason) {
13464
+ const already = this.store.getSweep().haltReason === reason;
13465
+ await this.store.haltSweep(reason);
13466
+ if (!already) {
13467
+ const claimed = this.store.getSweep().claimed;
13468
+ log37.info(TAG35, `claiming stopped (${reason}) after ${claimed} claimed`);
13469
+ }
13470
+ return this.snapshot();
13471
+ }
13472
+ async resume() {
13473
+ const before = this.store.getSweep();
13474
+ const was = before.haltReason ? this.describeStop(before.haltReason) : null;
13475
+ await this.store.resumeSweep();
13476
+ if (was)
13477
+ log37.info(TAG35, `claiming resumed by the operator — was stopped: ${was}`);
13478
+ return this.snapshot();
13479
+ }
13480
+ snapshot() {
13481
+ const sweep = this.store.getSweep();
13482
+ const cap = this.config.maxCardsPerSweep;
13483
+ const verdict = this.check();
13484
+ return {
13485
+ enabled: this.config.enabled,
13486
+ claiming: verdict.claiming,
13487
+ haltReason: verdict.claiming ? null : verdict.reason,
13488
+ haltedAt: sweep.haltedAt,
13489
+ claimed: sweep.claimed,
13490
+ maxCardsPerSweep: cap < 0 ? null : cap,
13491
+ totalClaimed: sweep.totalClaimed,
13492
+ detail: verdict.claiming ? null : verdict.detail
13493
+ };
13494
+ }
13495
+ describeStop(reason, budgetDetail) {
13496
+ const claimed = this.store.getSweep().claimed;
13497
+ const cards = `${claimed} card${claimed === 1 ? "" : "s"}`;
13498
+ const resume = "Resume with `harmony-agent sweep resume`.";
13499
+ switch (reason) {
13500
+ case "card_cap":
13501
+ return `card cap reached — ${cards} claimed this sweep, cap is ${this.config.maxCardsPerSweep}. ${resume}`;
13502
+ case "operator":
13503
+ return `stopped by the operator after ${cards}. ${resume}`;
13504
+ case "daily_budget":
13505
+ return `daily spend cap reached${budgetDetail ? ` (${budgetDetail})` : ""} after ${cards} — claiming starts again on its own when the UTC day rolls over.`;
13506
+ }
13507
+ }
13508
+ }
13509
+ function toSweepPresence(snapshot) {
13510
+ if (!snapshot?.enabled)
13511
+ return null;
13512
+ return {
13513
+ claiming: snapshot.claiming,
13514
+ reason: snapshot.haltReason,
13515
+ detail: snapshot.detail,
13516
+ claimed: snapshot.claimed,
13517
+ cap: snapshot.maxCardsPerSweep,
13518
+ haltedAt: snapshot.haltedAt
13519
+ };
13520
+ }
13521
+ function sameSweepPresence(a, b) {
13522
+ if (a === b)
13523
+ return true;
13524
+ if (!a || !b)
13525
+ return false;
13526
+ return a.claiming === b.claiming && a.reason === b.reason && a.detail === b.detail && a.claimed === b.claimed && a.cap === b.cap && a.haltedAt === b.haltedAt;
13527
+ }
13528
+ function describeCaps(sweep, dailyBudgetCents) {
13529
+ const cards = sweep.maxCardsPerSweep < 0 ? "no cap" : String(sweep.maxCardsPerSweep);
13530
+ return `cards/sweep ${cards} · spend/day ${formatDailyCap(dailyBudgetCents)}`;
13531
+ }
13532
+ function sweepBannerLine(config) {
13533
+ const sweep = config.sweep;
13534
+ const scope = sweep.requireLabel ? `cards labelled "${sweep.requireLabel}"` : "unassigned cards";
13535
+ const extra = sweep.trustedAuthors.length;
13536
+ const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
13537
+ return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
13538
+ }
13539
+ var TAG35 = "sweep";
13540
+ var init_sweep_guard = () => {};
13541
+
13246
13542
  // src/reconcile.ts
13247
- import { detectGitProvider as detectGitProvider5, log as log37 } from "@gethmy/harness";
13543
+ import { detectGitProvider as detectGitProvider5, log as log38 } from "@gethmy/harness";
13248
13544
  function daemonProcessLabels(approvedLabel) {
13249
13545
  const names = ["agent", "agent-recovered", NEED_REVIEW_LABEL, approvedLabel];
13250
13546
  return new Set(names.filter(Boolean).map((n) => n.toLowerCase()));
@@ -13274,6 +13570,7 @@ class Reconciler {
13274
13570
  gitProvider = null;
13275
13571
  repoDirectories = null;
13276
13572
  lastSweepStopReported = null;
13573
+ publishSweep = () => {};
13277
13574
  get lastTick() {
13278
13575
  return this.lastTickAt;
13279
13576
  }
@@ -13303,7 +13600,7 @@ class Reconciler {
13303
13600
  clearInterval(this.timer);
13304
13601
  this.timer = null;
13305
13602
  }
13306
- log37.info(TAG35, "Heartbeat stopped");
13603
+ log38.info(TAG36, "Heartbeat stopped");
13307
13604
  }
13308
13605
  async recoverStaleRuns() {
13309
13606
  if (!this.stateStore || !this.agentConfig)
@@ -13314,7 +13611,7 @@ class Reconciler {
13314
13611
  const pool = this.pool;
13315
13612
  for (const run of active) {
13316
13613
  if (isBudgetHeldRun(run)) {
13317
- log37.info(TAG35, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
13614
+ log38.info(TAG36, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
13318
13615
  continue;
13319
13616
  }
13320
13617
  const foreignDaemon = run.daemonPid !== process.pid;
@@ -13324,7 +13621,7 @@ class Reconciler {
13324
13621
  if (!daemonDead && !(heartbeatStale && ourZombie))
13325
13622
  continue;
13326
13623
  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`;
13327
- log37.warn(TAG35, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
13624
+ log38.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
13328
13625
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
13329
13626
  runId: run.runId,
13330
13627
  cardId: run.cardId,
@@ -13351,11 +13648,11 @@ class Reconciler {
13351
13648
  const stalledAt = Date.parse(card.updated_at ?? "");
13352
13649
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
13353
13650
  continue;
13354
- log37.warn(TAG35, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
13651
+ log38.warn(TAG36, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
13355
13652
  try {
13356
13653
  await this.client.moveCard(card.id, pickupCol.id);
13357
13654
  } catch (err) {
13358
- log37.error(TAG35, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
13655
+ log38.error(TAG36, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
13359
13656
  }
13360
13657
  }
13361
13658
  }
@@ -13387,7 +13684,7 @@ class Reconciler {
13387
13684
  return;
13388
13685
  const cardLabels = resolveCardLabels(card, labelMap);
13389
13686
  const subtasks = card.subtasks ?? [];
13390
- log37.info(TAG35, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
13687
+ log38.info(TAG36, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
13391
13688
  await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
13392
13689
  }
13393
13690
  });
@@ -13411,11 +13708,11 @@ class Reconciler {
13411
13708
  const parkedAt = Date.parse(card.updated_at ?? "");
13412
13709
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
13413
13710
  continue;
13414
- log37.warn(TAG35, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
13711
+ log38.warn(TAG36, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
13415
13712
  try {
13416
13713
  await this.client.moveCard(card.id, pickupCol.id);
13417
13714
  } catch (err) {
13418
- log37.error(TAG35, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
13715
+ log38.error(TAG36, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
13419
13716
  }
13420
13717
  }
13421
13718
  }
@@ -13443,13 +13740,14 @@ class Reconciler {
13443
13740
  now: Date.now()
13444
13741
  };
13445
13742
  const verdict = this.sweepGuard?.check() ?? { claiming: true };
13743
+ this.publishSweepState();
13446
13744
  if (!verdict.claiming) {
13447
13745
  this.reportSweepStopped(verdict.reason, verdict.detail);
13448
13746
  return;
13449
13747
  }
13450
13748
  this.lastSweepStopReported = null;
13451
13749
  if (!this.pool.hasFreeImplementSlot()) {
13452
- log37.debug(TAG35, "sweep skipped — no free implement slot");
13750
+ log38.debug(TAG36, "sweep skipped — no free implement slot");
13453
13751
  return;
13454
13752
  }
13455
13753
  const { claimed } = await sweepForCard({
@@ -13462,25 +13760,32 @@ class Reconciler {
13462
13760
  if (!claimed)
13463
13761
  return;
13464
13762
  await this.sweepGuard?.recordClaim(claimed.card.id);
13763
+ this.publishSweepState();
13465
13764
  await this.pool.enqueue(claimed.card, claimed.column, claimed.labels, claimed.card.subtasks ?? [], "implement");
13466
13765
  }
13467
13766
  reportSweepStopped(reason, detail) {
13468
13767
  const line = `sweep stopped claiming — ${detail}`;
13469
13768
  if (this.lastSweepStopReported === reason)
13470
- log37.debug(TAG35, line);
13769
+ log38.debug(TAG36, line);
13471
13770
  else
13472
- log37.warn(TAG35, line);
13771
+ log38.warn(TAG36, line);
13473
13772
  this.lastSweepStopReported = reason;
13474
13773
  }
13774
+ setSweepPresenceSink(sink) {
13775
+ this.publishSweep = sink;
13776
+ }
13777
+ publishSweepState() {
13778
+ this.publishSweep(toSweepPresence(this.sweepGuard?.snapshot()));
13779
+ }
13475
13780
  resolveRepoDirectories() {
13476
13781
  if (this.repoDirectories)
13477
13782
  return this.repoDirectories;
13478
13783
  const dirs = readRepoDirectories(process.cwd());
13479
13784
  this.repoDirectories = dirs;
13480
13785
  if (dirs.size === 0) {
13481
- log37.warn(TAG35, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
13786
+ log38.warn(TAG36, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
13482
13787
  } else {
13483
- log37.debug(TAG35, `Path signal allows ${dirs.size} top-level directories`);
13788
+ log38.debug(TAG36, `Path signal allows ${dirs.size} top-level directories`);
13484
13789
  }
13485
13790
  return dirs;
13486
13791
  }
@@ -13524,21 +13829,21 @@ class Reconciler {
13524
13829
  const subtasks = card.subtasks ?? [];
13525
13830
  const mode = route.mode;
13526
13831
  if (route.stage) {
13527
- log37.info(TAG35, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
13832
+ log38.info(TAG36, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
13528
13833
  }
13529
13834
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
13530
- log37.debug(TAG35, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
13835
+ log38.debug(TAG36, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
13531
13836
  continue;
13532
13837
  }
13533
13838
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
13534
- log37.debug(TAG35, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
13839
+ log38.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
13535
13840
  continue;
13536
13841
  }
13537
13842
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
13538
- log37.debug(TAG35, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
13843
+ log38.debug(TAG36, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
13539
13844
  continue;
13540
13845
  }
13541
- log37.info(TAG35, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
13846
+ log38.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
13542
13847
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
13543
13848
  }
13544
13849
  }
@@ -13548,13 +13853,13 @@ class Reconciler {
13548
13853
  try {
13549
13854
  await this.pool.drainBudgetDecisions();
13550
13855
  } catch (err) {
13551
- log37.error(TAG35, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
13856
+ log38.error(TAG36, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
13552
13857
  }
13553
13858
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
13554
13859
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
13555
13860
  for (const knownId of knownCardIds) {
13556
13861
  if (!allAgentCardIds.has(knownId)) {
13557
- log37.info(TAG35, `Missed unassign: ${knownId} — removing`);
13862
+ log38.info(TAG36, `Missed unassign: ${knownId} — removing`);
13558
13863
  await this.pool.removeCard(knownId);
13559
13864
  }
13560
13865
  }
@@ -13562,15 +13867,15 @@ class Reconciler {
13562
13867
  try {
13563
13868
  await this.sweepForWork(cards, columns, labelMap, pickupConfig);
13564
13869
  } catch (err) {
13565
- log37.error(TAG35, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
13870
+ log38.error(TAG36, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
13566
13871
  }
13567
- log37.debug(TAG35, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
13872
+ log38.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
13568
13873
  } catch (err) {
13569
- log37.error(TAG35, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
13874
+ log38.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
13570
13875
  }
13571
13876
  }
13572
13877
  }
13573
- var TAG35 = "reconcile";
13878
+ var TAG36 = "reconcile";
13574
13879
  var init_reconcile = __esm(() => {
13575
13880
  init_board_helpers();
13576
13881
  init_recovery();
@@ -13579,6 +13884,7 @@ var init_reconcile = __esm(() => {
13579
13884
  init_state_store();
13580
13885
  init_strand_recovery();
13581
13886
  init_sweep();
13887
+ init_sweep_guard();
13582
13888
  init_types2();
13583
13889
  });
13584
13890
 
@@ -13587,7 +13893,7 @@ var exports_startup_banner = {};
13587
13893
  __export(exports_startup_banner, {
13588
13894
  createStartupBanner: () => createStartupBanner
13589
13895
  });
13590
- import { isPretty, log as log38 } from "@gethmy/harness";
13896
+ import { isPretty, log as log39 } from "@gethmy/harness";
13591
13897
  function createStartupBanner(config, version) {
13592
13898
  return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
13593
13899
  }
@@ -13612,7 +13918,7 @@ function prettyBanner(config, version) {
13612
13918
  checks.push({ kind: "ok", message });
13613
13919
  },
13614
13920
  warn(message) {
13615
- log38.warn(TAG36, message);
13921
+ log39.warn(TAG37, message);
13616
13922
  checks.push({ kind: "warn", message: message.split(`
13617
13923
  `, 1)[0] });
13618
13924
  },
@@ -13637,25 +13943,25 @@ function prettyBanner(config, version) {
13637
13943
  };
13638
13944
  }
13639
13945
  function jsonBanner(config, version) {
13640
- log38.info(TAG36, `Harmony Agent Daemon v${version} starting...`);
13641
- log38.info(TAG36, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
13946
+ log39.info(TAG37, `Harmony Agent Daemon v${version} starting...`);
13947
+ log39.info(TAG37, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
13642
13948
  if (config.agent.review.enabled) {
13643
- log38.info(TAG36, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
13949
+ log39.info(TAG37, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
13644
13950
  }
13645
13951
  let failed = false;
13646
13952
  return {
13647
13953
  setProjectName(_name) {},
13648
13954
  setGitProvider(provider) {
13649
- log38.info(TAG36, `Git provider: ${provider}`);
13955
+ log39.info(TAG37, `Git provider: ${provider}`);
13650
13956
  },
13651
13957
  setHttpPort(port) {
13652
- log38.info(TAG36, `HTTP server on port ${port}`);
13958
+ log39.info(TAG37, `HTTP server on port ${port}`);
13653
13959
  },
13654
13960
  check(message) {
13655
- log38.info(TAG36, message);
13961
+ log39.info(TAG37, message);
13656
13962
  },
13657
13963
  warn(message) {
13658
- log38.warn(TAG36, message);
13964
+ log39.warn(TAG37, message);
13659
13965
  },
13660
13966
  fail() {
13661
13967
  failed = true;
@@ -13663,7 +13969,7 @@ function jsonBanner(config, version) {
13663
13969
  async ready(message) {
13664
13970
  if (failed)
13665
13971
  return;
13666
- log38.info(TAG36, message);
13972
+ log39.info(TAG37, message);
13667
13973
  }
13668
13974
  };
13669
13975
  }
@@ -13744,7 +14050,7 @@ function cyan(s) {
13744
14050
  function yellow(s) {
13745
14051
  return `${ANSI.yellow}${s}${ANSI.reset}`;
13746
14052
  }
13747
- var TAG36 = "daemon", RULE_WIDTH = 70, ANSI;
14053
+ var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
13748
14054
  var init_startup_banner = __esm(() => {
13749
14055
  ANSI = {
13750
14056
  reset: "\x1B[0m",
@@ -13845,133 +14151,27 @@ var init_stream_parser_selftest = __esm(() => {
13845
14151
  init_stream_parser();
13846
14152
  });
13847
14153
 
13848
- // src/sweep-guard.ts
13849
- import { log as log39 } from "@gethmy/harness";
13850
-
13851
- class SweepGuard {
13852
- config;
13853
- store;
13854
- checkDailyBudget;
13855
- constructor(config, store, checkDailyBudget) {
13856
- this.config = config;
13857
- this.store = store;
13858
- this.checkDailyBudget = checkDailyBudget;
13859
- }
13860
- check() {
13861
- const sweep = this.store.getSweep();
13862
- if (sweep.haltReason) {
13863
- return {
13864
- claiming: false,
13865
- reason: sweep.haltReason,
13866
- detail: this.describeStop(sweep.haltReason),
13867
- latched: true
13868
- };
13869
- }
13870
- const cap = this.config.maxCardsPerSweep;
13871
- if (cap >= 0 && sweep.claimed >= cap) {
13872
- return {
13873
- claiming: false,
13874
- reason: "card_cap",
13875
- detail: this.describeStop("card_cap"),
13876
- latched: true
13877
- };
13878
- }
13879
- const daily = this.checkDailyBudget?.();
13880
- if (daily && !daily.allow) {
13881
- return {
13882
- claiming: false,
13883
- latched: false,
13884
- reason: "daily_budget",
13885
- detail: this.describeStop("daily_budget", daily.detail)
13886
- };
13887
- }
13888
- return { claiming: true };
13889
- }
13890
- async recordClaim(cardId) {
13891
- await this.store.recordSweepClaim(cardId);
13892
- const verdict = this.check();
13893
- if (!verdict.claiming && verdict.latched)
13894
- await this.halt(verdict.reason);
13895
- return verdict;
13896
- }
13897
- lastClaimedCardId() {
13898
- return this.store.getSweep().lastClaimedCardId ?? null;
13899
- }
13900
- async halt(reason) {
13901
- const already = this.store.getSweep().haltReason === reason;
13902
- await this.store.haltSweep(reason);
13903
- if (!already) {
13904
- const claimed = this.store.getSweep().claimed;
13905
- log39.info(TAG37, `claiming stopped (${reason}) after ${claimed} claimed`);
13906
- }
13907
- return this.snapshot();
13908
- }
13909
- async resume() {
13910
- const before = this.store.getSweep();
13911
- const was = before.haltReason ? this.describeStop(before.haltReason) : null;
13912
- await this.store.resumeSweep();
13913
- if (was)
13914
- log39.info(TAG37, `claiming resumed by the operator — was stopped: ${was}`);
13915
- return this.snapshot();
13916
- }
13917
- snapshot() {
13918
- const sweep = this.store.getSweep();
13919
- const cap = this.config.maxCardsPerSweep;
13920
- const verdict = this.check();
13921
- return {
13922
- enabled: this.config.enabled,
13923
- claiming: verdict.claiming,
13924
- haltReason: verdict.claiming ? null : verdict.reason,
13925
- haltedAt: sweep.haltedAt,
13926
- claimed: sweep.claimed,
13927
- maxCardsPerSweep: cap < 0 ? null : cap,
13928
- totalClaimed: sweep.totalClaimed,
13929
- detail: verdict.claiming ? null : verdict.detail
13930
- };
13931
- }
13932
- describeStop(reason, budgetDetail) {
13933
- const claimed = this.store.getSweep().claimed;
13934
- const cards = `${claimed} card${claimed === 1 ? "" : "s"}`;
13935
- const resume = "Resume with `harmony-agent sweep resume`.";
13936
- switch (reason) {
13937
- case "card_cap":
13938
- return `card cap reached — ${cards} claimed this sweep, cap is ${this.config.maxCardsPerSweep}. ${resume}`;
13939
- case "operator":
13940
- return `stopped by the operator after ${cards}. ${resume}`;
13941
- case "daily_budget":
13942
- return `daily spend cap reached${budgetDetail ? ` (${budgetDetail})` : ""} after ${cards} — claiming starts again on its own when the UTC day rolls over.`;
13943
- }
13944
- }
13945
- }
13946
- function describeCaps(sweep, dailyBudgetCents) {
13947
- const cards = sweep.maxCardsPerSweep < 0 ? "no cap" : String(sweep.maxCardsPerSweep);
13948
- return `cards/sweep ${cards} · spend/day ${formatDailyCap(dailyBudgetCents)}`;
13949
- }
13950
- function sweepBannerLine(config) {
13951
- const sweep = config.sweep;
13952
- const scope = sweep.requireLabel ? `cards labelled "${sweep.requireLabel}"` : "unassigned cards";
13953
- const extra = sweep.trustedAuthors.length;
13954
- const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
13955
- return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
13956
- }
13957
- var TAG37 = "sweep";
13958
- var init_sweep_guard = () => {};
13959
-
13960
14154
  // src/watcher.ts
13961
14155
  import { randomUUID as randomUUID3 } from "node:crypto";
13962
14156
  import { isPretty as isPretty2, log as log40 } from "@gethmy/harness";
13963
14157
  import { createClient } from "@supabase/supabase-js";
14158
+ function toPresenceColumns(names) {
14159
+ return names.map((n) => n.trim()).filter((n) => n.length > 0).slice(0, MAX_PRESENCE_COLUMNS).map((n) => n.slice(0, MAX_PRESENCE_COLUMN_LENGTH));
14160
+ }
13964
14161
 
13965
14162
  class Watcher {
13966
14163
  credentials;
13967
14164
  projectId;
13968
14165
  identity;
14166
+ routing;
13969
14167
  onCardBroadcast;
13970
14168
  onAgentCommand;
13971
14169
  channel = null;
13972
14170
  presenceChannel = null;
13973
14171
  supabase = null;
13974
14172
  daemonId = randomUUID3();
14173
+ startedAt = new Date().toISOString();
14174
+ sweep = null;
13975
14175
  connected = false;
13976
14176
  presenceTracked = false;
13977
14177
  suppressStartupLogs = true;
@@ -14016,10 +14216,11 @@ class Watcher {
14016
14216
  log40.warn(TAG38, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
14017
14217
  return false;
14018
14218
  }
14019
- constructor(credentials, projectId, identity, onCardBroadcast, onAgentCommand) {
14219
+ constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand) {
14020
14220
  this.credentials = credentials;
14021
14221
  this.projectId = projectId;
14022
14222
  this.identity = identity;
14223
+ this.routing = routing;
14023
14224
  this.onCardBroadcast = onCardBroadcast;
14024
14225
  this.onAgentCommand = onAgentCommand;
14025
14226
  }
@@ -14031,6 +14232,42 @@ class Watcher {
14031
14232
  this.subscribeBroadcast();
14032
14233
  this.subscribePresence();
14033
14234
  }
14235
+ presencePayload() {
14236
+ const implement = toPresenceColumns(this.routing.pickupColumns);
14237
+ const review = toPresenceColumns(this.routing.reviewColumns);
14238
+ return {
14239
+ daemonId: this.daemonId,
14240
+ startedAt: this.startedAt,
14241
+ userId: this.identity.userId,
14242
+ agentId: this.identity.agentId,
14243
+ userEmail: this.identity.userEmail,
14244
+ agentIdentifier: this.identity.agentIdentifier,
14245
+ agentName: this.identity.agentName,
14246
+ ...this.sweep ? { sweep: this.sweep } : {},
14247
+ ...implement.length ? { pickup: { implement, review } } : {}
14248
+ };
14249
+ }
14250
+ publishSweep(state) {
14251
+ if (sameSweepPresence(this.sweep, state))
14252
+ return;
14253
+ this.sweep = state;
14254
+ if (!this.presenceTracked || this.stopping)
14255
+ return;
14256
+ const channel = this.presenceChannel;
14257
+ if (!channel)
14258
+ return;
14259
+ (async () => {
14260
+ let status;
14261
+ try {
14262
+ status = await channel.track(this.presencePayload());
14263
+ } catch (err) {
14264
+ status = `threw (${String(err)})`;
14265
+ }
14266
+ if (status !== "ok") {
14267
+ log40.debug(TAG38, `Presence sweep update returned "${status}"`);
14268
+ }
14269
+ })();
14270
+ }
14034
14271
  subscribePresence() {
14035
14272
  if (!this.supabase)
14036
14273
  return;
@@ -14045,15 +14282,7 @@ class Watcher {
14045
14282
  if (status === "SUBSCRIBED") {
14046
14283
  let trackStatus;
14047
14284
  try {
14048
- trackStatus = await presenceChannel.track({
14049
- daemonId: this.daemonId,
14050
- startedAt: new Date().toISOString(),
14051
- userId: this.identity.userId,
14052
- agentId: this.identity.agentId,
14053
- userEmail: this.identity.userEmail,
14054
- agentIdentifier: this.identity.agentIdentifier,
14055
- agentName: this.identity.agentName
14056
- });
14285
+ trackStatus = await presenceChannel.track(this.presencePayload());
14057
14286
  } catch (err) {
14058
14287
  trackStatus = `error (${String(err)})`;
14059
14288
  }
@@ -14214,8 +14443,10 @@ class Watcher {
14214
14443
  log40.info(TAG38, "Broadcast subscription stopped");
14215
14444
  }
14216
14445
  }
14217
- var TAG38 = "watcher";
14218
- var init_watcher = () => {};
14446
+ var TAG38 = "watcher", MAX_PRESENCE_COLUMNS = 24, MAX_PRESENCE_COLUMN_LENGTH = 120;
14447
+ var init_watcher = __esm(() => {
14448
+ init_sweep_guard();
14449
+ });
14219
14450
 
14220
14451
  // src/worktree-gc.ts
14221
14452
  var exports_worktree_gc = {};
@@ -14755,7 +14986,11 @@ async function main() {
14755
14986
  };
14756
14987
  },
14757
14988
  handleCommand: (cmd, cardId) => pool.handleAgentCommand(cardId, cmd),
14758
- handleSweepCommand: (cmd) => cmd === "stop" ? sweepGuard.halt("operator") : sweepGuard.resume(),
14989
+ handleSweepCommand: async (cmd) => {
14990
+ const snapshot = cmd === "stop" ? await sweepGuard.halt("operator") : await sweepGuard.resume();
14991
+ reconciler.publishSweepState();
14992
+ return snapshot;
14993
+ },
14759
14994
  getSweep: () => sweepGuard.snapshot(),
14760
14995
  handleAccountResume: () => accountGuard.resume(),
14761
14996
  getAccount: () => accountGuard.snapshot()
@@ -14766,6 +15001,9 @@ async function main() {
14766
15001
  userEmail: config.userEmail,
14767
15002
  agentIdentifier: config.agentIdentifier,
14768
15003
  agentName: config.agentName
15004
+ }, {
15005
+ pickupColumns: config.agent.pickupColumns,
15006
+ reviewColumns
14769
15007
  }, async (event) => {
14770
15008
  await handleBroadcast(event, client, pool, config, agentId);
14771
15009
  }, async (command) => {
@@ -14805,6 +15043,7 @@ async function main() {
14805
15043
  shutdown("unhandledRejection");
14806
15044
  });
14807
15045
  await watcher.start();
15046
+ reconciler.setSweepPresenceSink((state) => watcher.publishSweep(state));
14808
15047
  reconciler.start();
14809
15048
  mergeMonitor?.start();
14810
15049
  worktreeGc.start();