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