@gethmy/agent 1.32.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 +496 -227
  2. package/dist/index.js +496 -227
  3. package/package.json +3 -3
package/dist/cli.js CHANGED
@@ -841,6 +841,31 @@ function findRemovedConfigKeys(rawConfig) {
841
841
  }
842
842
  return found;
843
843
  }
844
+ function sandboxConfigIssues(args) {
845
+ const out = [];
846
+ if (args.image && !/^[A-Za-z0-9]/.test(args.image)) {
847
+ out.push(`${args.imagePath}: must start with a letter or digit (got "${args.image}") — a leading "-" is read by docker as a flag, not an image`);
848
+ }
849
+ if (args.timeoutPath !== undefined) {
850
+ const ms = args.timeoutMs;
851
+ if (typeof ms !== "number" || !Number.isInteger(ms) || ms < 1) {
852
+ out.push(`${args.timeoutPath}: must be an integer >= 1 (got ${ms})`);
853
+ }
854
+ }
855
+ return out;
856
+ }
857
+ function validateVerificationConfig(config) {
858
+ const v = config.verification;
859
+ const issues = sandboxConfigIssues({
860
+ image: v.sandboxImage,
861
+ imagePath: "verification.sandboxImage"
862
+ });
863
+ if (issues.length > 0) {
864
+ throw new ConfigValidationError(`Invalid verification config:
865
+ - ${issues.join(`
866
+ - `)}`, issues);
867
+ }
868
+ }
844
869
  function validateAutoMergeConfig(config) {
845
870
  const autoMerge = config.review.autoMerge;
846
871
  const issues = [];
@@ -869,18 +894,18 @@ function validateAutoMergeConfig(config) {
869
894
  if (!autoMerge.reReviewOnBranchChange) {
870
895
  issues.push("review.autoMerge.ciRepair.patch.enabled: needs review.autoMerge.reReviewOnBranchChange — without it a machine-authored repair could merge with nothing having reviewed its diff");
871
896
  }
872
- if (patch.sandboxImage && !/^[A-Za-z0-9]/.test(patch.sandboxImage)) {
873
- issues.push(`review.autoMerge.ciRepair.patch.sandboxImage: must start with a letter or digit (got "${patch.sandboxImage}") — a leading "-" is read by docker as a flag, not an image`);
874
- }
897
+ issues.push(...sandboxConfigIssues({
898
+ image: patch.sandboxImage,
899
+ timeoutMs: patch.sandboxTimeoutMs,
900
+ imagePath: "review.autoMerge.ciRepair.patch.sandboxImage",
901
+ timeoutPath: "review.autoMerge.ciRepair.patch.sandboxTimeoutMs"
902
+ }));
875
903
  if (!Number.isInteger(patch.maxTurns) || patch.maxTurns < 1) {
876
904
  issues.push(`review.autoMerge.ciRepair.patch.maxTurns: must be an integer >= 1 (got ${patch.maxTurns})`);
877
905
  }
878
906
  if (!(patch.maxBudgetUsd > 0)) {
879
907
  issues.push(`review.autoMerge.ciRepair.patch.maxBudgetUsd: must be greater than 0 (got ${patch.maxBudgetUsd})`);
880
908
  }
881
- if (!Number.isInteger(patch.sandboxTimeoutMs) || patch.sandboxTimeoutMs < 1) {
882
- issues.push(`review.autoMerge.ciRepair.patch.sandboxTimeoutMs: must be an integer >= 1 (got ${patch.sandboxTimeoutMs})`);
883
- }
884
909
  }
885
910
  const independent = autoMerge.independentReview;
886
911
  if (typeof independent?.enabled !== "boolean") {
@@ -1106,14 +1131,13 @@ function isDaemonAuthoredComment(comment, identity) {
1106
1131
  return true;
1107
1132
  }
1108
1133
  // ../harmony-shared/dist/agentStaleness.js
1109
- 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";
1110
1135
  var init_agentStaleness = __esm(() => {
1111
1136
  AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
1112
1137
  AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
1113
1138
  AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
1114
1139
  AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
1115
1140
  AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
1116
- SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
1117
1141
  ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
1118
1142
  });
1119
1143
  // ../harmony-shared/dist/branchRef.js
@@ -2750,6 +2774,7 @@ var init_types2 = __esm(() => {
2750
2774
  devServerBasePort: 4200,
2751
2775
  timeout: 120000,
2752
2776
  testTimeout: 600000,
2777
+ sandboxImage: "",
2753
2778
  failColumn: "To Do"
2754
2779
  },
2755
2780
  review: {
@@ -4692,6 +4717,9 @@ function formatBudgetComment(i) {
4692
4717
  if (i.lastAction)
4693
4718
  lines.push(`Last action: ${i.lastAction}.`);
4694
4719
  }
4720
+ if (i.hasCommits === false) {
4721
+ lines.push("No commits yet — nothing on the branch past where this run started.");
4722
+ }
4695
4723
  lines.push(i.branchName ? `The work is parked, not lost: branch \`${i.branchName}\`, worktree kept.` : "The work is parked, not lost.");
4696
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.`);
4697
4725
  const cap = i.cardTurnCap;
@@ -5233,7 +5261,8 @@ import {
5233
5261
  reportFindings,
5234
5262
  runFormatFix,
5235
5263
  runVerification,
5236
- teardownWorktree
5264
+ teardownWorktree,
5265
+ verificationSandbox
5237
5266
  } from "@gethmy/harness";
5238
5267
  function formatTokenCount(tokens) {
5239
5268
  if (tokens >= 1e6)
@@ -5275,7 +5304,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5275
5304
  revertWarnings: []
5276
5305
  };
5277
5306
  if (config.verification.enabled && config.verification.lint) {
5278
- runFormatFix(worktreePath, config.verification.timeout, workerId);
5307
+ await runFormatFix(worktreePath, config.verification.timeout, workerId, verificationSandbox(config));
5279
5308
  }
5280
5309
  commitUncommittedChanges(worktreePath, card);
5281
5310
  const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch, runBaselineSha);
@@ -5296,7 +5325,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5296
5325
  } else {
5297
5326
  await endRunSession({ client, tag: TAG15 }, card, { status: "completed" }, buildTokenPayload(sessionStats), "throw");
5298
5327
  }
5299
- await teardownWorktree(client, card.id, worktreePath, branchName);
5328
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5300
5329
  return true;
5301
5330
  }
5302
5331
  log16.warn(TAG15, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
@@ -5313,7 +5342,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5313
5342
  failureSummary,
5314
5343
  ...buildTokenPayload(sessionStats)
5315
5344
  });
5316
- await teardownWorktree(client, card.id, worktreePath, branchName);
5345
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5317
5346
  return false;
5318
5347
  }
5319
5348
  log16.info(TAG15, `Pushing branch ${branchName} (pre-verify)...`);
@@ -5408,7 +5437,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5408
5437
  recoveryBranch: branchName,
5409
5438
  ...buildTokenPayload(sessionStats)
5410
5439
  });
5411
- await teardownWorktree(client, card.id, worktreePath, branchName);
5440
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5412
5441
  return false;
5413
5442
  }
5414
5443
  log16.info(TAG15, `Verification passed for #${card.short_id}`);
@@ -5455,7 +5484,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
5455
5484
  agentSessionId: agentSessionId ?? null
5456
5485
  });
5457
5486
  }
5458
- await teardownWorktree(client, card.id, worktreePath, branchName);
5487
+ await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
5459
5488
  log16.info(TAG15, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
5460
5489
  return true;
5461
5490
  }
@@ -5521,7 +5550,7 @@ function commitUncommittedChanges(worktreePath, card) {
5521
5550
  return false;
5522
5551
  }
5523
5552
  }
5524
- 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], {
5525
5554
  cwd,
5526
5555
  encoding: "utf-8"
5527
5556
  })) {
@@ -5529,16 +5558,22 @@ function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args,
5529
5558
  try {
5530
5559
  gitImpl(["merge-base", "--is-ancestor", baselineSha, "HEAD"], worktreePath);
5531
5560
  } catch {
5532
- return false;
5561
+ return "unknown";
5533
5562
  }
5534
5563
  }
5535
5564
  const range = baselineSha ? `${baselineSha}..HEAD` : `origin/${baseBranch}..HEAD`;
5565
+ let count;
5536
5566
  try {
5537
- const count = gitImpl(["rev-list", "--count", range], worktreePath).trim();
5538
- return parseInt(count, 10) > 0;
5567
+ count = parseInt(gitImpl(["rev-list", "--count", range], worktreePath).trim(), 10);
5539
5568
  } catch {
5540
- return false;
5569
+ return "unknown";
5541
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";
5542
5577
  }
5543
5578
  function stripDaemonBlocks(description) {
5544
5579
  const indices = [SUMMARY_MARKER, BRANCH_PROVENANCE_MARKER].map((marker) => description.indexOf(marker)).filter((index) => index >= 0);
@@ -6437,9 +6472,12 @@ function stripReviewSummary(description) {
6437
6472
  return description;
6438
6473
  return description.slice(0, idx).trimEnd();
6439
6474
  }
6440
- async function postReviewComment(client, card, commentType, body) {
6475
+ async function postReviewComment(client, card, commentType, body, agentSessionId) {
6441
6476
  try {
6442
- await client.addComment(card.id, body, { commentType });
6477
+ await client.addComment(card.id, body, {
6478
+ commentType,
6479
+ agentSessionId: agentSessionId ?? undefined
6480
+ });
6443
6481
  } catch (err) {
6444
6482
  log19.error(TAG18, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6445
6483
  }
@@ -6505,7 +6543,7 @@ ${runLogTail}
6505
6543
  ].filter(Boolean).join(`
6506
6544
 
6507
6545
  `);
6508
- await postReviewComment(client, card, "blocker", body);
6546
+ await postReviewComment(client, card, "blocker", body, agentSessionId);
6509
6547
  }
6510
6548
  await client.endAgentSession(card.id, {
6511
6549
  status: "paused",
@@ -6584,7 +6622,7 @@ ${runLogTail}
6584
6622
  ].filter(Boolean).join(`
6585
6623
 
6586
6624
  `);
6587
- await postReviewComment(client, card, "decision", body);
6625
+ await postReviewComment(client, card, "decision", body, agentSessionId);
6588
6626
  }
6589
6627
  await client.endAgentSession(card.id, {
6590
6628
  status: "completed",
@@ -6612,7 +6650,7 @@ ${runLogTail}
6612
6650
  ].filter(Boolean).join(`
6613
6651
 
6614
6652
  `);
6615
- await postReviewComment(client, card, "blocker", body);
6653
+ await postReviewComment(client, card, "blocker", body, agentSessionId);
6616
6654
  await client.endAgentSession(card.id, {
6617
6655
  status: "completed",
6618
6656
  ...buildTokenPayload(sessionStats)
@@ -6648,7 +6686,7 @@ ${runLogTail}
6648
6686
  }));
6649
6687
  if (linkedFindings.length > 0) {
6650
6688
  for (const body2 of buildFindingComments(linkedFindings)) {
6651
- await postReviewComment(client, card, "finding", body2);
6689
+ await postReviewComment(client, card, "finding", body2, agentSessionId);
6652
6690
  }
6653
6691
  }
6654
6692
  await Promise.all((handbackVerdict.proceed ? minorFindings : []).map(async (finding) => {
@@ -6677,7 +6715,7 @@ ${runLogTail}
6677
6715
  ].filter(Boolean).join(`
6678
6716
 
6679
6717
  `);
6680
- await postReviewComment(client, card, "summary", body);
6718
+ await postReviewComment(client, card, "summary", body, agentSessionId);
6681
6719
  }
6682
6720
  if (handbackVerdict.proceed && config.planning.enabled && card.plan_id) {
6683
6721
  try {
@@ -7700,19 +7738,20 @@ import {
7700
7738
  buildGateCollectorRegistry,
7701
7739
  cleanupWorktree as cleanupWorktree4,
7702
7740
  collectGateEvidence,
7703
- containedEnv as containedEnv2,
7704
7741
  DevServerReadinessError,
7742
+ devServerLaunch,
7705
7743
  formatDiffSummary,
7706
7744
  GIT_NO_HOOKS as GIT_NO_HOOKS7,
7707
7745
  implementRunContainmentCliArgs,
7708
7746
  log as log24,
7709
7747
  probeDevServer,
7748
+ removeSandboxContainer,
7710
7749
  resolveStageGate,
7711
7750
  secretEnvKeysToStrip,
7712
7751
  signalGroup,
7713
7752
  spawnInGroup as spawnInGroup2,
7714
- spawnRunArgs,
7715
7753
  terminateGroup,
7754
+ verificationSandbox as verificationSandbox2,
7716
7755
  waitForDevServer
7717
7756
  } from "@gethmy/harness";
7718
7757
 
@@ -7731,6 +7770,7 @@ class ReviewWorker {
7731
7770
  startedAt = null;
7732
7771
  process = null;
7733
7772
  devServerProcess = null;
7773
+ devServerContainer = null;
7734
7774
  timeoutTimer = null;
7735
7775
  heartbeatTimer = null;
7736
7776
  progressTracker = null;
@@ -7841,7 +7881,10 @@ class ReviewWorker {
7841
7881
  if (crossedTurnCap(before, after, cap)) {
7842
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.`);
7843
7883
  const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
7844
- 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
+ });
7845
7888
  }
7846
7889
  } catch {}
7847
7890
  }
@@ -7952,11 +7995,19 @@ class ReviewWorker {
7952
7995
  const port = this.reviewPort;
7953
7996
  const cwd = this.worktreePath;
7954
7997
  log24.info(this.tag, `Starting dev server on port ${port}...`);
7955
- const [devCmd, devArgs] = spawnRunArgs("dev", "--port", String(port));
7956
- 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, {
7957
8008
  cwd,
7958
8009
  stdio: ["ignore", "pipe", "pipe"],
7959
- env: containedEnv2()
8010
+ stripEnvKeys: secretEnvKeysToStrip()
7960
8011
  });
7961
8012
  let devServerSpawnError = null;
7962
8013
  this.devServerProcess.once("error", (err) => {
@@ -7970,7 +8021,7 @@ class ReviewWorker {
7970
8021
  progressPercent: 10
7971
8022
  });
7972
8023
  if (devServerSpawnError) {
7973
- throw new DevServerReadinessError(`dev server failed to start (${devCmd}): ${devServerSpawnError.message}`);
8024
+ throw new DevServerReadinessError(`dev server failed to start (${launch.cmd}): ${devServerSpawnError.message}`);
7974
8025
  }
7975
8026
  await waitForDevServer(this.devServerProcess, 30000);
7976
8027
  await probeDevServer(port);
@@ -8230,7 +8281,8 @@ ${userPrompt}`;
8230
8281
  let commentId = null;
8231
8282
  try {
8232
8283
  const res = await this.client.addComment(card.id, body, {
8233
- commentType: "blocker"
8284
+ commentType: "blocker",
8285
+ agentSessionId: this.sessionId ?? undefined
8234
8286
  });
8235
8287
  commentId = res?.comment?.id ?? null;
8236
8288
  } catch (err) {
@@ -8355,7 +8407,8 @@ ${userPrompt}`;
8355
8407
  ...implementRunContainmentCliArgs({
8356
8408
  worktree: this.worktreePath,
8357
8409
  readOnly: true,
8358
- 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
8359
8412
  }),
8360
8413
  "--",
8361
8414
  prompt
@@ -8473,6 +8526,11 @@ ${userPrompt}`;
8473
8526
  this.devServerProcess = null;
8474
8527
  log24.debug(this.tag, "Killed dev server group");
8475
8528
  }
8529
+ if (this.devServerContainer) {
8530
+ const name = this.devServerContainer;
8531
+ this.devServerContainer = null;
8532
+ removeSandboxContainer(name);
8533
+ }
8476
8534
  }
8477
8535
  cleanup() {
8478
8536
  if (this.timeoutTimer) {
@@ -9078,7 +9136,10 @@ async function dispatchWave(parent, stage, plan, items, deps) {
9078
9136
  created += 1;
9079
9137
  }
9080
9138
  if (created > 0 && plan.truncated > 0) {
9081
- 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(() => {});
9082
9143
  }
9083
9144
  return created;
9084
9145
  }
@@ -9161,14 +9222,17 @@ function runMotorStage(args, deps) {
9161
9222
  args.repoPath,
9162
9223
  "--session",
9163
9224
  args.sessionId,
9164
- ...args.metricsPath ? ["--metrics", args.metricsPath] : []
9225
+ ...args.metricsPath ? ["--metrics", args.metricsPath] : [],
9226
+ ...args.sandboxImage?.trim() ? ["--sandbox-image", args.sandboxImage.trim()] : []
9165
9227
  ];
9166
9228
  return new Promise((resolve2) => {
9167
9229
  const child = spawnFn("node", argv, {
9168
9230
  env: {
9169
9231
  ...process.env,
9170
9232
  HARMONY_API_URL: motorApiBase(deps.env.apiUrl),
9171
- 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
9172
9236
  },
9173
9237
  stdio: ["ignore", "pipe", "pipe"]
9174
9238
  });
@@ -9293,6 +9357,26 @@ function runMotorStage(args, deps) {
9293
9357
  var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
9294
9358
  var init_motor_driver = () => {};
9295
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
+
9296
9380
  // src/stage-advance.ts
9297
9381
  import { gateConfigErrorReason, log as log29 } from "@gethmy/harness";
9298
9382
  function endDispositionFor(outcome) {
@@ -9363,7 +9447,7 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
9363
9447
  currentTask: reason
9364
9448
  });
9365
9449
  } catch {}
9366
- 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 });
9367
9451
  log29.info(TAG27, `#${card.short_id} GateMisconfigured: ${reason}`);
9368
9452
  return { kind: "held_misconfigured", reason, endDisposition };
9369
9453
  }
@@ -9434,7 +9518,12 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
9434
9518
  currentTask: reason
9435
9519
  });
9436
9520
  } catch {}
9437
- 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
+ });
9438
9527
  log29.info(TAG27, `#${card.short_id} LoopExhausted: ${reason}`);
9439
9528
  return { kind: "held_gate_unmet", reason, endDisposition };
9440
9529
  }
@@ -9450,7 +9539,10 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
9450
9539
  await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
9451
9540
  const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
9452
9541
  try {
9453
- 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
+ });
9454
9546
  } catch {}
9455
9547
  await runTransition(deps.client, guard.card, {
9456
9548
  move: { columnName: toColumn },
@@ -9474,7 +9566,10 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
9474
9566
  decisions: [],
9475
9567
  nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
9476
9568
  });
9477
- 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
+ });
9478
9573
  } catch (err) {
9479
9574
  log29.warn(TAG27, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
9480
9575
  }
@@ -9509,13 +9604,13 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9509
9604
  }
9510
9605
  if (next.kind === "out_of_range") {
9511
9606
  const reason = `Stage advancement aborted: stage index ${stageIndex} is out of range for the pinned playbook version — holding for a human.`;
9512
- 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 });
9513
9608
  return { kind: "held_misconfigured", reason, endDisposition };
9514
9609
  }
9515
9610
  const toColumn = await resolveStageColumnName(deps.client, card, next.stage);
9516
9611
  if (!toColumn) {
9517
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).`;
9518
- 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 });
9519
9614
  return { kind: "held_misconfigured", reason, endDisposition };
9520
9615
  }
9521
9616
  await persistStagePointer(deps.client, card, {
@@ -9539,7 +9634,12 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
9539
9634
  log29.info(TAG27, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
9540
9635
  if (next.stage.owner === "human") {
9541
9636
  const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
9542
- 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
+ });
9543
9643
  return {
9544
9644
  kind: "advanced",
9545
9645
  toStageId: next.stage.id,
@@ -9562,7 +9662,12 @@ async function handleGateUnmet(card, stage, summary, deps) {
9562
9662
  currentTask: reason
9563
9663
  });
9564
9664
  } catch {}
9565
- 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
+ });
9566
9671
  log29.info(TAG27, `#${card.short_id} GateUnmetExhausted: ${reason}`);
9567
9672
  return { kind: "held_gate_unmet", reason, endDisposition };
9568
9673
  }
@@ -9576,7 +9681,10 @@ async function handleGateUnmet(card, stage, summary, deps) {
9576
9681
  }
9577
9682
  const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
9578
9683
  try {
9579
- 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
+ });
9580
9688
  } catch {}
9581
9689
  await runTransition(deps.client, guard.card, {
9582
9690
  move: { columnName: toColumn },
@@ -9614,7 +9722,10 @@ async function guardStageReclaim(card, what, deps) {
9614
9722
  }
9615
9723
  log29.info(TAG27, `#${card.short_id} ${what} refused — ${detail} (${reason})${released ? "; released the daemon's claim so the stage router stops here" : ""}`);
9616
9724
  try {
9617
- 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
+ });
9618
9729
  } catch {}
9619
9730
  return { proceed: false, reason, released };
9620
9731
  }
@@ -9623,7 +9734,10 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
9623
9734
  await stateStore.decrementAttempt(card.id).catch(() => {});
9624
9735
  }
9625
9736
  try {
9626
- await client.addComment(card.id, reason, { commentType: "blocker" });
9737
+ await client.addComment(card.id, reason, {
9738
+ commentType: "blocker",
9739
+ agentSessionId: opts.agentSessionId ?? undefined
9740
+ });
9627
9741
  } catch {}
9628
9742
  try {
9629
9743
  await runTransition(client, card, { removeLabels: [AGENT_LABEL] }, { store: stateStore, runId });
@@ -9674,6 +9788,7 @@ import {
9674
9788
  spawnInGroup as spawnInGroup4,
9675
9789
  teardownWorktree as teardownWorktree2,
9676
9790
  terminateGroup as terminateGroup3,
9791
+ verificationSandbox as verificationSandbox3,
9677
9792
  WorktreeBaseError
9678
9793
  } from "@gethmy/harness";
9679
9794
  function sdkDraftLogLine(ev) {
@@ -9790,6 +9905,9 @@ class Worker {
9790
9905
  runTurns = 0;
9791
9906
  chargedCents = 0;
9792
9907
  chargedTurns = 0;
9908
+ runToolCalls = 0;
9909
+ noCommitNoticeSettled = false;
9910
+ runExpectsCommits = true;
9793
9911
  runLegIndex = 0;
9794
9912
  lastRunText = "";
9795
9913
  constructor(id, config, client, identity, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
@@ -9893,7 +10011,7 @@ class Worker {
9893
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.`);
9894
10012
  try {
9895
10013
  const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
9896
- 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 });
9897
10015
  } catch (err) {
9898
10016
  log30.warn(TAG28, `Could not post the turn-cap notice for ${cardId}: ${err instanceof Error ? err.message : err}`);
9899
10017
  }
@@ -9911,6 +10029,9 @@ class Worker {
9911
10029
  this.runTurns = 0;
9912
10030
  this.chargedCents = 0;
9913
10031
  this.chargedTurns = 0;
10032
+ this.runToolCalls = 0;
10033
+ this.noCommitNoticeSettled = false;
10034
+ this.runExpectsCommits = true;
9914
10035
  this.runLegIndex = 0;
9915
10036
  this.lastRunText = "";
9916
10037
  this.cliSessionId = null;
@@ -10121,10 +10242,12 @@ class Worker {
10121
10242
  this.cancel("timeout");
10122
10243
  }, this.config.maxTimeout);
10123
10244
  this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
10245
+ this.runExpectsCommits = stageCtx.kind === "run" ? stageRunExpectsCommit(stageCtx.stage, stageCtx.allowedTools) : true;
10124
10246
  await this.spawnClaude(prompt, card, subtasks, {
10125
10247
  model: implementModel,
10126
10248
  maxTurns: this.grantedTurns ?? undefined,
10127
10249
  resumeSessionId: this.cliSessionId ?? undefined,
10250
+ noCommitNotice: true,
10128
10251
  ...this.activeRunSpawnOpts ?? {}
10129
10252
  });
10130
10253
  if (this.aborted)
@@ -10157,7 +10280,7 @@ class Worker {
10157
10280
  const onBeforeWorktreeCleanup = stageRun ? async (worktreePath) => {
10158
10281
  stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
10159
10282
  } : undefined;
10160
- 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);
10161
10284
  if (completed === "park") {
10162
10285
  await this.parkForDecision(card, "max_turns");
10163
10286
  return;
@@ -10234,7 +10357,7 @@ class Worker {
10234
10357
  }
10235
10358
  if (this.worktreePath) {
10236
10359
  try {
10237
- 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);
10238
10361
  } catch {
10239
10362
  log30.warn(this.tag, "Failed to cleanup worktree before requeue");
10240
10363
  }
@@ -10295,7 +10418,7 @@ class Worker {
10295
10418
  } else if (this.runId && this.timedOut) {
10296
10419
  if (this.worktreePath) {
10297
10420
  try {
10298
- 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);
10299
10422
  } catch {
10300
10423
  log30.warn(this.tag, "Failed to cleanup worktree before requeue");
10301
10424
  }
@@ -10543,7 +10666,8 @@ class Worker {
10543
10666
  agentId: this.identity.agentId
10544
10667
  },
10545
10668
  sink: this.cliRunner,
10546
- 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
10547
10671
  });
10548
10672
  } catch (err) {
10549
10673
  const detail = err instanceof Error ? err.message : String(err);
@@ -10607,7 +10731,10 @@ class Worker {
10607
10731
  log30.info(this.tag, `Holding #${card.short_id}: ${reason}`);
10608
10732
  await this.stateStore.decrementAttempt(card.id);
10609
10733
  try {
10610
- 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
+ });
10611
10738
  } catch {}
10612
10739
  try {
10613
10740
  await runTransition(this.client, card, {
@@ -10673,9 +10800,16 @@ class Worker {
10673
10800
  const waitHours = this.config.budget.pause.waitHours;
10674
10801
  const until = computeDecisionDeadline(waitHours);
10675
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
+ }
10676
10809
  const body = formatBudgetComment({
10677
10810
  trigger,
10678
10811
  numTurns: stats?.cost?.numTurns ?? 0,
10812
+ hasCommits,
10679
10813
  maxTurns: this.effectiveMaxTurns,
10680
10814
  toolCalls: stats?.toolCalls ?? 0,
10681
10815
  durationMs: stats?.cost?.durationMs ?? 0,
@@ -10689,7 +10823,8 @@ class Worker {
10689
10823
  let commentId = null;
10690
10824
  try {
10691
10825
  const res = await this.client.addComment(card.id, body, {
10692
- commentType: "blocker"
10826
+ commentType: "blocker",
10827
+ agentSessionId: this.sessionId ?? undefined
10693
10828
  });
10694
10829
  commentId = res?.comment?.id ?? null;
10695
10830
  } catch (err) {
@@ -10798,7 +10933,8 @@ class Worker {
10798
10933
  workspaceId: this.workspaceId,
10799
10934
  repoPath: worktreePath,
10800
10935
  sessionId,
10801
- metricsPath
10936
+ metricsPath,
10937
+ sandboxImage: this.config.verification.sandboxImage
10802
10938
  }, {
10803
10939
  env: {
10804
10940
  apiUrl: this.client.getApiUrl(),
@@ -10897,7 +11033,7 @@ class Worker {
10897
11033
  if (!worktreePath)
10898
11034
  return;
10899
11035
  try {
10900
- await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined);
11036
+ await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined, this.sessionId);
10901
11037
  } catch {
10902
11038
  log30.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
10903
11039
  }
@@ -10972,7 +11108,10 @@ ${prompt}`;
10972
11108
  decisions: [],
10973
11109
  nextStageNeeds: "Pick up from the produced artifact above; treat the recorded decisions as settled."
10974
11110
  });
10975
- 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
+ });
10976
11115
  log30.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
10977
11116
  } catch (err) {
10978
11117
  log30.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
@@ -10998,7 +11137,8 @@ ${prompt}`;
10998
11137
  build: {
10999
11138
  worktreePath,
11000
11139
  buildTimeout: this.config.verification.timeout,
11001
- lintTimeout: this.config.verification.timeout
11140
+ lintTimeout: this.config.verification.timeout,
11141
+ sandbox: verificationSandbox3(this.config)
11002
11142
  },
11003
11143
  artifact: {
11004
11144
  worktreePath,
@@ -11038,7 +11178,8 @@ ${prompt}`;
11038
11178
  maxAttempts: this.config.budget.maxAttemptsPerCard,
11039
11179
  fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
11040
11180
  sink: this.cliRunner,
11041
- runId: this.runId ?? undefined
11181
+ runId: this.runId ?? undefined,
11182
+ agentSessionId: this.sessionId
11042
11183
  });
11043
11184
  } catch (err) {
11044
11185
  log30.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
@@ -11100,6 +11241,44 @@ ${prompt}`;
11100
11241
  }
11101
11242
  });
11102
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
+ }
11103
11282
  recordRunSized() {
11104
11283
  if (!this.modelChoice)
11105
11284
  return;
@@ -11132,7 +11311,8 @@ ${prompt}`;
11132
11311
  try {
11133
11312
  const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3), this.config.budget.pause.enabled);
11134
11313
  const res = await this.client.addComment(cardId, body, {
11135
- commentType: "blocker"
11314
+ commentType: "blocker",
11315
+ agentSessionId: this.sessionId ?? undefined
11136
11316
  });
11137
11317
  giveUpCommentId = res?.comment?.id ?? null;
11138
11318
  log30.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
@@ -11447,6 +11627,7 @@ ${prompt}`;
11447
11627
  model: this.selectImplementModel(card),
11448
11628
  maxTurns: STEERING_MAX_TURNS,
11449
11629
  resumeSessionId: this.cliSessionId,
11630
+ noCommitNotice: true,
11450
11631
  ...this.activeRunSpawnOpts ?? {}
11451
11632
  });
11452
11633
  } catch (err) {
@@ -11479,7 +11660,8 @@ ${prompt}`;
11479
11660
  ...this.config.claude.additionalArgs,
11480
11661
  ...implementRunContainmentCliArgs2({
11481
11662
  worktree: this.worktreePath,
11482
- 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
11483
11665
  }),
11484
11666
  "--",
11485
11667
  prompt
@@ -11518,6 +11700,11 @@ ${prompt}`;
11518
11700
  this.lastRunText += content;
11519
11701
  this.captureCliSessionId(parser.sessionId);
11520
11702
  });
11703
+ if (opts.noCommitNotice) {
11704
+ parser.on("tool_start", () => {
11705
+ this.countToolCallForNoCommitNotice();
11706
+ });
11707
+ }
11521
11708
  parser.on("parse_error", (msg) => {
11522
11709
  log30.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
11523
11710
  runLog?.stream.write(`
@@ -11607,7 +11794,8 @@ ${prompt}`;
11607
11794
  maxBudgetUsd: sdkCfg?.maxBudgetUsd,
11608
11795
  ...implementRunContainment({
11609
11796
  worktree: this.worktreePath,
11610
- extraDisallowedTools: disallowedTools
11797
+ extraDisallowedTools: disallowedTools,
11798
+ run: this.sessionId ? { cardId: card.id, agentSessionId: this.sessionId } : undefined
11611
11799
  }),
11612
11800
  onSpawn: (child) => {
11613
11801
  this.process = child;
@@ -11628,6 +11816,9 @@ ${prompt}`;
11628
11816
  try {
11629
11817
  for await (const ev of stream) {
11630
11818
  this.progressTracker?.ingest(ev);
11819
+ if (opts.noCommitNotice && ev.kind === "tool_started") {
11820
+ this.countToolCallForNoCommitNotice();
11821
+ }
11631
11822
  if (ev.kind === "assistant_text") {
11632
11823
  this.lastRunText += `${ev.payload.text}
11633
11824
  `;
@@ -11711,7 +11902,7 @@ ${prompt}`;
11711
11902
  }
11712
11903
  if (this.worktreePath && this.state !== "parked" && (this.state === "error" || this.timedOut || this.aborted)) {
11713
11904
  try {
11714
- 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);
11715
11906
  } catch {
11716
11907
  log30.warn(this.tag, "Failed to cleanup worktree");
11717
11908
  }
@@ -12227,7 +12418,10 @@ class Pool {
12227
12418
  try {
12228
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.
12229
12420
 
12230
- 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
+ });
12231
12425
  } catch (err) {
12232
12426
  log31.warn(TAG29, `Could not explain the refused Continue on ${run.cardId}: ${err instanceof Error ? err.message : err}`);
12233
12427
  }
@@ -13214,8 +13408,139 @@ var init_sweep = __esm(() => {
13214
13408
  init_unblock();
13215
13409
  });
13216
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
+
13217
13542
  // src/reconcile.ts
13218
- import { detectGitProvider as detectGitProvider5, log as log37 } from "@gethmy/harness";
13543
+ import { detectGitProvider as detectGitProvider5, log as log38 } from "@gethmy/harness";
13219
13544
  function daemonProcessLabels(approvedLabel) {
13220
13545
  const names = ["agent", "agent-recovered", NEED_REVIEW_LABEL, approvedLabel];
13221
13546
  return new Set(names.filter(Boolean).map((n) => n.toLowerCase()));
@@ -13245,6 +13570,7 @@ class Reconciler {
13245
13570
  gitProvider = null;
13246
13571
  repoDirectories = null;
13247
13572
  lastSweepStopReported = null;
13573
+ publishSweep = () => {};
13248
13574
  get lastTick() {
13249
13575
  return this.lastTickAt;
13250
13576
  }
@@ -13274,7 +13600,7 @@ class Reconciler {
13274
13600
  clearInterval(this.timer);
13275
13601
  this.timer = null;
13276
13602
  }
13277
- log37.info(TAG35, "Heartbeat stopped");
13603
+ log38.info(TAG36, "Heartbeat stopped");
13278
13604
  }
13279
13605
  async recoverStaleRuns() {
13280
13606
  if (!this.stateStore || !this.agentConfig)
@@ -13285,7 +13611,7 @@ class Reconciler {
13285
13611
  const pool = this.pool;
13286
13612
  for (const run of active) {
13287
13613
  if (isBudgetHeldRun(run)) {
13288
- 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`);
13289
13615
  continue;
13290
13616
  }
13291
13617
  const foreignDaemon = run.daemonPid !== process.pid;
@@ -13295,7 +13621,7 @@ class Reconciler {
13295
13621
  if (!daemonDead && !(heartbeatStale && ourZombie))
13296
13622
  continue;
13297
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`;
13298
- log37.warn(TAG35, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
13624
+ log38.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
13299
13625
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
13300
13626
  runId: run.runId,
13301
13627
  cardId: run.cardId,
@@ -13322,11 +13648,11 @@ class Reconciler {
13322
13648
  const stalledAt = Date.parse(card.updated_at ?? "");
13323
13649
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
13324
13650
  continue;
13325
- 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}"`);
13326
13652
  try {
13327
13653
  await this.client.moveCard(card.id, pickupCol.id);
13328
13654
  } catch (err) {
13329
- 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}`);
13330
13656
  }
13331
13657
  }
13332
13658
  }
@@ -13358,7 +13684,7 @@ class Reconciler {
13358
13684
  return;
13359
13685
  const cardLabels = resolveCardLabels(card, labelMap);
13360
13686
  const subtasks = card.subtasks ?? [];
13361
- 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)`);
13362
13688
  await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
13363
13689
  }
13364
13690
  });
@@ -13382,11 +13708,11 @@ class Reconciler {
13382
13708
  const parkedAt = Date.parse(card.updated_at ?? "");
13383
13709
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
13384
13710
  continue;
13385
- 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}"`);
13386
13712
  try {
13387
13713
  await this.client.moveCard(card.id, pickupCol.id);
13388
13714
  } catch (err) {
13389
- 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}`);
13390
13716
  }
13391
13717
  }
13392
13718
  }
@@ -13414,13 +13740,14 @@ class Reconciler {
13414
13740
  now: Date.now()
13415
13741
  };
13416
13742
  const verdict = this.sweepGuard?.check() ?? { claiming: true };
13743
+ this.publishSweepState();
13417
13744
  if (!verdict.claiming) {
13418
13745
  this.reportSweepStopped(verdict.reason, verdict.detail);
13419
13746
  return;
13420
13747
  }
13421
13748
  this.lastSweepStopReported = null;
13422
13749
  if (!this.pool.hasFreeImplementSlot()) {
13423
- log37.debug(TAG35, "sweep skipped — no free implement slot");
13750
+ log38.debug(TAG36, "sweep skipped — no free implement slot");
13424
13751
  return;
13425
13752
  }
13426
13753
  const { claimed } = await sweepForCard({
@@ -13433,25 +13760,32 @@ class Reconciler {
13433
13760
  if (!claimed)
13434
13761
  return;
13435
13762
  await this.sweepGuard?.recordClaim(claimed.card.id);
13763
+ this.publishSweepState();
13436
13764
  await this.pool.enqueue(claimed.card, claimed.column, claimed.labels, claimed.card.subtasks ?? [], "implement");
13437
13765
  }
13438
13766
  reportSweepStopped(reason, detail) {
13439
13767
  const line = `sweep stopped claiming — ${detail}`;
13440
13768
  if (this.lastSweepStopReported === reason)
13441
- log37.debug(TAG35, line);
13769
+ log38.debug(TAG36, line);
13442
13770
  else
13443
- log37.warn(TAG35, line);
13771
+ log38.warn(TAG36, line);
13444
13772
  this.lastSweepStopReported = reason;
13445
13773
  }
13774
+ setSweepPresenceSink(sink) {
13775
+ this.publishSweep = sink;
13776
+ }
13777
+ publishSweepState() {
13778
+ this.publishSweep(toSweepPresence(this.sweepGuard?.snapshot()));
13779
+ }
13446
13780
  resolveRepoDirectories() {
13447
13781
  if (this.repoDirectories)
13448
13782
  return this.repoDirectories;
13449
13783
  const dirs = readRepoDirectories(process.cwd());
13450
13784
  this.repoDirectories = dirs;
13451
13785
  if (dirs.size === 0) {
13452
- 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`);
13453
13787
  } else {
13454
- log37.debug(TAG35, `Path signal allows ${dirs.size} top-level directories`);
13788
+ log38.debug(TAG36, `Path signal allows ${dirs.size} top-level directories`);
13455
13789
  }
13456
13790
  return dirs;
13457
13791
  }
@@ -13495,21 +13829,21 @@ class Reconciler {
13495
13829
  const subtasks = card.subtasks ?? [];
13496
13830
  const mode = route.mode;
13497
13831
  if (route.stage) {
13498
- 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`);
13499
13833
  }
13500
13834
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
13501
- 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`);
13502
13836
  continue;
13503
13837
  }
13504
13838
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
13505
- 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)`);
13506
13840
  continue;
13507
13841
  }
13508
13842
  if (mode === "review" && !qualifiesForAutoReview(card.description)) {
13509
- 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)`);
13510
13844
  continue;
13511
13845
  }
13512
- 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`);
13513
13847
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
13514
13848
  }
13515
13849
  }
@@ -13519,13 +13853,13 @@ class Reconciler {
13519
13853
  try {
13520
13854
  await this.pool.drainBudgetDecisions();
13521
13855
  } catch (err) {
13522
- 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}`);
13523
13857
  }
13524
13858
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
13525
13859
  await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
13526
13860
  for (const knownId of knownCardIds) {
13527
13861
  if (!allAgentCardIds.has(knownId)) {
13528
- log37.info(TAG35, `Missed unassign: ${knownId} — removing`);
13862
+ log38.info(TAG36, `Missed unassign: ${knownId} — removing`);
13529
13863
  await this.pool.removeCard(knownId);
13530
13864
  }
13531
13865
  }
@@ -13533,15 +13867,15 @@ class Reconciler {
13533
13867
  try {
13534
13868
  await this.sweepForWork(cards, columns, labelMap, pickupConfig);
13535
13869
  } catch (err) {
13536
- 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}`);
13537
13871
  }
13538
- log37.debug(TAG35, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
13872
+ log38.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
13539
13873
  } catch (err) {
13540
- log37.error(TAG35, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
13874
+ log38.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
13541
13875
  }
13542
13876
  }
13543
13877
  }
13544
- var TAG35 = "reconcile";
13878
+ var TAG36 = "reconcile";
13545
13879
  var init_reconcile = __esm(() => {
13546
13880
  init_board_helpers();
13547
13881
  init_recovery();
@@ -13550,6 +13884,7 @@ var init_reconcile = __esm(() => {
13550
13884
  init_state_store();
13551
13885
  init_strand_recovery();
13552
13886
  init_sweep();
13887
+ init_sweep_guard();
13553
13888
  init_types2();
13554
13889
  });
13555
13890
 
@@ -13558,7 +13893,7 @@ var exports_startup_banner = {};
13558
13893
  __export(exports_startup_banner, {
13559
13894
  createStartupBanner: () => createStartupBanner
13560
13895
  });
13561
- import { isPretty, log as log38 } from "@gethmy/harness";
13896
+ import { isPretty, log as log39 } from "@gethmy/harness";
13562
13897
  function createStartupBanner(config, version) {
13563
13898
  return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
13564
13899
  }
@@ -13583,7 +13918,7 @@ function prettyBanner(config, version) {
13583
13918
  checks.push({ kind: "ok", message });
13584
13919
  },
13585
13920
  warn(message) {
13586
- log38.warn(TAG36, message);
13921
+ log39.warn(TAG37, message);
13587
13922
  checks.push({ kind: "warn", message: message.split(`
13588
13923
  `, 1)[0] });
13589
13924
  },
@@ -13608,25 +13943,25 @@ function prettyBanner(config, version) {
13608
13943
  };
13609
13944
  }
13610
13945
  function jsonBanner(config, version) {
13611
- log38.info(TAG36, `Harmony Agent Daemon v${version} starting...`);
13612
- 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(", ")}`);
13613
13948
  if (config.agent.review.enabled) {
13614
- 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}`);
13615
13950
  }
13616
13951
  let failed = false;
13617
13952
  return {
13618
13953
  setProjectName(_name) {},
13619
13954
  setGitProvider(provider) {
13620
- log38.info(TAG36, `Git provider: ${provider}`);
13955
+ log39.info(TAG37, `Git provider: ${provider}`);
13621
13956
  },
13622
13957
  setHttpPort(port) {
13623
- log38.info(TAG36, `HTTP server on port ${port}`);
13958
+ log39.info(TAG37, `HTTP server on port ${port}`);
13624
13959
  },
13625
13960
  check(message) {
13626
- log38.info(TAG36, message);
13961
+ log39.info(TAG37, message);
13627
13962
  },
13628
13963
  warn(message) {
13629
- log38.warn(TAG36, message);
13964
+ log39.warn(TAG37, message);
13630
13965
  },
13631
13966
  fail() {
13632
13967
  failed = true;
@@ -13634,7 +13969,7 @@ function jsonBanner(config, version) {
13634
13969
  async ready(message) {
13635
13970
  if (failed)
13636
13971
  return;
13637
- log38.info(TAG36, message);
13972
+ log39.info(TAG37, message);
13638
13973
  }
13639
13974
  };
13640
13975
  }
@@ -13715,7 +14050,7 @@ function cyan(s) {
13715
14050
  function yellow(s) {
13716
14051
  return `${ANSI.yellow}${s}${ANSI.reset}`;
13717
14052
  }
13718
- var TAG36 = "daemon", RULE_WIDTH = 70, ANSI;
14053
+ var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
13719
14054
  var init_startup_banner = __esm(() => {
13720
14055
  ANSI = {
13721
14056
  reset: "\x1B[0m",
@@ -13816,133 +14151,27 @@ var init_stream_parser_selftest = __esm(() => {
13816
14151
  init_stream_parser();
13817
14152
  });
13818
14153
 
13819
- // src/sweep-guard.ts
13820
- import { log as log39 } from "@gethmy/harness";
13821
-
13822
- class SweepGuard {
13823
- config;
13824
- store;
13825
- checkDailyBudget;
13826
- constructor(config, store, checkDailyBudget) {
13827
- this.config = config;
13828
- this.store = store;
13829
- this.checkDailyBudget = checkDailyBudget;
13830
- }
13831
- check() {
13832
- const sweep = this.store.getSweep();
13833
- if (sweep.haltReason) {
13834
- return {
13835
- claiming: false,
13836
- reason: sweep.haltReason,
13837
- detail: this.describeStop(sweep.haltReason),
13838
- latched: true
13839
- };
13840
- }
13841
- const cap = this.config.maxCardsPerSweep;
13842
- if (cap >= 0 && sweep.claimed >= cap) {
13843
- return {
13844
- claiming: false,
13845
- reason: "card_cap",
13846
- detail: this.describeStop("card_cap"),
13847
- latched: true
13848
- };
13849
- }
13850
- const daily = this.checkDailyBudget?.();
13851
- if (daily && !daily.allow) {
13852
- return {
13853
- claiming: false,
13854
- latched: false,
13855
- reason: "daily_budget",
13856
- detail: this.describeStop("daily_budget", daily.detail)
13857
- };
13858
- }
13859
- return { claiming: true };
13860
- }
13861
- async recordClaim(cardId) {
13862
- await this.store.recordSweepClaim(cardId);
13863
- const verdict = this.check();
13864
- if (!verdict.claiming && verdict.latched)
13865
- await this.halt(verdict.reason);
13866
- return verdict;
13867
- }
13868
- lastClaimedCardId() {
13869
- return this.store.getSweep().lastClaimedCardId ?? null;
13870
- }
13871
- async halt(reason) {
13872
- const already = this.store.getSweep().haltReason === reason;
13873
- await this.store.haltSweep(reason);
13874
- if (!already) {
13875
- const claimed = this.store.getSweep().claimed;
13876
- log39.info(TAG37, `claiming stopped (${reason}) after ${claimed} claimed`);
13877
- }
13878
- return this.snapshot();
13879
- }
13880
- async resume() {
13881
- const before = this.store.getSweep();
13882
- const was = before.haltReason ? this.describeStop(before.haltReason) : null;
13883
- await this.store.resumeSweep();
13884
- if (was)
13885
- log39.info(TAG37, `claiming resumed by the operator — was stopped: ${was}`);
13886
- return this.snapshot();
13887
- }
13888
- snapshot() {
13889
- const sweep = this.store.getSweep();
13890
- const cap = this.config.maxCardsPerSweep;
13891
- const verdict = this.check();
13892
- return {
13893
- enabled: this.config.enabled,
13894
- claiming: verdict.claiming,
13895
- haltReason: verdict.claiming ? null : verdict.reason,
13896
- haltedAt: sweep.haltedAt,
13897
- claimed: sweep.claimed,
13898
- maxCardsPerSweep: cap < 0 ? null : cap,
13899
- totalClaimed: sweep.totalClaimed,
13900
- detail: verdict.claiming ? null : verdict.detail
13901
- };
13902
- }
13903
- describeStop(reason, budgetDetail) {
13904
- const claimed = this.store.getSweep().claimed;
13905
- const cards = `${claimed} card${claimed === 1 ? "" : "s"}`;
13906
- const resume = "Resume with `harmony-agent sweep resume`.";
13907
- switch (reason) {
13908
- case "card_cap":
13909
- return `card cap reached — ${cards} claimed this sweep, cap is ${this.config.maxCardsPerSweep}. ${resume}`;
13910
- case "operator":
13911
- return `stopped by the operator after ${cards}. ${resume}`;
13912
- case "daily_budget":
13913
- return `daily spend cap reached${budgetDetail ? ` (${budgetDetail})` : ""} after ${cards} — claiming starts again on its own when the UTC day rolls over.`;
13914
- }
13915
- }
13916
- }
13917
- function describeCaps(sweep, dailyBudgetCents) {
13918
- const cards = sweep.maxCardsPerSweep < 0 ? "no cap" : String(sweep.maxCardsPerSweep);
13919
- return `cards/sweep ${cards} · spend/day ${formatDailyCap(dailyBudgetCents)}`;
13920
- }
13921
- function sweepBannerLine(config) {
13922
- const sweep = config.sweep;
13923
- const scope = sweep.requireLabel ? `cards labelled "${sweep.requireLabel}"` : "unassigned cards";
13924
- const extra = sweep.trustedAuthors.length;
13925
- const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
13926
- return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
13927
- }
13928
- var TAG37 = "sweep";
13929
- var init_sweep_guard = () => {};
13930
-
13931
14154
  // src/watcher.ts
13932
14155
  import { randomUUID as randomUUID3 } from "node:crypto";
13933
14156
  import { isPretty as isPretty2, log as log40 } from "@gethmy/harness";
13934
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
+ }
13935
14161
 
13936
14162
  class Watcher {
13937
14163
  credentials;
13938
14164
  projectId;
13939
14165
  identity;
14166
+ routing;
13940
14167
  onCardBroadcast;
13941
14168
  onAgentCommand;
13942
14169
  channel = null;
13943
14170
  presenceChannel = null;
13944
14171
  supabase = null;
13945
14172
  daemonId = randomUUID3();
14173
+ startedAt = new Date().toISOString();
14174
+ sweep = null;
13946
14175
  connected = false;
13947
14176
  presenceTracked = false;
13948
14177
  suppressStartupLogs = true;
@@ -13987,10 +14216,11 @@ class Watcher {
13987
14216
  log40.warn(TAG38, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
13988
14217
  return false;
13989
14218
  }
13990
- constructor(credentials, projectId, identity, onCardBroadcast, onAgentCommand) {
14219
+ constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand) {
13991
14220
  this.credentials = credentials;
13992
14221
  this.projectId = projectId;
13993
14222
  this.identity = identity;
14223
+ this.routing = routing;
13994
14224
  this.onCardBroadcast = onCardBroadcast;
13995
14225
  this.onAgentCommand = onAgentCommand;
13996
14226
  }
@@ -14002,6 +14232,42 @@ class Watcher {
14002
14232
  this.subscribeBroadcast();
14003
14233
  this.subscribePresence();
14004
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
+ }
14005
14271
  subscribePresence() {
14006
14272
  if (!this.supabase)
14007
14273
  return;
@@ -14016,15 +14282,7 @@ class Watcher {
14016
14282
  if (status === "SUBSCRIBED") {
14017
14283
  let trackStatus;
14018
14284
  try {
14019
- trackStatus = await presenceChannel.track({
14020
- daemonId: this.daemonId,
14021
- startedAt: new Date().toISOString(),
14022
- userId: this.identity.userId,
14023
- agentId: this.identity.agentId,
14024
- userEmail: this.identity.userEmail,
14025
- agentIdentifier: this.identity.agentIdentifier,
14026
- agentName: this.identity.agentName
14027
- });
14285
+ trackStatus = await presenceChannel.track(this.presencePayload());
14028
14286
  } catch (err) {
14029
14287
  trackStatus = `error (${String(err)})`;
14030
14288
  }
@@ -14185,8 +14443,10 @@ class Watcher {
14185
14443
  log40.info(TAG38, "Broadcast subscription stopped");
14186
14444
  }
14187
14445
  }
14188
- var TAG38 = "watcher";
14189
- 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
+ });
14190
14450
 
14191
14451
  // src/worktree-gc.ts
14192
14452
  var exports_worktree_gc = {};
@@ -14581,6 +14841,7 @@ async function main() {
14581
14841
  validateBudgetConfig(config.agent);
14582
14842
  validateSweepConfig(config.agent);
14583
14843
  validateRankingConfig(config.agent);
14844
+ validateVerificationConfig(config.agent);
14584
14845
  } catch (err) {
14585
14846
  if (err instanceof ConfigValidationError) {
14586
14847
  banner.fail();
@@ -14725,7 +14986,11 @@ async function main() {
14725
14986
  };
14726
14987
  },
14727
14988
  handleCommand: (cmd, cardId) => pool.handleAgentCommand(cardId, cmd),
14728
- 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
+ },
14729
14994
  getSweep: () => sweepGuard.snapshot(),
14730
14995
  handleAccountResume: () => accountGuard.resume(),
14731
14996
  getAccount: () => accountGuard.snapshot()
@@ -14736,6 +15001,9 @@ async function main() {
14736
15001
  userEmail: config.userEmail,
14737
15002
  agentIdentifier: config.agentIdentifier,
14738
15003
  agentName: config.agentName
15004
+ }, {
15005
+ pickupColumns: config.agent.pickupColumns,
15006
+ reviewColumns
14739
15007
  }, async (event) => {
14740
15008
  await handleBroadcast(event, client, pool, config, agentId);
14741
15009
  }, async (command) => {
@@ -14775,6 +15043,7 @@ async function main() {
14775
15043
  shutdown("unhandledRejection");
14776
15044
  });
14777
15045
  await watcher.start();
15046
+ reconciler.setSweepPresenceSink((state) => watcher.publishSweep(state));
14778
15047
  reconciler.start();
14779
15048
  mergeMonitor?.start();
14780
15049
  worktreeGc.start();