@gethmy/agent 1.33.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +459 -220
- package/dist/index.js +459 -220
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1131,14 +1131,13 @@ function isDaemonAuthoredComment(comment, identity) {
|
|
|
1131
1131
|
return true;
|
|
1132
1132
|
}
|
|
1133
1133
|
// ../harmony-shared/dist/agentStaleness.js
|
|
1134
|
-
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";
|
|
1135
1135
|
var init_agentStaleness = __esm(() => {
|
|
1136
1136
|
AGENT_HEARTBEAT_LIVENESS_MS = 5 * 60 * 1000;
|
|
1137
1137
|
AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
|
|
1138
1138
|
AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
|
|
1139
1139
|
AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
|
|
1140
1140
|
AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
|
|
1141
|
-
SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
|
|
1142
1141
|
ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
|
|
1143
1142
|
});
|
|
1144
1143
|
// ../harmony-shared/dist/branchRef.js
|
|
@@ -4718,6 +4717,9 @@ function formatBudgetComment(i) {
|
|
|
4718
4717
|
if (i.lastAction)
|
|
4719
4718
|
lines.push(`Last action: ${i.lastAction}.`);
|
|
4720
4719
|
}
|
|
4720
|
+
if (i.hasCommits === false) {
|
|
4721
|
+
lines.push("No commits yet — nothing on the branch past where this run started.");
|
|
4722
|
+
}
|
|
4721
4723
|
lines.push(i.branchName ? `The work is parked, not lost: branch \`${i.branchName}\`, worktree kept.` : "The work is parked, not lost.");
|
|
4722
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.`);
|
|
4723
4725
|
const cap = i.cardTurnCap;
|
|
@@ -5323,7 +5325,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
5323
5325
|
} else {
|
|
5324
5326
|
await endRunSession({ client, tag: TAG15 }, card, { status: "completed" }, buildTokenPayload(sessionStats), "throw");
|
|
5325
5327
|
}
|
|
5326
|
-
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
5328
|
+
await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
|
|
5327
5329
|
return true;
|
|
5328
5330
|
}
|
|
5329
5331
|
log16.warn(TAG15, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
|
|
@@ -5340,7 +5342,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
5340
5342
|
failureSummary,
|
|
5341
5343
|
...buildTokenPayload(sessionStats)
|
|
5342
5344
|
});
|
|
5343
|
-
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
5345
|
+
await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
|
|
5344
5346
|
return false;
|
|
5345
5347
|
}
|
|
5346
5348
|
log16.info(TAG15, `Pushing branch ${branchName} (pre-verify)...`);
|
|
@@ -5435,7 +5437,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
5435
5437
|
recoveryBranch: branchName,
|
|
5436
5438
|
...buildTokenPayload(sessionStats)
|
|
5437
5439
|
});
|
|
5438
|
-
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
5440
|
+
await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
|
|
5439
5441
|
return false;
|
|
5440
5442
|
}
|
|
5441
5443
|
log16.info(TAG15, `Verification passed for #${card.short_id}`);
|
|
@@ -5482,7 +5484,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
|
|
|
5482
5484
|
agentSessionId: agentSessionId ?? null
|
|
5483
5485
|
});
|
|
5484
5486
|
}
|
|
5485
|
-
await teardownWorktree(client, card.id, worktreePath, branchName);
|
|
5487
|
+
await teardownWorktree(client, card.id, worktreePath, branchName, agentSessionId);
|
|
5486
5488
|
log16.info(TAG15, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
|
|
5487
5489
|
return true;
|
|
5488
5490
|
}
|
|
@@ -5548,7 +5550,7 @@ function commitUncommittedChanges(worktreePath, card) {
|
|
|
5548
5550
|
return false;
|
|
5549
5551
|
}
|
|
5550
5552
|
}
|
|
5551
|
-
function
|
|
5553
|
+
function measureRunCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args, cwd) => execFileSync5("git", [...GIT_NO_HOOKS6, ...args], {
|
|
5552
5554
|
cwd,
|
|
5553
5555
|
encoding: "utf-8"
|
|
5554
5556
|
})) {
|
|
@@ -5556,16 +5558,22 @@ function checkHasCommits(worktreePath, baseBranch, baselineSha, gitImpl = (args,
|
|
|
5556
5558
|
try {
|
|
5557
5559
|
gitImpl(["merge-base", "--is-ancestor", baselineSha, "HEAD"], worktreePath);
|
|
5558
5560
|
} catch {
|
|
5559
|
-
return
|
|
5561
|
+
return "unknown";
|
|
5560
5562
|
}
|
|
5561
5563
|
}
|
|
5562
5564
|
const range = baselineSha ? `${baselineSha}..HEAD` : `origin/${baseBranch}..HEAD`;
|
|
5565
|
+
let count;
|
|
5563
5566
|
try {
|
|
5564
|
-
|
|
5565
|
-
return parseInt(count, 10) > 0;
|
|
5567
|
+
count = parseInt(gitImpl(["rev-list", "--count", range], worktreePath).trim(), 10);
|
|
5566
5568
|
} catch {
|
|
5567
|
-
return
|
|
5569
|
+
return "unknown";
|
|
5568
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";
|
|
5569
5577
|
}
|
|
5570
5578
|
function stripDaemonBlocks(description) {
|
|
5571
5579
|
const indices = [SUMMARY_MARKER, BRANCH_PROVENANCE_MARKER].map((marker) => description.indexOf(marker)).filter((index) => index >= 0);
|
|
@@ -6464,9 +6472,12 @@ function stripReviewSummary(description) {
|
|
|
6464
6472
|
return description;
|
|
6465
6473
|
return description.slice(0, idx).trimEnd();
|
|
6466
6474
|
}
|
|
6467
|
-
async function postReviewComment(client, card, commentType, body) {
|
|
6475
|
+
async function postReviewComment(client, card, commentType, body, agentSessionId) {
|
|
6468
6476
|
try {
|
|
6469
|
-
await client.addComment(card.id, body, {
|
|
6477
|
+
await client.addComment(card.id, body, {
|
|
6478
|
+
commentType,
|
|
6479
|
+
agentSessionId: agentSessionId ?? undefined
|
|
6480
|
+
});
|
|
6470
6481
|
} catch (err) {
|
|
6471
6482
|
log19.error(TAG18, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
6472
6483
|
}
|
|
@@ -6532,7 +6543,7 @@ ${runLogTail}
|
|
|
6532
6543
|
].filter(Boolean).join(`
|
|
6533
6544
|
|
|
6534
6545
|
`);
|
|
6535
|
-
await postReviewComment(client, card, "blocker", body);
|
|
6546
|
+
await postReviewComment(client, card, "blocker", body, agentSessionId);
|
|
6536
6547
|
}
|
|
6537
6548
|
await client.endAgentSession(card.id, {
|
|
6538
6549
|
status: "paused",
|
|
@@ -6611,7 +6622,7 @@ ${runLogTail}
|
|
|
6611
6622
|
].filter(Boolean).join(`
|
|
6612
6623
|
|
|
6613
6624
|
`);
|
|
6614
|
-
await postReviewComment(client, card, "decision", body);
|
|
6625
|
+
await postReviewComment(client, card, "decision", body, agentSessionId);
|
|
6615
6626
|
}
|
|
6616
6627
|
await client.endAgentSession(card.id, {
|
|
6617
6628
|
status: "completed",
|
|
@@ -6639,7 +6650,7 @@ ${runLogTail}
|
|
|
6639
6650
|
].filter(Boolean).join(`
|
|
6640
6651
|
|
|
6641
6652
|
`);
|
|
6642
|
-
await postReviewComment(client, card, "blocker", body);
|
|
6653
|
+
await postReviewComment(client, card, "blocker", body, agentSessionId);
|
|
6643
6654
|
await client.endAgentSession(card.id, {
|
|
6644
6655
|
status: "completed",
|
|
6645
6656
|
...buildTokenPayload(sessionStats)
|
|
@@ -6675,7 +6686,7 @@ ${runLogTail}
|
|
|
6675
6686
|
}));
|
|
6676
6687
|
if (linkedFindings.length > 0) {
|
|
6677
6688
|
for (const body2 of buildFindingComments(linkedFindings)) {
|
|
6678
|
-
await postReviewComment(client, card, "finding", body2);
|
|
6689
|
+
await postReviewComment(client, card, "finding", body2, agentSessionId);
|
|
6679
6690
|
}
|
|
6680
6691
|
}
|
|
6681
6692
|
await Promise.all((handbackVerdict.proceed ? minorFindings : []).map(async (finding) => {
|
|
@@ -6704,7 +6715,7 @@ ${runLogTail}
|
|
|
6704
6715
|
].filter(Boolean).join(`
|
|
6705
6716
|
|
|
6706
6717
|
`);
|
|
6707
|
-
await postReviewComment(client, card, "summary", body);
|
|
6718
|
+
await postReviewComment(client, card, "summary", body, agentSessionId);
|
|
6708
6719
|
}
|
|
6709
6720
|
if (handbackVerdict.proceed && config.planning.enabled && card.plan_id) {
|
|
6710
6721
|
try {
|
|
@@ -7727,19 +7738,20 @@ import {
|
|
|
7727
7738
|
buildGateCollectorRegistry,
|
|
7728
7739
|
cleanupWorktree as cleanupWorktree4,
|
|
7729
7740
|
collectGateEvidence,
|
|
7730
|
-
containedEnv as containedEnv2,
|
|
7731
7741
|
DevServerReadinessError,
|
|
7742
|
+
devServerLaunch,
|
|
7732
7743
|
formatDiffSummary,
|
|
7733
7744
|
GIT_NO_HOOKS as GIT_NO_HOOKS7,
|
|
7734
7745
|
implementRunContainmentCliArgs,
|
|
7735
7746
|
log as log24,
|
|
7736
7747
|
probeDevServer,
|
|
7748
|
+
removeSandboxContainer,
|
|
7737
7749
|
resolveStageGate,
|
|
7738
7750
|
secretEnvKeysToStrip,
|
|
7739
7751
|
signalGroup,
|
|
7740
7752
|
spawnInGroup as spawnInGroup2,
|
|
7741
|
-
spawnRunArgs,
|
|
7742
7753
|
terminateGroup,
|
|
7754
|
+
verificationSandbox as verificationSandbox2,
|
|
7743
7755
|
waitForDevServer
|
|
7744
7756
|
} from "@gethmy/harness";
|
|
7745
7757
|
|
|
@@ -7758,6 +7770,7 @@ class ReviewWorker {
|
|
|
7758
7770
|
startedAt = null;
|
|
7759
7771
|
process = null;
|
|
7760
7772
|
devServerProcess = null;
|
|
7773
|
+
devServerContainer = null;
|
|
7761
7774
|
timeoutTimer = null;
|
|
7762
7775
|
heartbeatTimer = null;
|
|
7763
7776
|
progressTracker = null;
|
|
@@ -7868,7 +7881,10 @@ class ReviewWorker {
|
|
|
7868
7881
|
if (crossedTurnCap(before, after, cap)) {
|
|
7869
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.`);
|
|
7870
7883
|
const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
|
|
7871
|
-
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
|
+
});
|
|
7872
7888
|
}
|
|
7873
7889
|
} catch {}
|
|
7874
7890
|
}
|
|
@@ -7979,11 +7995,19 @@ class ReviewWorker {
|
|
|
7979
7995
|
const port = this.reviewPort;
|
|
7980
7996
|
const cwd = this.worktreePath;
|
|
7981
7997
|
log24.info(this.tag, `Starting dev server on port ${port}...`);
|
|
7982
|
-
const
|
|
7983
|
-
|
|
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, {
|
|
7984
8008
|
cwd,
|
|
7985
8009
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7986
|
-
|
|
8010
|
+
stripEnvKeys: secretEnvKeysToStrip()
|
|
7987
8011
|
});
|
|
7988
8012
|
let devServerSpawnError = null;
|
|
7989
8013
|
this.devServerProcess.once("error", (err) => {
|
|
@@ -7997,7 +8021,7 @@ class ReviewWorker {
|
|
|
7997
8021
|
progressPercent: 10
|
|
7998
8022
|
});
|
|
7999
8023
|
if (devServerSpawnError) {
|
|
8000
|
-
throw new DevServerReadinessError(`dev server failed to start (${
|
|
8024
|
+
throw new DevServerReadinessError(`dev server failed to start (${launch.cmd}): ${devServerSpawnError.message}`);
|
|
8001
8025
|
}
|
|
8002
8026
|
await waitForDevServer(this.devServerProcess, 30000);
|
|
8003
8027
|
await probeDevServer(port);
|
|
@@ -8257,7 +8281,8 @@ ${userPrompt}`;
|
|
|
8257
8281
|
let commentId = null;
|
|
8258
8282
|
try {
|
|
8259
8283
|
const res = await this.client.addComment(card.id, body, {
|
|
8260
|
-
commentType: "blocker"
|
|
8284
|
+
commentType: "blocker",
|
|
8285
|
+
agentSessionId: this.sessionId ?? undefined
|
|
8261
8286
|
});
|
|
8262
8287
|
commentId = res?.comment?.id ?? null;
|
|
8263
8288
|
} catch (err) {
|
|
@@ -8382,7 +8407,8 @@ ${userPrompt}`;
|
|
|
8382
8407
|
...implementRunContainmentCliArgs({
|
|
8383
8408
|
worktree: this.worktreePath,
|
|
8384
8409
|
readOnly: true,
|
|
8385
|
-
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
|
|
8386
8412
|
}),
|
|
8387
8413
|
"--",
|
|
8388
8414
|
prompt
|
|
@@ -8500,6 +8526,11 @@ ${userPrompt}`;
|
|
|
8500
8526
|
this.devServerProcess = null;
|
|
8501
8527
|
log24.debug(this.tag, "Killed dev server group");
|
|
8502
8528
|
}
|
|
8529
|
+
if (this.devServerContainer) {
|
|
8530
|
+
const name = this.devServerContainer;
|
|
8531
|
+
this.devServerContainer = null;
|
|
8532
|
+
removeSandboxContainer(name);
|
|
8533
|
+
}
|
|
8503
8534
|
}
|
|
8504
8535
|
cleanup() {
|
|
8505
8536
|
if (this.timeoutTimer) {
|
|
@@ -9105,7 +9136,10 @@ async function dispatchWave(parent, stage, plan, items, deps) {
|
|
|
9105
9136
|
created += 1;
|
|
9106
9137
|
}
|
|
9107
9138
|
if (created > 0 && plan.truncated > 0) {
|
|
9108
|
-
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(() => {});
|
|
9109
9143
|
}
|
|
9110
9144
|
return created;
|
|
9111
9145
|
}
|
|
@@ -9188,14 +9222,17 @@ function runMotorStage(args, deps) {
|
|
|
9188
9222
|
args.repoPath,
|
|
9189
9223
|
"--session",
|
|
9190
9224
|
args.sessionId,
|
|
9191
|
-
...args.metricsPath ? ["--metrics", args.metricsPath] : []
|
|
9225
|
+
...args.metricsPath ? ["--metrics", args.metricsPath] : [],
|
|
9226
|
+
...args.sandboxImage?.trim() ? ["--sandbox-image", args.sandboxImage.trim()] : []
|
|
9192
9227
|
];
|
|
9193
9228
|
return new Promise((resolve2) => {
|
|
9194
9229
|
const child = spawnFn("node", argv, {
|
|
9195
9230
|
env: {
|
|
9196
9231
|
...process.env,
|
|
9197
9232
|
HARMONY_API_URL: motorApiBase(deps.env.apiUrl),
|
|
9198
|
-
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
|
|
9199
9236
|
},
|
|
9200
9237
|
stdio: ["ignore", "pipe", "pipe"]
|
|
9201
9238
|
});
|
|
@@ -9320,6 +9357,26 @@ function runMotorStage(args, deps) {
|
|
|
9320
9357
|
var ABORT_SIGINT_GRACE_MS = 12000, ABORT_SIGTERM_GRACE_MS = 6000;
|
|
9321
9358
|
var init_motor_driver = () => {};
|
|
9322
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
|
+
|
|
9323
9380
|
// src/stage-advance.ts
|
|
9324
9381
|
import { gateConfigErrorReason, log as log29 } from "@gethmy/harness";
|
|
9325
9382
|
function endDispositionFor(outcome) {
|
|
@@ -9390,7 +9447,7 @@ async function holdGateMisconfigured(card, stage, detail, deps) {
|
|
|
9390
9447
|
currentTask: reason
|
|
9391
9448
|
});
|
|
9392
9449
|
} catch {}
|
|
9393
|
-
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 });
|
|
9394
9451
|
log29.info(TAG27, `#${card.short_id} GateMisconfigured: ${reason}`);
|
|
9395
9452
|
return { kind: "held_misconfigured", reason, endDisposition };
|
|
9396
9453
|
}
|
|
@@ -9461,7 +9518,12 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
9461
9518
|
currentTask: reason
|
|
9462
9519
|
});
|
|
9463
9520
|
} catch {}
|
|
9464
|
-
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
|
+
});
|
|
9465
9527
|
log29.info(TAG27, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
9466
9528
|
return { kind: "held_gate_unmet", reason, endDisposition };
|
|
9467
9529
|
}
|
|
@@ -9477,7 +9539,10 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
9477
9539
|
await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
|
|
9478
9540
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
9479
9541
|
try {
|
|
9480
|
-
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
|
+
});
|
|
9481
9546
|
} catch {}
|
|
9482
9547
|
await runTransition(deps.client, guard.card, {
|
|
9483
9548
|
move: { columnName: toColumn },
|
|
@@ -9501,7 +9566,10 @@ ${findings.map((f) => `- [${f.level}] ${f.message}`).join(`
|
|
|
9501
9566
|
decisions: [],
|
|
9502
9567
|
nextStageNeeds: "Address the findings above on the same branch and re-run; this stage repeats until its exit gate passes."
|
|
9503
9568
|
});
|
|
9504
|
-
await deps.client.addComment(card.id, body, {
|
|
9569
|
+
await deps.client.addComment(card.id, body, {
|
|
9570
|
+
commentType: "decision",
|
|
9571
|
+
agentSessionId: deps.agentSessionId ?? undefined
|
|
9572
|
+
});
|
|
9505
9573
|
} catch (err) {
|
|
9506
9574
|
log29.warn(TAG27, `iteration-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
9507
9575
|
}
|
|
@@ -9536,13 +9604,13 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
9536
9604
|
}
|
|
9537
9605
|
if (next.kind === "out_of_range") {
|
|
9538
9606
|
const reason = `Stage advancement aborted: stage index ${stageIndex} is out of range for the pinned playbook version — holding for a human.`;
|
|
9539
|
-
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 });
|
|
9540
9608
|
return { kind: "held_misconfigured", reason, endDisposition };
|
|
9541
9609
|
}
|
|
9542
9610
|
const toColumn = await resolveStageColumnName(deps.client, card, next.stage);
|
|
9543
9611
|
if (!toColumn) {
|
|
9544
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).`;
|
|
9545
|
-
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 });
|
|
9546
9614
|
return { kind: "held_misconfigured", reason, endDisposition };
|
|
9547
9615
|
}
|
|
9548
9616
|
await persistStagePointer(deps.client, card, {
|
|
@@ -9566,7 +9634,12 @@ async function advanceStageOnGate(card, stage, stageIndex, def, evaluation, deps
|
|
|
9566
9634
|
log29.info(TAG27, `#${card.short_id} advanced "${stage.name}" → "${next.stage.name}" (column "${toColumn}")`);
|
|
9567
9635
|
if (next.stage.owner === "human") {
|
|
9568
9636
|
const reason = `Stage "${next.stage.name}" is yours: ${handoffText(next.stage)}`;
|
|
9569
|
-
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
|
+
});
|
|
9570
9643
|
return {
|
|
9571
9644
|
kind: "advanced",
|
|
9572
9645
|
toStageId: next.stage.id,
|
|
@@ -9589,7 +9662,12 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
9589
9662
|
currentTask: reason
|
|
9590
9663
|
});
|
|
9591
9664
|
} catch {}
|
|
9592
|
-
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
|
+
});
|
|
9593
9671
|
log29.info(TAG27, `#${card.short_id} GateUnmetExhausted: ${reason}`);
|
|
9594
9672
|
return { kind: "held_gate_unmet", reason, endDisposition };
|
|
9595
9673
|
}
|
|
@@ -9603,7 +9681,10 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
9603
9681
|
}
|
|
9604
9682
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
9605
9683
|
try {
|
|
9606
|
-
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
|
+
});
|
|
9607
9688
|
} catch {}
|
|
9608
9689
|
await runTransition(deps.client, guard.card, {
|
|
9609
9690
|
move: { columnName: toColumn },
|
|
@@ -9641,7 +9722,10 @@ async function guardStageReclaim(card, what, deps) {
|
|
|
9641
9722
|
}
|
|
9642
9723
|
log29.info(TAG27, `#${card.short_id} ${what} refused — ${detail} (${reason})${released ? "; released the daemon's claim so the stage router stops here" : ""}`);
|
|
9643
9724
|
try {
|
|
9644
|
-
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
|
+
});
|
|
9645
9729
|
} catch {}
|
|
9646
9730
|
return { proceed: false, reason, released };
|
|
9647
9731
|
}
|
|
@@ -9650,7 +9734,10 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
|
|
|
9650
9734
|
await stateStore.decrementAttempt(card.id).catch(() => {});
|
|
9651
9735
|
}
|
|
9652
9736
|
try {
|
|
9653
|
-
await client.addComment(card.id, reason, {
|
|
9737
|
+
await client.addComment(card.id, reason, {
|
|
9738
|
+
commentType: "blocker",
|
|
9739
|
+
agentSessionId: opts.agentSessionId ?? undefined
|
|
9740
|
+
});
|
|
9654
9741
|
} catch {}
|
|
9655
9742
|
try {
|
|
9656
9743
|
await runTransition(client, card, { removeLabels: [AGENT_LABEL] }, { store: stateStore, runId });
|
|
@@ -9701,7 +9788,7 @@ import {
|
|
|
9701
9788
|
spawnInGroup as spawnInGroup4,
|
|
9702
9789
|
teardownWorktree as teardownWorktree2,
|
|
9703
9790
|
terminateGroup as terminateGroup3,
|
|
9704
|
-
verificationSandbox as
|
|
9791
|
+
verificationSandbox as verificationSandbox3,
|
|
9705
9792
|
WorktreeBaseError
|
|
9706
9793
|
} from "@gethmy/harness";
|
|
9707
9794
|
function sdkDraftLogLine(ev) {
|
|
@@ -9818,6 +9905,9 @@ class Worker {
|
|
|
9818
9905
|
runTurns = 0;
|
|
9819
9906
|
chargedCents = 0;
|
|
9820
9907
|
chargedTurns = 0;
|
|
9908
|
+
runToolCalls = 0;
|
|
9909
|
+
noCommitNoticeSettled = false;
|
|
9910
|
+
runExpectsCommits = true;
|
|
9821
9911
|
runLegIndex = 0;
|
|
9822
9912
|
lastRunText = "";
|
|
9823
9913
|
constructor(id, config, client, identity, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
@@ -9921,7 +10011,7 @@ class Worker {
|
|
|
9921
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.`);
|
|
9922
10012
|
try {
|
|
9923
10013
|
const legs = this.stateStore.getRun(this.runId ?? "")?.legs?.length || 1;
|
|
9924
|
-
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 });
|
|
9925
10015
|
} catch (err) {
|
|
9926
10016
|
log30.warn(TAG28, `Could not post the turn-cap notice for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
9927
10017
|
}
|
|
@@ -9939,6 +10029,9 @@ class Worker {
|
|
|
9939
10029
|
this.runTurns = 0;
|
|
9940
10030
|
this.chargedCents = 0;
|
|
9941
10031
|
this.chargedTurns = 0;
|
|
10032
|
+
this.runToolCalls = 0;
|
|
10033
|
+
this.noCommitNoticeSettled = false;
|
|
10034
|
+
this.runExpectsCommits = true;
|
|
9942
10035
|
this.runLegIndex = 0;
|
|
9943
10036
|
this.lastRunText = "";
|
|
9944
10037
|
this.cliSessionId = null;
|
|
@@ -10149,10 +10242,12 @@ class Worker {
|
|
|
10149
10242
|
this.cancel("timeout");
|
|
10150
10243
|
}, this.config.maxTimeout);
|
|
10151
10244
|
this.activeRunSpawnOpts = computeRunSpawnGating(stageCtx.kind === "run" ? stageCtx.allowedTools : null);
|
|
10245
|
+
this.runExpectsCommits = stageCtx.kind === "run" ? stageRunExpectsCommit(stageCtx.stage, stageCtx.allowedTools) : true;
|
|
10152
10246
|
await this.spawnClaude(prompt, card, subtasks, {
|
|
10153
10247
|
model: implementModel,
|
|
10154
10248
|
maxTurns: this.grantedTurns ?? undefined,
|
|
10155
10249
|
resumeSessionId: this.cliSessionId ?? undefined,
|
|
10250
|
+
noCommitNotice: true,
|
|
10156
10251
|
...this.activeRunSpawnOpts ?? {}
|
|
10157
10252
|
});
|
|
10158
10253
|
if (this.aborted)
|
|
@@ -10185,7 +10280,7 @@ class Worker {
|
|
|
10185
10280
|
const onBeforeWorktreeCleanup = stageRun ? async (worktreePath) => {
|
|
10186
10281
|
stageGateEvaluation = await this.collectStageGateEvidence(card, stageRun.stage, worktreePath, subtasks);
|
|
10187
10282
|
} : undefined;
|
|
10188
|
-
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);
|
|
10189
10284
|
if (completed === "park") {
|
|
10190
10285
|
await this.parkForDecision(card, "max_turns");
|
|
10191
10286
|
return;
|
|
@@ -10262,7 +10357,7 @@ class Worker {
|
|
|
10262
10357
|
}
|
|
10263
10358
|
if (this.worktreePath) {
|
|
10264
10359
|
try {
|
|
10265
|
-
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);
|
|
10266
10361
|
} catch {
|
|
10267
10362
|
log30.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
10268
10363
|
}
|
|
@@ -10323,7 +10418,7 @@ class Worker {
|
|
|
10323
10418
|
} else if (this.runId && this.timedOut) {
|
|
10324
10419
|
if (this.worktreePath) {
|
|
10325
10420
|
try {
|
|
10326
|
-
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);
|
|
10327
10422
|
} catch {
|
|
10328
10423
|
log30.warn(this.tag, "Failed to cleanup worktree before requeue");
|
|
10329
10424
|
}
|
|
@@ -10571,7 +10666,8 @@ class Worker {
|
|
|
10571
10666
|
agentId: this.identity.agentId
|
|
10572
10667
|
},
|
|
10573
10668
|
sink: this.cliRunner,
|
|
10574
|
-
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
|
|
10575
10671
|
});
|
|
10576
10672
|
} catch (err) {
|
|
10577
10673
|
const detail = err instanceof Error ? err.message : String(err);
|
|
@@ -10635,7 +10731,10 @@ class Worker {
|
|
|
10635
10731
|
log30.info(this.tag, `Holding #${card.short_id}: ${reason}`);
|
|
10636
10732
|
await this.stateStore.decrementAttempt(card.id);
|
|
10637
10733
|
try {
|
|
10638
|
-
await this.client.addComment(card.id, reason, {
|
|
10734
|
+
await this.client.addComment(card.id, reason, {
|
|
10735
|
+
commentType: "blocker",
|
|
10736
|
+
agentSessionId: this.sessionId ?? undefined
|
|
10737
|
+
});
|
|
10639
10738
|
} catch {}
|
|
10640
10739
|
try {
|
|
10641
10740
|
await runTransition(this.client, card, {
|
|
@@ -10701,9 +10800,16 @@ class Worker {
|
|
|
10701
10800
|
const waitHours = this.config.budget.pause.waitHours;
|
|
10702
10801
|
const until = computeDecisionDeadline(waitHours);
|
|
10703
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
|
+
}
|
|
10704
10809
|
const body = formatBudgetComment({
|
|
10705
10810
|
trigger,
|
|
10706
10811
|
numTurns: stats?.cost?.numTurns ?? 0,
|
|
10812
|
+
hasCommits,
|
|
10707
10813
|
maxTurns: this.effectiveMaxTurns,
|
|
10708
10814
|
toolCalls: stats?.toolCalls ?? 0,
|
|
10709
10815
|
durationMs: stats?.cost?.durationMs ?? 0,
|
|
@@ -10717,7 +10823,8 @@ class Worker {
|
|
|
10717
10823
|
let commentId = null;
|
|
10718
10824
|
try {
|
|
10719
10825
|
const res = await this.client.addComment(card.id, body, {
|
|
10720
|
-
commentType: "blocker"
|
|
10826
|
+
commentType: "blocker",
|
|
10827
|
+
agentSessionId: this.sessionId ?? undefined
|
|
10721
10828
|
});
|
|
10722
10829
|
commentId = res?.comment?.id ?? null;
|
|
10723
10830
|
} catch (err) {
|
|
@@ -10826,7 +10933,8 @@ class Worker {
|
|
|
10826
10933
|
workspaceId: this.workspaceId,
|
|
10827
10934
|
repoPath: worktreePath,
|
|
10828
10935
|
sessionId,
|
|
10829
|
-
metricsPath
|
|
10936
|
+
metricsPath,
|
|
10937
|
+
sandboxImage: this.config.verification.sandboxImage
|
|
10830
10938
|
}, {
|
|
10831
10939
|
env: {
|
|
10832
10940
|
apiUrl: this.client.getApiUrl(),
|
|
@@ -10925,7 +11033,7 @@ class Worker {
|
|
|
10925
11033
|
if (!worktreePath)
|
|
10926
11034
|
return;
|
|
10927
11035
|
try {
|
|
10928
|
-
await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined);
|
|
11036
|
+
await teardownWorktree2(this.client, card.id, worktreePath, this.branchName ?? undefined, this.sessionId);
|
|
10929
11037
|
} catch {
|
|
10930
11038
|
log30.warn(this.tag, "Failed to cleanup worktree after the motor stage run");
|
|
10931
11039
|
}
|
|
@@ -11000,7 +11108,10 @@ ${prompt}`;
|
|
|
11000
11108
|
decisions: [],
|
|
11001
11109
|
nextStageNeeds: "Pick up from the produced artifact above; treat the recorded decisions as settled."
|
|
11002
11110
|
});
|
|
11003
|
-
await this.client.addComment(card.id, body, {
|
|
11111
|
+
await this.client.addComment(card.id, body, {
|
|
11112
|
+
commentType: "decision",
|
|
11113
|
+
agentSessionId: this.sessionId ?? undefined
|
|
11114
|
+
});
|
|
11004
11115
|
log30.info(this.tag, `Wrote stage handoff for #${card.short_id} (stage "${stage.name}")`);
|
|
11005
11116
|
} catch (err) {
|
|
11006
11117
|
log30.warn(this.tag, `stage-handoff write failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -11027,7 +11138,7 @@ ${prompt}`;
|
|
|
11027
11138
|
worktreePath,
|
|
11028
11139
|
buildTimeout: this.config.verification.timeout,
|
|
11029
11140
|
lintTimeout: this.config.verification.timeout,
|
|
11030
|
-
sandbox:
|
|
11141
|
+
sandbox: verificationSandbox3(this.config)
|
|
11031
11142
|
},
|
|
11032
11143
|
artifact: {
|
|
11033
11144
|
worktreePath,
|
|
@@ -11067,7 +11178,8 @@ ${prompt}`;
|
|
|
11067
11178
|
maxAttempts: this.config.budget.maxAttemptsPerCard,
|
|
11068
11179
|
fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
|
|
11069
11180
|
sink: this.cliRunner,
|
|
11070
|
-
runId: this.runId ?? undefined
|
|
11181
|
+
runId: this.runId ?? undefined,
|
|
11182
|
+
agentSessionId: this.sessionId
|
|
11071
11183
|
});
|
|
11072
11184
|
} catch (err) {
|
|
11073
11185
|
log30.warn(this.tag, `stage advancement failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
@@ -11129,6 +11241,44 @@ ${prompt}`;
|
|
|
11129
11241
|
}
|
|
11130
11242
|
});
|
|
11131
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
|
+
}
|
|
11132
11282
|
recordRunSized() {
|
|
11133
11283
|
if (!this.modelChoice)
|
|
11134
11284
|
return;
|
|
@@ -11161,7 +11311,8 @@ ${prompt}`;
|
|
|
11161
11311
|
try {
|
|
11162
11312
|
const body = buildGaveUpComment(max, this.stateStore.getRecentFailures(cardId, 3), this.config.budget.pause.enabled);
|
|
11163
11313
|
const res = await this.client.addComment(cardId, body, {
|
|
11164
|
-
commentType: "blocker"
|
|
11314
|
+
commentType: "blocker",
|
|
11315
|
+
agentSessionId: this.sessionId ?? undefined
|
|
11165
11316
|
});
|
|
11166
11317
|
giveUpCommentId = res?.comment?.id ?? null;
|
|
11167
11318
|
log30.warn(this.tag, `gave up on ${cardId} after ${attempts} attempts`);
|
|
@@ -11476,6 +11627,7 @@ ${prompt}`;
|
|
|
11476
11627
|
model: this.selectImplementModel(card),
|
|
11477
11628
|
maxTurns: STEERING_MAX_TURNS,
|
|
11478
11629
|
resumeSessionId: this.cliSessionId,
|
|
11630
|
+
noCommitNotice: true,
|
|
11479
11631
|
...this.activeRunSpawnOpts ?? {}
|
|
11480
11632
|
});
|
|
11481
11633
|
} catch (err) {
|
|
@@ -11508,7 +11660,8 @@ ${prompt}`;
|
|
|
11508
11660
|
...this.config.claude.additionalArgs,
|
|
11509
11661
|
...implementRunContainmentCliArgs2({
|
|
11510
11662
|
worktree: this.worktreePath,
|
|
11511
|
-
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
|
|
11512
11665
|
}),
|
|
11513
11666
|
"--",
|
|
11514
11667
|
prompt
|
|
@@ -11547,6 +11700,11 @@ ${prompt}`;
|
|
|
11547
11700
|
this.lastRunText += content;
|
|
11548
11701
|
this.captureCliSessionId(parser.sessionId);
|
|
11549
11702
|
});
|
|
11703
|
+
if (opts.noCommitNotice) {
|
|
11704
|
+
parser.on("tool_start", () => {
|
|
11705
|
+
this.countToolCallForNoCommitNotice();
|
|
11706
|
+
});
|
|
11707
|
+
}
|
|
11550
11708
|
parser.on("parse_error", (msg) => {
|
|
11551
11709
|
log30.debug(this.tag, `Stream parse error (non-fatal): ${msg}`);
|
|
11552
11710
|
runLog?.stream.write(`
|
|
@@ -11636,7 +11794,8 @@ ${prompt}`;
|
|
|
11636
11794
|
maxBudgetUsd: sdkCfg?.maxBudgetUsd,
|
|
11637
11795
|
...implementRunContainment({
|
|
11638
11796
|
worktree: this.worktreePath,
|
|
11639
|
-
extraDisallowedTools: disallowedTools
|
|
11797
|
+
extraDisallowedTools: disallowedTools,
|
|
11798
|
+
run: this.sessionId ? { cardId: card.id, agentSessionId: this.sessionId } : undefined
|
|
11640
11799
|
}),
|
|
11641
11800
|
onSpawn: (child) => {
|
|
11642
11801
|
this.process = child;
|
|
@@ -11657,6 +11816,9 @@ ${prompt}`;
|
|
|
11657
11816
|
try {
|
|
11658
11817
|
for await (const ev of stream) {
|
|
11659
11818
|
this.progressTracker?.ingest(ev);
|
|
11819
|
+
if (opts.noCommitNotice && ev.kind === "tool_started") {
|
|
11820
|
+
this.countToolCallForNoCommitNotice();
|
|
11821
|
+
}
|
|
11660
11822
|
if (ev.kind === "assistant_text") {
|
|
11661
11823
|
this.lastRunText += `${ev.payload.text}
|
|
11662
11824
|
`;
|
|
@@ -11740,7 +11902,7 @@ ${prompt}`;
|
|
|
11740
11902
|
}
|
|
11741
11903
|
if (this.worktreePath && this.state !== "parked" && (this.state === "error" || this.timedOut || this.aborted)) {
|
|
11742
11904
|
try {
|
|
11743
|
-
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);
|
|
11744
11906
|
} catch {
|
|
11745
11907
|
log30.warn(this.tag, "Failed to cleanup worktree");
|
|
11746
11908
|
}
|
|
@@ -12256,7 +12418,10 @@ class Pool {
|
|
|
12256
12418
|
try {
|
|
12257
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.
|
|
12258
12420
|
|
|
12259
|
-
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
|
+
});
|
|
12260
12425
|
} catch (err) {
|
|
12261
12426
|
log31.warn(TAG29, `Could not explain the refused Continue on ${run.cardId}: ${err instanceof Error ? err.message : err}`);
|
|
12262
12427
|
}
|
|
@@ -13243,8 +13408,139 @@ var init_sweep = __esm(() => {
|
|
|
13243
13408
|
init_unblock();
|
|
13244
13409
|
});
|
|
13245
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
|
+
|
|
13246
13542
|
// src/reconcile.ts
|
|
13247
|
-
import { detectGitProvider as detectGitProvider5, log as
|
|
13543
|
+
import { detectGitProvider as detectGitProvider5, log as log38 } from "@gethmy/harness";
|
|
13248
13544
|
function daemonProcessLabels(approvedLabel) {
|
|
13249
13545
|
const names = ["agent", "agent-recovered", NEED_REVIEW_LABEL, approvedLabel];
|
|
13250
13546
|
return new Set(names.filter(Boolean).map((n) => n.toLowerCase()));
|
|
@@ -13274,6 +13570,7 @@ class Reconciler {
|
|
|
13274
13570
|
gitProvider = null;
|
|
13275
13571
|
repoDirectories = null;
|
|
13276
13572
|
lastSweepStopReported = null;
|
|
13573
|
+
publishSweep = () => {};
|
|
13277
13574
|
get lastTick() {
|
|
13278
13575
|
return this.lastTickAt;
|
|
13279
13576
|
}
|
|
@@ -13303,7 +13600,7 @@ class Reconciler {
|
|
|
13303
13600
|
clearInterval(this.timer);
|
|
13304
13601
|
this.timer = null;
|
|
13305
13602
|
}
|
|
13306
|
-
|
|
13603
|
+
log38.info(TAG36, "Heartbeat stopped");
|
|
13307
13604
|
}
|
|
13308
13605
|
async recoverStaleRuns() {
|
|
13309
13606
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -13314,7 +13611,7 @@ class Reconciler {
|
|
|
13314
13611
|
const pool = this.pool;
|
|
13315
13612
|
for (const run of active) {
|
|
13316
13613
|
if (isBudgetHeldRun(run)) {
|
|
13317
|
-
|
|
13614
|
+
log38.info(TAG36, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
13318
13615
|
continue;
|
|
13319
13616
|
}
|
|
13320
13617
|
const foreignDaemon = run.daemonPid !== process.pid;
|
|
@@ -13324,7 +13621,7 @@ class Reconciler {
|
|
|
13324
13621
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
13325
13622
|
continue;
|
|
13326
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`;
|
|
13327
|
-
|
|
13624
|
+
log38.warn(TAG36, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
13328
13625
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
13329
13626
|
runId: run.runId,
|
|
13330
13627
|
cardId: run.cardId,
|
|
@@ -13351,11 +13648,11 @@ class Reconciler {
|
|
|
13351
13648
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
13352
13649
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
13353
13650
|
continue;
|
|
13354
|
-
|
|
13651
|
+
log38.warn(TAG36, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
13355
13652
|
try {
|
|
13356
13653
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
13357
13654
|
} catch (err) {
|
|
13358
|
-
|
|
13655
|
+
log38.error(TAG36, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
13359
13656
|
}
|
|
13360
13657
|
}
|
|
13361
13658
|
}
|
|
@@ -13387,7 +13684,7 @@ class Reconciler {
|
|
|
13387
13684
|
return;
|
|
13388
13685
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
13389
13686
|
const subtasks = card.subtasks ?? [];
|
|
13390
|
-
|
|
13687
|
+
log38.info(TAG36, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
|
|
13391
13688
|
await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
|
|
13392
13689
|
}
|
|
13393
13690
|
});
|
|
@@ -13411,11 +13708,11 @@ class Reconciler {
|
|
|
13411
13708
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
13412
13709
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
13413
13710
|
continue;
|
|
13414
|
-
|
|
13711
|
+
log38.warn(TAG36, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
13415
13712
|
try {
|
|
13416
13713
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
13417
13714
|
} catch (err) {
|
|
13418
|
-
|
|
13715
|
+
log38.error(TAG36, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
13419
13716
|
}
|
|
13420
13717
|
}
|
|
13421
13718
|
}
|
|
@@ -13443,13 +13740,14 @@ class Reconciler {
|
|
|
13443
13740
|
now: Date.now()
|
|
13444
13741
|
};
|
|
13445
13742
|
const verdict = this.sweepGuard?.check() ?? { claiming: true };
|
|
13743
|
+
this.publishSweepState();
|
|
13446
13744
|
if (!verdict.claiming) {
|
|
13447
13745
|
this.reportSweepStopped(verdict.reason, verdict.detail);
|
|
13448
13746
|
return;
|
|
13449
13747
|
}
|
|
13450
13748
|
this.lastSweepStopReported = null;
|
|
13451
13749
|
if (!this.pool.hasFreeImplementSlot()) {
|
|
13452
|
-
|
|
13750
|
+
log38.debug(TAG36, "sweep skipped — no free implement slot");
|
|
13453
13751
|
return;
|
|
13454
13752
|
}
|
|
13455
13753
|
const { claimed } = await sweepForCard({
|
|
@@ -13462,25 +13760,32 @@ class Reconciler {
|
|
|
13462
13760
|
if (!claimed)
|
|
13463
13761
|
return;
|
|
13464
13762
|
await this.sweepGuard?.recordClaim(claimed.card.id);
|
|
13763
|
+
this.publishSweepState();
|
|
13465
13764
|
await this.pool.enqueue(claimed.card, claimed.column, claimed.labels, claimed.card.subtasks ?? [], "implement");
|
|
13466
13765
|
}
|
|
13467
13766
|
reportSweepStopped(reason, detail) {
|
|
13468
13767
|
const line = `sweep stopped claiming — ${detail}`;
|
|
13469
13768
|
if (this.lastSweepStopReported === reason)
|
|
13470
|
-
|
|
13769
|
+
log38.debug(TAG36, line);
|
|
13471
13770
|
else
|
|
13472
|
-
|
|
13771
|
+
log38.warn(TAG36, line);
|
|
13473
13772
|
this.lastSweepStopReported = reason;
|
|
13474
13773
|
}
|
|
13774
|
+
setSweepPresenceSink(sink) {
|
|
13775
|
+
this.publishSweep = sink;
|
|
13776
|
+
}
|
|
13777
|
+
publishSweepState() {
|
|
13778
|
+
this.publishSweep(toSweepPresence(this.sweepGuard?.snapshot()));
|
|
13779
|
+
}
|
|
13475
13780
|
resolveRepoDirectories() {
|
|
13476
13781
|
if (this.repoDirectories)
|
|
13477
13782
|
return this.repoDirectories;
|
|
13478
13783
|
const dirs = readRepoDirectories(process.cwd());
|
|
13479
13784
|
this.repoDirectories = dirs;
|
|
13480
13785
|
if (dirs.size === 0) {
|
|
13481
|
-
|
|
13786
|
+
log38.warn(TAG36, `Could not read top-level directories of ${process.cwd()} — sweep cohesion runs without its path signal`);
|
|
13482
13787
|
} else {
|
|
13483
|
-
|
|
13788
|
+
log38.debug(TAG36, `Path signal allows ${dirs.size} top-level directories`);
|
|
13484
13789
|
}
|
|
13485
13790
|
return dirs;
|
|
13486
13791
|
}
|
|
@@ -13524,21 +13829,21 @@ class Reconciler {
|
|
|
13524
13829
|
const subtasks = card.subtasks ?? [];
|
|
13525
13830
|
const mode = route.mode;
|
|
13526
13831
|
if (route.stage) {
|
|
13527
|
-
|
|
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`);
|
|
13528
13833
|
}
|
|
13529
13834
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
13530
|
-
|
|
13835
|
+
log38.debug(TAG36, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
13531
13836
|
continue;
|
|
13532
13837
|
}
|
|
13533
13838
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
13534
|
-
|
|
13839
|
+
log38.debug(TAG36, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
13535
13840
|
continue;
|
|
13536
13841
|
}
|
|
13537
13842
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
13538
|
-
|
|
13843
|
+
log38.debug(TAG36, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
13539
13844
|
continue;
|
|
13540
13845
|
}
|
|
13541
|
-
|
|
13846
|
+
log38.info(TAG36, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
13542
13847
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
13543
13848
|
}
|
|
13544
13849
|
}
|
|
@@ -13548,13 +13853,13 @@ class Reconciler {
|
|
|
13548
13853
|
try {
|
|
13549
13854
|
await this.pool.drainBudgetDecisions();
|
|
13550
13855
|
} catch (err) {
|
|
13551
|
-
|
|
13856
|
+
log38.error(TAG36, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
13552
13857
|
}
|
|
13553
13858
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
13554
13859
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
13555
13860
|
for (const knownId of knownCardIds) {
|
|
13556
13861
|
if (!allAgentCardIds.has(knownId)) {
|
|
13557
|
-
|
|
13862
|
+
log38.info(TAG36, `Missed unassign: ${knownId} — removing`);
|
|
13558
13863
|
await this.pool.removeCard(knownId);
|
|
13559
13864
|
}
|
|
13560
13865
|
}
|
|
@@ -13562,15 +13867,15 @@ class Reconciler {
|
|
|
13562
13867
|
try {
|
|
13563
13868
|
await this.sweepForWork(cards, columns, labelMap, pickupConfig);
|
|
13564
13869
|
} catch (err) {
|
|
13565
|
-
|
|
13870
|
+
log38.error(TAG36, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
|
|
13566
13871
|
}
|
|
13567
|
-
|
|
13872
|
+
log38.debug(TAG36, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
13568
13873
|
} catch (err) {
|
|
13569
|
-
|
|
13874
|
+
log38.error(TAG36, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
13570
13875
|
}
|
|
13571
13876
|
}
|
|
13572
13877
|
}
|
|
13573
|
-
var
|
|
13878
|
+
var TAG36 = "reconcile";
|
|
13574
13879
|
var init_reconcile = __esm(() => {
|
|
13575
13880
|
init_board_helpers();
|
|
13576
13881
|
init_recovery();
|
|
@@ -13579,6 +13884,7 @@ var init_reconcile = __esm(() => {
|
|
|
13579
13884
|
init_state_store();
|
|
13580
13885
|
init_strand_recovery();
|
|
13581
13886
|
init_sweep();
|
|
13887
|
+
init_sweep_guard();
|
|
13582
13888
|
init_types2();
|
|
13583
13889
|
});
|
|
13584
13890
|
|
|
@@ -13587,7 +13893,7 @@ var exports_startup_banner = {};
|
|
|
13587
13893
|
__export(exports_startup_banner, {
|
|
13588
13894
|
createStartupBanner: () => createStartupBanner
|
|
13589
13895
|
});
|
|
13590
|
-
import { isPretty, log as
|
|
13896
|
+
import { isPretty, log as log39 } from "@gethmy/harness";
|
|
13591
13897
|
function createStartupBanner(config, version) {
|
|
13592
13898
|
return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
|
|
13593
13899
|
}
|
|
@@ -13612,7 +13918,7 @@ function prettyBanner(config, version) {
|
|
|
13612
13918
|
checks.push({ kind: "ok", message });
|
|
13613
13919
|
},
|
|
13614
13920
|
warn(message) {
|
|
13615
|
-
|
|
13921
|
+
log39.warn(TAG37, message);
|
|
13616
13922
|
checks.push({ kind: "warn", message: message.split(`
|
|
13617
13923
|
`, 1)[0] });
|
|
13618
13924
|
},
|
|
@@ -13637,25 +13943,25 @@ function prettyBanner(config, version) {
|
|
|
13637
13943
|
};
|
|
13638
13944
|
}
|
|
13639
13945
|
function jsonBanner(config, version) {
|
|
13640
|
-
|
|
13641
|
-
|
|
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(", ")}`);
|
|
13642
13948
|
if (config.agent.review.enabled) {
|
|
13643
|
-
|
|
13949
|
+
log39.info(TAG37, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
13644
13950
|
}
|
|
13645
13951
|
let failed = false;
|
|
13646
13952
|
return {
|
|
13647
13953
|
setProjectName(_name) {},
|
|
13648
13954
|
setGitProvider(provider) {
|
|
13649
|
-
|
|
13955
|
+
log39.info(TAG37, `Git provider: ${provider}`);
|
|
13650
13956
|
},
|
|
13651
13957
|
setHttpPort(port) {
|
|
13652
|
-
|
|
13958
|
+
log39.info(TAG37, `HTTP server on port ${port}`);
|
|
13653
13959
|
},
|
|
13654
13960
|
check(message) {
|
|
13655
|
-
|
|
13961
|
+
log39.info(TAG37, message);
|
|
13656
13962
|
},
|
|
13657
13963
|
warn(message) {
|
|
13658
|
-
|
|
13964
|
+
log39.warn(TAG37, message);
|
|
13659
13965
|
},
|
|
13660
13966
|
fail() {
|
|
13661
13967
|
failed = true;
|
|
@@ -13663,7 +13969,7 @@ function jsonBanner(config, version) {
|
|
|
13663
13969
|
async ready(message) {
|
|
13664
13970
|
if (failed)
|
|
13665
13971
|
return;
|
|
13666
|
-
|
|
13972
|
+
log39.info(TAG37, message);
|
|
13667
13973
|
}
|
|
13668
13974
|
};
|
|
13669
13975
|
}
|
|
@@ -13744,7 +14050,7 @@ function cyan(s) {
|
|
|
13744
14050
|
function yellow(s) {
|
|
13745
14051
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
13746
14052
|
}
|
|
13747
|
-
var
|
|
14053
|
+
var TAG37 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
13748
14054
|
var init_startup_banner = __esm(() => {
|
|
13749
14055
|
ANSI = {
|
|
13750
14056
|
reset: "\x1B[0m",
|
|
@@ -13845,133 +14151,27 @@ var init_stream_parser_selftest = __esm(() => {
|
|
|
13845
14151
|
init_stream_parser();
|
|
13846
14152
|
});
|
|
13847
14153
|
|
|
13848
|
-
// src/sweep-guard.ts
|
|
13849
|
-
import { log as log39 } from "@gethmy/harness";
|
|
13850
|
-
|
|
13851
|
-
class SweepGuard {
|
|
13852
|
-
config;
|
|
13853
|
-
store;
|
|
13854
|
-
checkDailyBudget;
|
|
13855
|
-
constructor(config, store, checkDailyBudget) {
|
|
13856
|
-
this.config = config;
|
|
13857
|
-
this.store = store;
|
|
13858
|
-
this.checkDailyBudget = checkDailyBudget;
|
|
13859
|
-
}
|
|
13860
|
-
check() {
|
|
13861
|
-
const sweep = this.store.getSweep();
|
|
13862
|
-
if (sweep.haltReason) {
|
|
13863
|
-
return {
|
|
13864
|
-
claiming: false,
|
|
13865
|
-
reason: sweep.haltReason,
|
|
13866
|
-
detail: this.describeStop(sweep.haltReason),
|
|
13867
|
-
latched: true
|
|
13868
|
-
};
|
|
13869
|
-
}
|
|
13870
|
-
const cap = this.config.maxCardsPerSweep;
|
|
13871
|
-
if (cap >= 0 && sweep.claimed >= cap) {
|
|
13872
|
-
return {
|
|
13873
|
-
claiming: false,
|
|
13874
|
-
reason: "card_cap",
|
|
13875
|
-
detail: this.describeStop("card_cap"),
|
|
13876
|
-
latched: true
|
|
13877
|
-
};
|
|
13878
|
-
}
|
|
13879
|
-
const daily = this.checkDailyBudget?.();
|
|
13880
|
-
if (daily && !daily.allow) {
|
|
13881
|
-
return {
|
|
13882
|
-
claiming: false,
|
|
13883
|
-
latched: false,
|
|
13884
|
-
reason: "daily_budget",
|
|
13885
|
-
detail: this.describeStop("daily_budget", daily.detail)
|
|
13886
|
-
};
|
|
13887
|
-
}
|
|
13888
|
-
return { claiming: true };
|
|
13889
|
-
}
|
|
13890
|
-
async recordClaim(cardId) {
|
|
13891
|
-
await this.store.recordSweepClaim(cardId);
|
|
13892
|
-
const verdict = this.check();
|
|
13893
|
-
if (!verdict.claiming && verdict.latched)
|
|
13894
|
-
await this.halt(verdict.reason);
|
|
13895
|
-
return verdict;
|
|
13896
|
-
}
|
|
13897
|
-
lastClaimedCardId() {
|
|
13898
|
-
return this.store.getSweep().lastClaimedCardId ?? null;
|
|
13899
|
-
}
|
|
13900
|
-
async halt(reason) {
|
|
13901
|
-
const already = this.store.getSweep().haltReason === reason;
|
|
13902
|
-
await this.store.haltSweep(reason);
|
|
13903
|
-
if (!already) {
|
|
13904
|
-
const claimed = this.store.getSweep().claimed;
|
|
13905
|
-
log39.info(TAG37, `claiming stopped (${reason}) after ${claimed} claimed`);
|
|
13906
|
-
}
|
|
13907
|
-
return this.snapshot();
|
|
13908
|
-
}
|
|
13909
|
-
async resume() {
|
|
13910
|
-
const before = this.store.getSweep();
|
|
13911
|
-
const was = before.haltReason ? this.describeStop(before.haltReason) : null;
|
|
13912
|
-
await this.store.resumeSweep();
|
|
13913
|
-
if (was)
|
|
13914
|
-
log39.info(TAG37, `claiming resumed by the operator — was stopped: ${was}`);
|
|
13915
|
-
return this.snapshot();
|
|
13916
|
-
}
|
|
13917
|
-
snapshot() {
|
|
13918
|
-
const sweep = this.store.getSweep();
|
|
13919
|
-
const cap = this.config.maxCardsPerSweep;
|
|
13920
|
-
const verdict = this.check();
|
|
13921
|
-
return {
|
|
13922
|
-
enabled: this.config.enabled,
|
|
13923
|
-
claiming: verdict.claiming,
|
|
13924
|
-
haltReason: verdict.claiming ? null : verdict.reason,
|
|
13925
|
-
haltedAt: sweep.haltedAt,
|
|
13926
|
-
claimed: sweep.claimed,
|
|
13927
|
-
maxCardsPerSweep: cap < 0 ? null : cap,
|
|
13928
|
-
totalClaimed: sweep.totalClaimed,
|
|
13929
|
-
detail: verdict.claiming ? null : verdict.detail
|
|
13930
|
-
};
|
|
13931
|
-
}
|
|
13932
|
-
describeStop(reason, budgetDetail) {
|
|
13933
|
-
const claimed = this.store.getSweep().claimed;
|
|
13934
|
-
const cards = `${claimed} card${claimed === 1 ? "" : "s"}`;
|
|
13935
|
-
const resume = "Resume with `harmony-agent sweep resume`.";
|
|
13936
|
-
switch (reason) {
|
|
13937
|
-
case "card_cap":
|
|
13938
|
-
return `card cap reached — ${cards} claimed this sweep, cap is ${this.config.maxCardsPerSweep}. ${resume}`;
|
|
13939
|
-
case "operator":
|
|
13940
|
-
return `stopped by the operator after ${cards}. ${resume}`;
|
|
13941
|
-
case "daily_budget":
|
|
13942
|
-
return `daily spend cap reached${budgetDetail ? ` (${budgetDetail})` : ""} after ${cards} — claiming starts again on its own when the UTC day rolls over.`;
|
|
13943
|
-
}
|
|
13944
|
-
}
|
|
13945
|
-
}
|
|
13946
|
-
function describeCaps(sweep, dailyBudgetCents) {
|
|
13947
|
-
const cards = sweep.maxCardsPerSweep < 0 ? "no cap" : String(sweep.maxCardsPerSweep);
|
|
13948
|
-
return `cards/sweep ${cards} · spend/day ${formatDailyCap(dailyBudgetCents)}`;
|
|
13949
|
-
}
|
|
13950
|
-
function sweepBannerLine(config) {
|
|
13951
|
-
const sweep = config.sweep;
|
|
13952
|
-
const scope = sweep.requireLabel ? `cards labelled "${sweep.requireLabel}"` : "unassigned cards";
|
|
13953
|
-
const extra = sweep.trustedAuthors.length;
|
|
13954
|
-
const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
|
|
13955
|
-
return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
|
|
13956
|
-
}
|
|
13957
|
-
var TAG37 = "sweep";
|
|
13958
|
-
var init_sweep_guard = () => {};
|
|
13959
|
-
|
|
13960
14154
|
// src/watcher.ts
|
|
13961
14155
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
13962
14156
|
import { isPretty as isPretty2, log as log40 } from "@gethmy/harness";
|
|
13963
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
|
+
}
|
|
13964
14161
|
|
|
13965
14162
|
class Watcher {
|
|
13966
14163
|
credentials;
|
|
13967
14164
|
projectId;
|
|
13968
14165
|
identity;
|
|
14166
|
+
routing;
|
|
13969
14167
|
onCardBroadcast;
|
|
13970
14168
|
onAgentCommand;
|
|
13971
14169
|
channel = null;
|
|
13972
14170
|
presenceChannel = null;
|
|
13973
14171
|
supabase = null;
|
|
13974
14172
|
daemonId = randomUUID3();
|
|
14173
|
+
startedAt = new Date().toISOString();
|
|
14174
|
+
sweep = null;
|
|
13975
14175
|
connected = false;
|
|
13976
14176
|
presenceTracked = false;
|
|
13977
14177
|
suppressStartupLogs = true;
|
|
@@ -14016,10 +14216,11 @@ class Watcher {
|
|
|
14016
14216
|
log40.warn(TAG38, `removeChannel(${old.topic}) returned "${status}" and the client still holds that topic — retrying the removal`);
|
|
14017
14217
|
return false;
|
|
14018
14218
|
}
|
|
14019
|
-
constructor(credentials, projectId, identity, onCardBroadcast, onAgentCommand) {
|
|
14219
|
+
constructor(credentials, projectId, identity, routing, onCardBroadcast, onAgentCommand) {
|
|
14020
14220
|
this.credentials = credentials;
|
|
14021
14221
|
this.projectId = projectId;
|
|
14022
14222
|
this.identity = identity;
|
|
14223
|
+
this.routing = routing;
|
|
14023
14224
|
this.onCardBroadcast = onCardBroadcast;
|
|
14024
14225
|
this.onAgentCommand = onAgentCommand;
|
|
14025
14226
|
}
|
|
@@ -14031,6 +14232,42 @@ class Watcher {
|
|
|
14031
14232
|
this.subscribeBroadcast();
|
|
14032
14233
|
this.subscribePresence();
|
|
14033
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
|
+
}
|
|
14034
14271
|
subscribePresence() {
|
|
14035
14272
|
if (!this.supabase)
|
|
14036
14273
|
return;
|
|
@@ -14045,15 +14282,7 @@ class Watcher {
|
|
|
14045
14282
|
if (status === "SUBSCRIBED") {
|
|
14046
14283
|
let trackStatus;
|
|
14047
14284
|
try {
|
|
14048
|
-
trackStatus = await presenceChannel.track(
|
|
14049
|
-
daemonId: this.daemonId,
|
|
14050
|
-
startedAt: new Date().toISOString(),
|
|
14051
|
-
userId: this.identity.userId,
|
|
14052
|
-
agentId: this.identity.agentId,
|
|
14053
|
-
userEmail: this.identity.userEmail,
|
|
14054
|
-
agentIdentifier: this.identity.agentIdentifier,
|
|
14055
|
-
agentName: this.identity.agentName
|
|
14056
|
-
});
|
|
14285
|
+
trackStatus = await presenceChannel.track(this.presencePayload());
|
|
14057
14286
|
} catch (err) {
|
|
14058
14287
|
trackStatus = `error (${String(err)})`;
|
|
14059
14288
|
}
|
|
@@ -14214,8 +14443,10 @@ class Watcher {
|
|
|
14214
14443
|
log40.info(TAG38, "Broadcast subscription stopped");
|
|
14215
14444
|
}
|
|
14216
14445
|
}
|
|
14217
|
-
var TAG38 = "watcher";
|
|
14218
|
-
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
|
+
});
|
|
14219
14450
|
|
|
14220
14451
|
// src/worktree-gc.ts
|
|
14221
14452
|
var exports_worktree_gc = {};
|
|
@@ -14755,7 +14986,11 @@ async function main() {
|
|
|
14755
14986
|
};
|
|
14756
14987
|
},
|
|
14757
14988
|
handleCommand: (cmd, cardId) => pool.handleAgentCommand(cardId, cmd),
|
|
14758
|
-
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
|
+
},
|
|
14759
14994
|
getSweep: () => sweepGuard.snapshot(),
|
|
14760
14995
|
handleAccountResume: () => accountGuard.resume(),
|
|
14761
14996
|
getAccount: () => accountGuard.snapshot()
|
|
@@ -14766,6 +15001,9 @@ async function main() {
|
|
|
14766
15001
|
userEmail: config.userEmail,
|
|
14767
15002
|
agentIdentifier: config.agentIdentifier,
|
|
14768
15003
|
agentName: config.agentName
|
|
15004
|
+
}, {
|
|
15005
|
+
pickupColumns: config.agent.pickupColumns,
|
|
15006
|
+
reviewColumns
|
|
14769
15007
|
}, async (event) => {
|
|
14770
15008
|
await handleBroadcast(event, client, pool, config, agentId);
|
|
14771
15009
|
}, async (command) => {
|
|
@@ -14805,6 +15043,7 @@ async function main() {
|
|
|
14805
15043
|
shutdown("unhandledRejection");
|
|
14806
15044
|
});
|
|
14807
15045
|
await watcher.start();
|
|
15046
|
+
reconciler.setSweepPresenceSink((state) => watcher.publishSweep(state));
|
|
14808
15047
|
reconciler.start();
|
|
14809
15048
|
mergeMonitor?.start();
|
|
14810
15049
|
worktreeGc.start();
|