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