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