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