@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.
- package/dist/cli.js +496 -227
- package/dist/index.js +496 -227
- 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
|
-
|
|
873
|
-
|
|
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,
|
|
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
|
|
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
|
|
5561
|
+
return "unknown";
|
|
5533
5562
|
}
|
|
5534
5563
|
}
|
|
5535
5564
|
const range = baselineSha ? `${baselineSha}..HEAD` : `origin/${baseBranch}..HEAD`;
|
|
5565
|
+
let count;
|
|
5536
5566
|
try {
|
|
5537
|
-
|
|
5538
|
-
return parseInt(count, 10) > 0;
|
|
5567
|
+
count = parseInt(gitImpl(["rev-list", "--count", range], worktreePath).trim(), 10);
|
|
5539
5568
|
} catch {
|
|
5540
|
-
return
|
|
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, {
|
|
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), {
|
|
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
|
|
7956
|
-
|
|
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
|
-
|
|
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 (${
|
|
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.`, {
|
|
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, {
|
|
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}".`, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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}.`, {
|
|
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.`, {
|
|
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, {
|
|
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,
|
|
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, {
|
|
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, {
|
|
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.`, {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13769
|
+
log38.debug(TAG36, line);
|
|
13442
13770
|
else
|
|
13443
|
-
|
|
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
|
-
|
|
13786
|
+
log38.warn(TAG36, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
|
|
13453
13787
|
} else {
|
|
13454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13843
|
+
log38.debug(TAG36, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
13510
13844
|
continue;
|
|
13511
13845
|
}
|
|
13512
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13870
|
+
log38.error(TAG36, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
|
|
13537
13871
|
}
|
|
13538
|
-
|
|
13872
|
+
log38.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
13539
13873
|
} catch (err) {
|
|
13540
|
-
|
|
13874
|
+
log38.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
13541
13875
|
}
|
|
13542
13876
|
}
|
|
13543
13877
|
}
|
|
13544
|
-
var
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
13612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13955
|
+
log39.info(TAG37, `Git provider: ${provider}`);
|
|
13621
13956
|
},
|
|
13622
13957
|
setHttpPort(port) {
|
|
13623
|
-
|
|
13958
|
+
log39.info(TAG37, `HTTP server on port ${port}`);
|
|
13624
13959
|
},
|
|
13625
13960
|
check(message) {
|
|
13626
|
-
|
|
13961
|
+
log39.info(TAG37, message);
|
|
13627
13962
|
},
|
|
13628
13963
|
warn(message) {
|
|
13629
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
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();
|