@gethmy/agent 1.28.2 → 1.29.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/README.md +14 -5
- package/dist/cli.js +987 -160
- package/dist/index.js +929 -157
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -583,6 +583,68 @@ var init_board_reviewer = __esm(() => {
|
|
|
583
583
|
init_board_review();
|
|
584
584
|
});
|
|
585
585
|
|
|
586
|
+
// src/budget.ts
|
|
587
|
+
function usd(cents) {
|
|
588
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
589
|
+
}
|
|
590
|
+
function formatDailyCap(cents) {
|
|
591
|
+
return cents < 0 ? "no cap" : usd(cents);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
class BudgetGuard {
|
|
595
|
+
config;
|
|
596
|
+
store;
|
|
597
|
+
constructor(config, store) {
|
|
598
|
+
this.config = config;
|
|
599
|
+
this.store = store;
|
|
600
|
+
}
|
|
601
|
+
check(cardId) {
|
|
602
|
+
const card = this.store.getCard(cardId);
|
|
603
|
+
if (card && card.attempts >= this.config.maxAttemptsPerCard) {
|
|
604
|
+
return {
|
|
605
|
+
allow: false,
|
|
606
|
+
reason: "max_attempts",
|
|
607
|
+
detail: `${card.attempts} of ${this.config.maxAttemptsPerCard} attempts exhausted`
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
return this.checkDailyBudget();
|
|
611
|
+
}
|
|
612
|
+
checkDailyBudget() {
|
|
613
|
+
const cap = this.config.dailyBudgetCents;
|
|
614
|
+
if (cap < 0)
|
|
615
|
+
return { allow: true };
|
|
616
|
+
const spentCents = this.store.getDailyCostCents();
|
|
617
|
+
if (spentCents < cap)
|
|
618
|
+
return { allow: true };
|
|
619
|
+
return {
|
|
620
|
+
allow: false,
|
|
621
|
+
reason: "daily_budget",
|
|
622
|
+
detail: `${usd(spentCents)} of ${usd(cap)} spent today (UTC)`
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
627
|
+
const wayBackIn = pauseEnabled ? "Continue to grant a fresh attempt, or reassign the card." : "Reassign the card to grant a fresh attempt.";
|
|
628
|
+
const lines = [
|
|
629
|
+
"**Out of attempts — over to you.**",
|
|
630
|
+
`Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}. ${wayBackIn}`
|
|
631
|
+
];
|
|
632
|
+
if (failures.length > 0) {
|
|
633
|
+
lines.push("", "Recent failures:");
|
|
634
|
+
for (const f of failures) {
|
|
635
|
+
const when = new Date(f.ts).toISOString().replace("T", " ").slice(0, 16);
|
|
636
|
+
const tag = f.reason ? ` [${f.reason}]` : "";
|
|
637
|
+
const branch = f.recoveryBranch ? `
|
|
638
|
+
recover: \`git fetch && git checkout ${f.recoveryBranch}\`` : "";
|
|
639
|
+
lines.push(`- ${when} UTC${tag} — ${f.summary}${branch}`);
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
lines.push("", "_No prior failure summaries recorded._");
|
|
643
|
+
}
|
|
644
|
+
return lines.join(`
|
|
645
|
+
`);
|
|
646
|
+
}
|
|
647
|
+
|
|
586
648
|
// ../harmony-shared/dist/agentCommentTrust.js
|
|
587
649
|
function isDaemonAuthoredComment(comment, identity) {
|
|
588
650
|
if (comment.author_type !== "agent")
|
|
@@ -2102,6 +2164,13 @@ var init_types2 = __esm(() => {
|
|
|
2102
2164
|
pickupColumns: ["To Do"],
|
|
2103
2165
|
priorityLabels: { urgent: 100, critical: 90, bug: 50 },
|
|
2104
2166
|
columnBoost: true,
|
|
2167
|
+
ranking: {
|
|
2168
|
+
priorityWeight: 100,
|
|
2169
|
+
successorWeight: 25,
|
|
2170
|
+
successorCap: 4,
|
|
2171
|
+
agePerDayWeight: 5,
|
|
2172
|
+
ageCapDays: 30
|
|
2173
|
+
},
|
|
2105
2174
|
runner: "sdk",
|
|
2106
2175
|
completion: {
|
|
2107
2176
|
createPR: false,
|
|
@@ -2188,7 +2257,14 @@ var init_types2 = __esm(() => {
|
|
|
2188
2257
|
planning: DEFAULT_PLANNING_CONFIG,
|
|
2189
2258
|
playbooks: { enabled: true, humanStageColumns: [], metrics: {} },
|
|
2190
2259
|
contractFirst: DEFAULT_CONTRACT_CONFIG,
|
|
2191
|
-
boardReview: DEFAULT_BOARD_REVIEW_CONFIG
|
|
2260
|
+
boardReview: DEFAULT_BOARD_REVIEW_CONFIG,
|
|
2261
|
+
sweep: {
|
|
2262
|
+
enabled: false,
|
|
2263
|
+
requireLabel: "",
|
|
2264
|
+
trustedAuthors: [],
|
|
2265
|
+
maxProbesPerTick: 5,
|
|
2266
|
+
maxCardsPerSweep: 10
|
|
2267
|
+
}
|
|
2192
2268
|
};
|
|
2193
2269
|
});
|
|
2194
2270
|
|
|
@@ -2258,6 +2334,10 @@ function loadDaemonConfig() {
|
|
|
2258
2334
|
...DEFAULT_AGENT_CONFIG.completion,
|
|
2259
2335
|
...agentOverrides.completion ?? {}
|
|
2260
2336
|
},
|
|
2337
|
+
ranking: {
|
|
2338
|
+
...DEFAULT_AGENT_CONFIG.ranking,
|
|
2339
|
+
...agentOverrides.ranking ?? {}
|
|
2340
|
+
},
|
|
2261
2341
|
claude: {
|
|
2262
2342
|
...DEFAULT_AGENT_CONFIG.claude,
|
|
2263
2343
|
...agentOverrides.claude ?? {}
|
|
@@ -2305,6 +2385,13 @@ function loadDaemonConfig() {
|
|
|
2305
2385
|
boardReview: {
|
|
2306
2386
|
...DEFAULT_AGENT_CONFIG.boardReview,
|
|
2307
2387
|
...agentOverrides.boardReview ?? {}
|
|
2388
|
+
},
|
|
2389
|
+
sweep: {
|
|
2390
|
+
...DEFAULT_AGENT_CONFIG.sweep,
|
|
2391
|
+
...agentOverrides.sweep ?? {},
|
|
2392
|
+
trustedAuthors: [
|
|
2393
|
+
...agentOverrides.sweep?.trustedAuthors ?? DEFAULT_AGENT_CONFIG.sweep.trustedAuthors
|
|
2394
|
+
]
|
|
2308
2395
|
}
|
|
2309
2396
|
};
|
|
2310
2397
|
if (agent.runner !== "cli" && agent.runner !== "sdk") {
|
|
@@ -2348,6 +2435,67 @@ function validateAutoMergeConfig(config) {
|
|
|
2348
2435
|
throw new ConfigValidationError(`Invalid agent config — review.autoMerge.strategy "${s}" must be one of: ${valid.join(", ")}`, [`review.autoMerge.strategy: invalid value "${s}"`]);
|
|
2349
2436
|
}
|
|
2350
2437
|
}
|
|
2438
|
+
function validateSweepConfig(config) {
|
|
2439
|
+
const sweep = config.sweep;
|
|
2440
|
+
const issues = [];
|
|
2441
|
+
if (!Number.isInteger(sweep.maxProbesPerTick) || sweep.maxProbesPerTick < 1) {
|
|
2442
|
+
issues.push(`sweep.maxProbesPerTick: must be an integer >= 1, got ${JSON.stringify(sweep.maxProbesPerTick)}`);
|
|
2443
|
+
}
|
|
2444
|
+
if (!Number.isInteger(sweep.maxCardsPerSweep) || sweep.maxCardsPerSweep === 0) {
|
|
2445
|
+
issues.push(sweep.maxCardsPerSweep === 0 ? `sweep.maxCardsPerSweep: 0 is ambiguous — it could mean "no cap" or "claim nothing". Use a positive number of cards, or -1 to run with no card cap.` : `sweep.maxCardsPerSweep: must be a whole number of cards (positive) or -1 for no cap, got ${JSON.stringify(sweep.maxCardsPerSweep)}`);
|
|
2446
|
+
}
|
|
2447
|
+
if (sweep.enabled && sweep.maxCardsPerSweep < 0 && config.budget.dailyBudgetCents < 0) {
|
|
2448
|
+
issues.push("sweep.enabled: true with no ceiling at all — sweep.maxCardsPerSweep and budget.dailyBudgetCents are both opted out (-1). A self-claiming daemon needs at least one: set a card cap (e.g. 10) or a daily spend cap (e.g. 5000 for $50.00/day).");
|
|
2449
|
+
}
|
|
2450
|
+
if (sweep.enabled && !config.http.enabled) {
|
|
2451
|
+
issues.push("sweep.enabled: true but http.enabled is false — the sweep kill switch IS the local HTTP server (`POST /sweep/stop`, `harmony-agent sweep stop`), so this configuration ships a self-claiming daemon with no way to stop it, and no way to clear a latched card cap either. Set http.enabled: true.");
|
|
2452
|
+
}
|
|
2453
|
+
if (sweep.enabled && config.pickupColumns.length === 0) {
|
|
2454
|
+
issues.push("sweep.enabled: true but pickupColumns is empty — the sweep has no column to claim from");
|
|
2455
|
+
}
|
|
2456
|
+
if (sweep.enabled && config.boardReview.enabled) {
|
|
2457
|
+
const digest = config.boardReview.digestColumn;
|
|
2458
|
+
if (!digest) {
|
|
2459
|
+
issues.push("sweep.enabled and boardReview.enabled are both true but boardReview.digestColumn is empty — the digest would fall back to the first pickup column, where the sweep would claim it and run an implement session on card titles written by other members. Set digestColumn to a column the daemon does not pick up from.");
|
|
2460
|
+
} else if (config.pickupColumns.some((c) => c.toLowerCase() === digest.toLowerCase())) {
|
|
2461
|
+
issues.push(`boardReview.digestColumn: "${digest}" is also a sweep pickup column — the daemon would claim its own digest card and run it. Use a column the daemon does not pick up from.`);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2465
|
+
for (const author of sweep.trustedAuthors) {
|
|
2466
|
+
if (!UUID.test(author)) {
|
|
2467
|
+
issues.push(`sweep.trustedAuthors: "${author}" is not a user id — expected a workspace member's UUID (harmony_get_workspace_members lists them)`);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
if (issues.length > 0) {
|
|
2471
|
+
throw new ConfigValidationError(`Invalid agent config — sweep mode:
|
|
2472
|
+
- ${issues.join(`
|
|
2473
|
+
- `)}`, issues);
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
function validateBudgetConfig(config) {
|
|
2477
|
+
const cents = config.budget.dailyBudgetCents;
|
|
2478
|
+
if (Number.isInteger(cents) && cents !== 0)
|
|
2479
|
+
return;
|
|
2480
|
+
const issue = cents === 0 ? `budget.dailyBudgetCents: 0 is ambiguous — it could mean "no cap" or "spend nothing"` : `budget.dailyBudgetCents: ${cents} is not a whole number of cents`;
|
|
2481
|
+
throw new ConfigValidationError(`Invalid agent config — ${issue}.
|
|
2482
|
+
` + ` Set a positive cap in cents (e.g. 5000 for $50.00/day), or -1 to run with no daily cap.`, [issue]);
|
|
2483
|
+
}
|
|
2484
|
+
function validateRankingConfig(config) {
|
|
2485
|
+
const issues = [];
|
|
2486
|
+
const entries = Object.entries(config.ranking);
|
|
2487
|
+
for (const [key, value] of entries) {
|
|
2488
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
2489
|
+
issues.push(`ranking.${key}: must be a finite number >= 0, got ${JSON.stringify(value)}`);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
if (issues.length > 0) {
|
|
2493
|
+
throw new ConfigValidationError(`Invalid agent config — ranking weights:
|
|
2494
|
+
- ${issues.join(`
|
|
2495
|
+
- `)}
|
|
2496
|
+
` + ` Set a term's weight to 0 to switch it off; zeroing priorityWeight, successorWeight and agePerDayWeight reproduces the pre-#979 ordering.`, issues);
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2351
2499
|
function columnNames(board) {
|
|
2352
2500
|
return board.columns.map((c) => c.name);
|
|
2353
2501
|
}
|
|
@@ -2410,6 +2558,13 @@ async function validateColumnReferences(client, projectId, config) {
|
|
|
2410
2558
|
where: "boardReview.digestColumn"
|
|
2411
2559
|
});
|
|
2412
2560
|
}
|
|
2561
|
+
if (config.sweep.enabled && config.sweep.requireLabel) {
|
|
2562
|
+
const target = config.sweep.requireLabel.toLowerCase();
|
|
2563
|
+
const boardLabels = board.labels ?? [];
|
|
2564
|
+
if (!boardLabels.some((l) => l.name.toLowerCase() === target)) {
|
|
2565
|
+
issues.push(`sweep.requireLabel: label "${config.sweep.requireLabel}" not found on board — the sweep would never claim a card. Known labels: ${boardLabels.map((l) => l.name).join(", ") || "(none)"}`);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2413
2568
|
for (const { value, where } of required) {
|
|
2414
2569
|
if (!value)
|
|
2415
2570
|
continue;
|
|
@@ -2419,7 +2574,7 @@ async function validateColumnReferences(client, projectId, config) {
|
|
|
2419
2574
|
}
|
|
2420
2575
|
if (issues.length > 0) {
|
|
2421
2576
|
const help = `Available columns: ${known.join(", ")}`;
|
|
2422
|
-
throw new ConfigValidationError(`Invalid agent config — the following
|
|
2577
|
+
throw new ConfigValidationError(`Invalid agent config — the following board references are invalid:
|
|
2423
2578
|
- ${issues.join(`
|
|
2424
2579
|
- `)}
|
|
2425
2580
|
${help}`, issues);
|
|
@@ -2545,7 +2700,14 @@ class HttpServer {
|
|
|
2545
2700
|
if (method === "GET" && path === "/status") {
|
|
2546
2701
|
return this.respondStatus(res);
|
|
2547
2702
|
}
|
|
2703
|
+
if (method === "GET" && path === "/sweep") {
|
|
2704
|
+
return this.respondSweep(res);
|
|
2705
|
+
}
|
|
2548
2706
|
if (method === "POST") {
|
|
2707
|
+
const sweepCmd = parseSweepCommand(path);
|
|
2708
|
+
if (sweepCmd) {
|
|
2709
|
+
return this.respondSweepCommand(res, sweepCmd);
|
|
2710
|
+
}
|
|
2549
2711
|
const cmd = parseCommand(path);
|
|
2550
2712
|
if (cmd) {
|
|
2551
2713
|
return this.respondCommand(res, cmd.command, cmd.cardId);
|
|
@@ -2566,6 +2728,45 @@ class HttpServer {
|
|
|
2566
2728
|
res.writeHead(200, { "content-type": "application/json" });
|
|
2567
2729
|
res.end(JSON.stringify(snapshot));
|
|
2568
2730
|
}
|
|
2731
|
+
respondSweep(res) {
|
|
2732
|
+
const getSweep = this.opts.getSweep;
|
|
2733
|
+
if (!getSweep) {
|
|
2734
|
+
this.respondSweepUnavailable(res);
|
|
2735
|
+
return;
|
|
2736
|
+
}
|
|
2737
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2738
|
+
res.end(JSON.stringify(getSweep()));
|
|
2739
|
+
}
|
|
2740
|
+
async respondSweepCommand(res, command) {
|
|
2741
|
+
const handle = this.opts.handleSweepCommand;
|
|
2742
|
+
if (!handle) {
|
|
2743
|
+
this.respondSweepUnavailable(res);
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
const sweep = await handle(command);
|
|
2748
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
2749
|
+
res.end(JSON.stringify({
|
|
2750
|
+
ok: true,
|
|
2751
|
+
command,
|
|
2752
|
+
sweep,
|
|
2753
|
+
inFlight: this.opts.getStatus().workers.filter(busy).length
|
|
2754
|
+
}));
|
|
2755
|
+
} catch (err) {
|
|
2756
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
2757
|
+
res.end(JSON.stringify({
|
|
2758
|
+
error: "sweep_command_failed",
|
|
2759
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
2760
|
+
}));
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
respondSweepUnavailable(res) {
|
|
2764
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
2765
|
+
res.end(JSON.stringify({
|
|
2766
|
+
error: "sweep_unavailable",
|
|
2767
|
+
detail: "this daemon has no sweep control wired"
|
|
2768
|
+
}));
|
|
2769
|
+
}
|
|
2569
2770
|
async respondCommand(res, command, cardId) {
|
|
2570
2771
|
try {
|
|
2571
2772
|
await this.opts.handleCommand(command, cardId);
|
|
@@ -2589,6 +2790,13 @@ function parseCommand(path) {
|
|
|
2589
2790
|
return null;
|
|
2590
2791
|
return { command: match[1], cardId: decodeURIComponent(match[2]) };
|
|
2591
2792
|
}
|
|
2793
|
+
function parseSweepCommand(path) {
|
|
2794
|
+
const match = path.match(/^\/sweep\/(stop|resume)$/);
|
|
2795
|
+
return match ? match[1] : null;
|
|
2796
|
+
}
|
|
2797
|
+
function busy(w) {
|
|
2798
|
+
return w.cardId !== null;
|
|
2799
|
+
}
|
|
2592
2800
|
var TAG3 = "http";
|
|
2593
2801
|
var init_http_server = () => {};
|
|
2594
2802
|
|
|
@@ -3059,48 +3267,6 @@ var matchesColumn = (columns, columnName) => {
|
|
|
3059
3267
|
return columns.some((name) => name.toLowerCase() === lc);
|
|
3060
3268
|
};
|
|
3061
3269
|
|
|
3062
|
-
// src/budget.ts
|
|
3063
|
-
class BudgetGuard {
|
|
3064
|
-
config;
|
|
3065
|
-
store;
|
|
3066
|
-
constructor(config, store) {
|
|
3067
|
-
this.config = config;
|
|
3068
|
-
this.store = store;
|
|
3069
|
-
}
|
|
3070
|
-
check(cardId) {
|
|
3071
|
-
const card = this.store.getCard(cardId);
|
|
3072
|
-
if (card && card.attempts >= this.config.maxAttemptsPerCard) {
|
|
3073
|
-
return {
|
|
3074
|
-
allow: false,
|
|
3075
|
-
reason: "max_attempts",
|
|
3076
|
-
detail: `${card.attempts} of ${this.config.maxAttemptsPerCard} attempts exhausted`
|
|
3077
|
-
};
|
|
3078
|
-
}
|
|
3079
|
-
return { allow: true };
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
3083
|
-
const wayBackIn = pauseEnabled ? "Continue to grant a fresh attempt, or reassign the card." : "Reassign the card to grant a fresh attempt.";
|
|
3084
|
-
const lines = [
|
|
3085
|
-
"**Out of attempts — over to you.**",
|
|
3086
|
-
`Stopped after ${maxAttempts} failed attempt${maxAttempts === 1 ? "" : "s"}. ${wayBackIn}`
|
|
3087
|
-
];
|
|
3088
|
-
if (failures.length > 0) {
|
|
3089
|
-
lines.push("", "Recent failures:");
|
|
3090
|
-
for (const f of failures) {
|
|
3091
|
-
const when = new Date(f.ts).toISOString().replace("T", " ").slice(0, 16);
|
|
3092
|
-
const tag = f.reason ? ` [${f.reason}]` : "";
|
|
3093
|
-
const branch = f.recoveryBranch ? `
|
|
3094
|
-
recover: \`git fetch && git checkout ${f.recoveryBranch}\`` : "";
|
|
3095
|
-
lines.push(`- ${when} UTC${tag} — ${f.summary}${branch}`);
|
|
3096
|
-
}
|
|
3097
|
-
} else {
|
|
3098
|
-
lines.push("", "_No prior failure summaries recorded._");
|
|
3099
|
-
}
|
|
3100
|
-
return lines.join(`
|
|
3101
|
-
`);
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
3270
|
// src/budget-pause.ts
|
|
3105
3271
|
function classifyRunExit(x) {
|
|
3106
3272
|
if (x.exitCode === 0)
|
|
@@ -3256,6 +3422,45 @@ var init_handback = () => {};
|
|
|
3256
3422
|
|
|
3257
3423
|
// src/queue.ts
|
|
3258
3424
|
import { log as log8 } from "@gethmy/harness";
|
|
3425
|
+
function scoreCardBreakdown(config, card, column, labels, signals = {}) {
|
|
3426
|
+
const w = config.ranking;
|
|
3427
|
+
const priority = w.priorityWeight * (PRIORITY_STEP[card.priority ?? "medium"] ?? PRIORITY_STEP.medium);
|
|
3428
|
+
let label = 0;
|
|
3429
|
+
for (const l of labels) {
|
|
3430
|
+
const boost = config.priorityLabels[l.name.toLowerCase()] ?? 0;
|
|
3431
|
+
if (boost > label)
|
|
3432
|
+
label = boost;
|
|
3433
|
+
}
|
|
3434
|
+
const columnTerm = config.columnBoost ? Math.max(0, 100 - column.position * 10) : 0;
|
|
3435
|
+
const successorCount = Math.min(Math.max(0, signals.successors ?? 0), Math.max(0, w.successorCap));
|
|
3436
|
+
const successors = w.successorWeight * successorCount;
|
|
3437
|
+
const ageDays = daysInColumn(card, signals.now ?? Date.now());
|
|
3438
|
+
const age = w.agePerDayWeight * Math.min(ageDays, Math.max(0, w.ageCapDays));
|
|
3439
|
+
return {
|
|
3440
|
+
total: priority + label + columnTerm + successors + age,
|
|
3441
|
+
priority,
|
|
3442
|
+
label,
|
|
3443
|
+
column: columnTerm,
|
|
3444
|
+
successors,
|
|
3445
|
+
age,
|
|
3446
|
+
ageDays
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
function scoreCard(config, card, column, labels, signals = {}) {
|
|
3450
|
+
return scoreCardBreakdown(config, card, column, labels, signals).total;
|
|
3451
|
+
}
|
|
3452
|
+
function daysInColumn(card, now) {
|
|
3453
|
+
const enteredAt = card.column_entered_at;
|
|
3454
|
+
if (!enteredAt)
|
|
3455
|
+
return 0;
|
|
3456
|
+
const entered = Date.parse(enteredAt);
|
|
3457
|
+
if (Number.isNaN(entered))
|
|
3458
|
+
return 0;
|
|
3459
|
+
return Math.max(0, Math.floor((now - entered) / MS_PER_DAY));
|
|
3460
|
+
}
|
|
3461
|
+
function formatBreakdown(b) {
|
|
3462
|
+
return `score=${b.total} ` + `[priority=${b.priority} label=${b.label} column=${b.column} ` + `successors=${b.successors} age=${b.age} (${b.ageDays}d)]`;
|
|
3463
|
+
}
|
|
3259
3464
|
|
|
3260
3465
|
class PriorityQueue {
|
|
3261
3466
|
config;
|
|
@@ -3263,25 +3468,17 @@ class PriorityQueue {
|
|
|
3263
3468
|
constructor(config) {
|
|
3264
3469
|
this.config = config;
|
|
3265
3470
|
}
|
|
3266
|
-
scoreCard(
|
|
3267
|
-
|
|
3268
|
-
for (const label of labels) {
|
|
3269
|
-
const boost = this.config.priorityLabels[label.name.toLowerCase()] ?? 0;
|
|
3270
|
-
if (boost > score)
|
|
3271
|
-
score = boost;
|
|
3272
|
-
}
|
|
3273
|
-
if (this.config.columnBoost) {
|
|
3274
|
-
score += Math.max(0, 100 - column.position * 10);
|
|
3275
|
-
}
|
|
3276
|
-
return score;
|
|
3471
|
+
scoreCard(card, column, labels, signals = {}) {
|
|
3472
|
+
return scoreCard(this.config, card, column, labels, signals);
|
|
3277
3473
|
}
|
|
3278
|
-
enqueue(card, column, labels, mode = "implement") {
|
|
3474
|
+
enqueue(card, column, labels, mode = "implement", signals = {}) {
|
|
3279
3475
|
const existing = this.items.findIndex((i) => i.cardId === card.id);
|
|
3280
3476
|
if (existing !== -1) {
|
|
3281
3477
|
log8.debug(TAG8, `Card #${card.short_id} already queued, updating priority`);
|
|
3282
3478
|
this.items.splice(existing, 1);
|
|
3283
3479
|
}
|
|
3284
|
-
const
|
|
3480
|
+
const breakdown = scoreCardBreakdown(this.config, card, column, labels, signals);
|
|
3481
|
+
const priority = breakdown.total;
|
|
3285
3482
|
const item = {
|
|
3286
3483
|
cardId: card.id,
|
|
3287
3484
|
shortId: card.short_id,
|
|
@@ -3298,7 +3495,7 @@ class PriorityQueue {
|
|
|
3298
3495
|
}
|
|
3299
3496
|
}
|
|
3300
3497
|
this.items.splice(insertIdx, 0, item);
|
|
3301
|
-
log8.info(TAG8, `Enqueued #${card.short_id} "${card.title}" (
|
|
3498
|
+
log8.info(TAG8, `Enqueued #${card.short_id} "${card.title}" (${formatBreakdown(breakdown)}, pos=${insertIdx}, queue=${this.items.length})`);
|
|
3302
3499
|
}
|
|
3303
3500
|
dequeue() {
|
|
3304
3501
|
return this.items.shift() ?? null;
|
|
@@ -3327,8 +3524,15 @@ class PriorityQueue {
|
|
|
3327
3524
|
return this.items.slice();
|
|
3328
3525
|
}
|
|
3329
3526
|
}
|
|
3330
|
-
var TAG8 = "queue";
|
|
3331
|
-
var init_queue = () => {
|
|
3527
|
+
var TAG8 = "queue", MS_PER_DAY = 86400000, PRIORITY_STEP;
|
|
3528
|
+
var init_queue = __esm(() => {
|
|
3529
|
+
PRIORITY_STEP = {
|
|
3530
|
+
low: 0,
|
|
3531
|
+
medium: 1,
|
|
3532
|
+
high: 2,
|
|
3533
|
+
urgent: 3
|
|
3534
|
+
};
|
|
3535
|
+
});
|
|
3332
3536
|
|
|
3333
3537
|
// src/episode-writer.ts
|
|
3334
3538
|
import { log as log9 } from "@gethmy/harness";
|
|
@@ -5341,6 +5545,9 @@ import {
|
|
|
5341
5545
|
import { homedir as homedir3 } from "node:os";
|
|
5342
5546
|
import { dirname, join as join3 } from "node:path";
|
|
5343
5547
|
import { log as log16 } from "@gethmy/harness";
|
|
5548
|
+
function emptySweep() {
|
|
5549
|
+
return { claimed: 0, totalClaimed: 0, haltReason: null, haltedAt: null };
|
|
5550
|
+
}
|
|
5344
5551
|
function emptyState() {
|
|
5345
5552
|
return {
|
|
5346
5553
|
version: SCHEMA_VERSION,
|
|
@@ -5349,7 +5556,8 @@ function emptyState() {
|
|
|
5349
5556
|
daemonStartedAt: null,
|
|
5350
5557
|
runs: [],
|
|
5351
5558
|
cards: [],
|
|
5352
|
-
daily: []
|
|
5559
|
+
daily: [],
|
|
5560
|
+
sweep: emptySweep()
|
|
5353
5561
|
};
|
|
5354
5562
|
}
|
|
5355
5563
|
function todayUtc() {
|
|
@@ -5404,7 +5612,8 @@ class StateStore {
|
|
|
5404
5612
|
daemonStartedAt: null,
|
|
5405
5613
|
runs: [],
|
|
5406
5614
|
cards: parsed.cards ?? [],
|
|
5407
|
-
daily: parsed.daily ?? []
|
|
5615
|
+
daily: parsed.daily ?? [],
|
|
5616
|
+
sweep: parsed.sweep ?? emptySweep()
|
|
5408
5617
|
};
|
|
5409
5618
|
}
|
|
5410
5619
|
return {
|
|
@@ -5414,7 +5623,8 @@ class StateStore {
|
|
|
5414
5623
|
daemonStartedAt: parsed.daemonStartedAt ?? null,
|
|
5415
5624
|
runs: parsed.runs ?? [],
|
|
5416
5625
|
cards: parsed.cards ?? [],
|
|
5417
|
-
daily: parsed.daily ?? []
|
|
5626
|
+
daily: parsed.daily ?? [],
|
|
5627
|
+
sweep: parsed.sweep ?? emptySweep()
|
|
5418
5628
|
};
|
|
5419
5629
|
} catch (err) {
|
|
5420
5630
|
log16.error(TAG14, `failed to read state file: ${err instanceof Error ? err.message : err}`);
|
|
@@ -5667,6 +5877,27 @@ class StateStore {
|
|
|
5667
5877
|
this.state.daily = this.state.daily.filter((d) => d.date >= cutoff);
|
|
5668
5878
|
await this.persist();
|
|
5669
5879
|
}
|
|
5880
|
+
getSweep() {
|
|
5881
|
+
return { ...this.state.sweep };
|
|
5882
|
+
}
|
|
5883
|
+
async recordSweepClaim() {
|
|
5884
|
+
this.state.sweep.claimed += 1;
|
|
5885
|
+
this.state.sweep.totalClaimed += 1;
|
|
5886
|
+
await this.persist();
|
|
5887
|
+
}
|
|
5888
|
+
async haltSweep(reason) {
|
|
5889
|
+
if (this.state.sweep.haltReason === reason)
|
|
5890
|
+
return;
|
|
5891
|
+
this.state.sweep.haltReason = reason;
|
|
5892
|
+
this.state.sweep.haltedAt = Date.now();
|
|
5893
|
+
await this.persist();
|
|
5894
|
+
}
|
|
5895
|
+
async resumeSweep() {
|
|
5896
|
+
this.state.sweep.claimed = 0;
|
|
5897
|
+
this.state.sweep.haltReason = null;
|
|
5898
|
+
this.state.sweep.haltedAt = null;
|
|
5899
|
+
await this.persist();
|
|
5900
|
+
}
|
|
5670
5901
|
getDailyCostCents(date) {
|
|
5671
5902
|
const key = date ?? todayUtc();
|
|
5672
5903
|
return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
|
|
@@ -6073,6 +6304,14 @@ class ReviewWorker {
|
|
|
6073
6304
|
numTurns: cost.numTurns
|
|
6074
6305
|
};
|
|
6075
6306
|
}
|
|
6307
|
+
async chargeDailyLedger(cardId) {
|
|
6308
|
+
const { costCents } = this.endLedger();
|
|
6309
|
+
if (costCents <= 0)
|
|
6310
|
+
return;
|
|
6311
|
+
try {
|
|
6312
|
+
await this.stateStore.addCost(cardId, costCents);
|
|
6313
|
+
} catch {}
|
|
6314
|
+
}
|
|
6076
6315
|
async run(card, column, labels, subtasks) {
|
|
6077
6316
|
this.aborted = false;
|
|
6078
6317
|
this.timedOut = false;
|
|
@@ -6369,6 +6608,10 @@ ${userPrompt}`;
|
|
|
6369
6608
|
const status = this.timedOut ? "failed" : this.state === "error" || this.aborted || this.sessionConflict ? "paused" : "completed";
|
|
6370
6609
|
await this.stateStore.endRun(this.runId, status, this.sessionConflict ? { errorMessage: "session_conflict", ...this.endLedger() } : this.endLedger());
|
|
6371
6610
|
}
|
|
6611
|
+
const settled = this.stateStore.getRun(this.runId);
|
|
6612
|
+
if (this.cardId && settled && settled.endedAt !== null) {
|
|
6613
|
+
await this.chargeDailyLedger(this.cardId);
|
|
6614
|
+
}
|
|
6372
6615
|
} catch {}
|
|
6373
6616
|
}
|
|
6374
6617
|
if (this.cardId && this.timedOut && this.config.budget.pause.enabled && this.state !== "parked" && this.state !== "error") {
|
|
@@ -6816,20 +7059,21 @@ function isBlockerResolved(blocker, columns) {
|
|
|
6816
7059
|
const blockerColumn = columns.find((c) => c.id === blocker.column_id);
|
|
6817
7060
|
return blockerColumn?.mark_cards_done === true;
|
|
6818
7061
|
}
|
|
6819
|
-
async function
|
|
7062
|
+
async function getChainSignals(client, card, projectId, knownColumns) {
|
|
6820
7063
|
const links = await fetchBlocksLinks(client, card.id);
|
|
6821
7064
|
if (!links)
|
|
6822
|
-
return null;
|
|
7065
|
+
return { blockers: null, successors: 0 };
|
|
7066
|
+
const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done).length;
|
|
6823
7067
|
const incoming = links.filter((l) => l.direction === "incoming");
|
|
6824
7068
|
if (incoming.length === 0)
|
|
6825
|
-
return [];
|
|
6826
|
-
const
|
|
6827
|
-
const
|
|
6828
|
-
return incoming.filter((l) => !isBlockerResolved(l.target_card, columns)).map((l) => ({
|
|
7069
|
+
return { blockers: [], successors };
|
|
7070
|
+
const columns = knownColumns ?? ((await client.getBoard(projectId, { summary: true })).columns ?? []);
|
|
7071
|
+
const blockers = incoming.filter((l) => !isBlockerResolved(l.target_card, columns)).map((l) => ({
|
|
6829
7072
|
cardId: l.target_card.id,
|
|
6830
7073
|
shortId: l.target_card.short_id,
|
|
6831
7074
|
title: l.target_card.title
|
|
6832
7075
|
}));
|
|
7076
|
+
return { blockers, successors };
|
|
6833
7077
|
}
|
|
6834
7078
|
async function promoteUnblockedSuccessors(completedCard, deps) {
|
|
6835
7079
|
const links = await fetchBlocksLinks(deps.client, completedCard.id);
|
|
@@ -7658,13 +7902,21 @@ async function advanceConvergeLoop(card, stage, stageIndex, def, evaluation, loo
|
|
|
7658
7902
|
log24.info(TAG22, `#${card.short_id} LoopExhausted: ${reason}`);
|
|
7659
7903
|
return { kind: "held_gate_unmet", reason };
|
|
7660
7904
|
}
|
|
7905
|
+
const guard = await guardStageReclaim(card, `converge-loop iteration of "${stage.name}"`, deps);
|
|
7906
|
+
if (!guard.proceed) {
|
|
7907
|
+
return {
|
|
7908
|
+
kind: "reclaim_refused",
|
|
7909
|
+
reason: guard.reason,
|
|
7910
|
+
released: guard.released
|
|
7911
|
+
};
|
|
7912
|
+
}
|
|
7661
7913
|
await deps.stateStore.decrementAttempt(card.id).catch(() => {});
|
|
7662
7914
|
await writeIterationHandoff(card, stage, iteration, maxIterations, evaluation, deps);
|
|
7663
7915
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
7664
7916
|
try {
|
|
7665
7917
|
await deps.client.addComment(card.id, `Converge loop — ${summary}. Re-running "${stage.name}".`, { commentType: "progress" });
|
|
7666
7918
|
} catch {}
|
|
7667
|
-
await runTransition(deps.client, card, {
|
|
7919
|
+
await runTransition(deps.client, guard.card, {
|
|
7668
7920
|
move: { columnName: toColumn },
|
|
7669
7921
|
addLabels: [{ name: AGENT_LABEL }],
|
|
7670
7922
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
@@ -7780,11 +8032,19 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
7780
8032
|
log24.info(TAG22, `#${card.short_id} GateUnmetExhausted: ${reason}`);
|
|
7781
8033
|
return { kind: "held_gate_unmet", reason };
|
|
7782
8034
|
}
|
|
8035
|
+
const guard = await guardStageReclaim(card, `gate-unmet re-run of "${stage.name}"`, deps);
|
|
8036
|
+
if (!guard.proceed) {
|
|
8037
|
+
return {
|
|
8038
|
+
kind: "reclaim_refused",
|
|
8039
|
+
reason: guard.reason,
|
|
8040
|
+
released: guard.released
|
|
8041
|
+
};
|
|
8042
|
+
}
|
|
7783
8043
|
const toColumn = await resolveStageColumnName(deps.client, card, stage) ?? deps.fallbackColumn;
|
|
7784
8044
|
try {
|
|
7785
8045
|
await deps.client.addComment(card.id, `Stage gate unmet — re-running "${stage.name}". ${summary}.`, { commentType: "progress" });
|
|
7786
8046
|
} catch {}
|
|
7787
|
-
await runTransition(deps.client, card, {
|
|
8047
|
+
await runTransition(deps.client, guard.card, {
|
|
7788
8048
|
move: { columnName: toColumn },
|
|
7789
8049
|
addLabels: [{ name: AGENT_LABEL }],
|
|
7790
8050
|
...isAgentRunnableOwner(stage.owner) ? { assignAgent: deps.agentId } : {}
|
|
@@ -7792,6 +8052,38 @@ async function handleGateUnmet(card, stage, summary, deps) {
|
|
|
7792
8052
|
log24.info(TAG22, `#${card.short_id} gate unmet for "${stage.name}" — requeued to "${toColumn}" for re-run (attempt ${attempts}/${deps.maxAttempts})`);
|
|
7793
8053
|
return { kind: "requeued_gate_unmet", toColumn };
|
|
7794
8054
|
}
|
|
8055
|
+
async function guardStageReclaim(card, what, deps) {
|
|
8056
|
+
const { verdict, card: fresh } = await guardedHandback(deps.client, card.id, {
|
|
8057
|
+
agentId: deps.closeoutReleasesAssignment ? null : deps.agentId,
|
|
8058
|
+
workingColumnId: null
|
|
8059
|
+
});
|
|
8060
|
+
const foreign = fresh?.assigned_agent_id && fresh.assigned_agent_id !== deps.agentId ? fresh.assigned_agent_id : null;
|
|
8061
|
+
if (verdict.proceed && !foreign) {
|
|
8062
|
+
return { proceed: true, card: fresh ?? card };
|
|
8063
|
+
}
|
|
8064
|
+
const [reason, detail] = verdict.proceed ? [
|
|
8065
|
+
"reassigned",
|
|
8066
|
+
`the card is assigned to agent ${foreign}, not ${deps.agentId}`
|
|
8067
|
+
] : [verdict.reason, verdict.detail];
|
|
8068
|
+
const stillOurs = !!fresh && !fresh.archived_at && !foreign && fresh.assigned_agent_id === deps.agentId;
|
|
8069
|
+
let released = false;
|
|
8070
|
+
if (fresh && !fresh.archived_at && !foreign) {
|
|
8071
|
+
try {
|
|
8072
|
+
await runTransition(deps.client, fresh, {
|
|
8073
|
+
removeLabels: [AGENT_LABEL],
|
|
8074
|
+
...stillOurs ? { assignAgent: null } : {}
|
|
8075
|
+
}, { store: deps.stateStore, runId: deps.runId });
|
|
8076
|
+
released = stillOurs;
|
|
8077
|
+
} catch (err) {
|
|
8078
|
+
log24.warn(TAG22, `#${card.short_id} could not hand the card over after refusing the ${what}: ${err instanceof Error ? err.message : err}${stillOurs ? " — the claim is still ours, so the next reconcile tick may re-pick the card" : ""}`);
|
|
8079
|
+
}
|
|
8080
|
+
}
|
|
8081
|
+
log24.info(TAG22, `#${card.short_id} ${what} refused — ${detail} (${reason})${released ? "; released the daemon's claim so the stage router stops here" : ""}`);
|
|
8082
|
+
try {
|
|
8083
|
+
await deps.client.addComment(card.id, `Stopped the ${what} — ${detail}. The card is not the daemon's to reclaim, so this stage will not re-run until it is assigned back.`, { commentType: "decision" });
|
|
8084
|
+
} catch {}
|
|
8085
|
+
return { proceed: false, reason, released };
|
|
8086
|
+
}
|
|
7795
8087
|
async function holdForHuman(client, card, reason, runId, stateStore, opts = {}) {
|
|
7796
8088
|
if (!opts.keepAttempts) {
|
|
7797
8089
|
await stateStore.decrementAttempt(card.id).catch(() => {});
|
|
@@ -7819,6 +8111,7 @@ async function holdForHuman(client, card, reason, runId, stateStore, opts = {})
|
|
|
7819
8111
|
var TAG22 = "stage-advance", AGENT_LABEL = "agent";
|
|
7820
8112
|
var init_stage_advance = __esm(() => {
|
|
7821
8113
|
init_dist();
|
|
8114
|
+
init_handback();
|
|
7822
8115
|
init_transitions();
|
|
7823
8116
|
});
|
|
7824
8117
|
|
|
@@ -8278,6 +8571,10 @@ class Worker {
|
|
|
8278
8571
|
case "held_misconfigured":
|
|
8279
8572
|
this.held = true;
|
|
8280
8573
|
break;
|
|
8574
|
+
case "reclaim_refused":
|
|
8575
|
+
this.held = true;
|
|
8576
|
+
log25.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
|
|
8577
|
+
break;
|
|
8281
8578
|
case "advanced":
|
|
8282
8579
|
case "completed_terminal":
|
|
8283
8580
|
case "no_advance":
|
|
@@ -8941,6 +9238,10 @@ class Worker {
|
|
|
8941
9238
|
case "held_misconfigured":
|
|
8942
9239
|
this.held = true;
|
|
8943
9240
|
break;
|
|
9241
|
+
case "reclaim_refused":
|
|
9242
|
+
this.held = true;
|
|
9243
|
+
log25.info(this.tag, `#${card.short_id} stage reclaim refused (${outcome.reason})${outcome.released ? " — released the daemon's claim" : ""}`);
|
|
9244
|
+
break;
|
|
8944
9245
|
case "advanced":
|
|
8945
9246
|
case "completed_terminal":
|
|
8946
9247
|
case "no_advance":
|
|
@@ -9111,6 +9412,7 @@ ${prompt}`;
|
|
|
9111
9412
|
client: this.client,
|
|
9112
9413
|
stateStore: this.stateStore,
|
|
9113
9414
|
agentId: this.identity.agentId,
|
|
9415
|
+
closeoutReleasesAssignment: !!this.config.completion.moveToColumn,
|
|
9114
9416
|
maxAttempts: this.config.budget.maxAttemptsPerCard,
|
|
9115
9417
|
fallbackColumn: this.config.pickupColumns[0] ?? "To Do",
|
|
9116
9418
|
sink: this.cliRunner,
|
|
@@ -9908,6 +10210,7 @@ class Pool {
|
|
|
9908
10210
|
return;
|
|
9909
10211
|
}
|
|
9910
10212
|
this.reservations.add(card.id);
|
|
10213
|
+
let chainSuccessors = 0;
|
|
9911
10214
|
try {
|
|
9912
10215
|
if (mode === "implement") {
|
|
9913
10216
|
if (this.authPaused) {
|
|
@@ -9924,14 +10227,14 @@ class Pool {
|
|
|
9924
10227
|
const decision = this.budget.check(card.id);
|
|
9925
10228
|
if (!decision.allow) {
|
|
9926
10229
|
if (decision.reason === "daily_budget") {
|
|
9927
|
-
|
|
9928
|
-
await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
|
|
10230
|
+
await this.denyDailyBudget(card, decision.detail);
|
|
9929
10231
|
} else {
|
|
9930
10232
|
log26.debug(TAG24, `#${card.short_id} gave up: ${decision.detail}`);
|
|
9931
10233
|
}
|
|
9932
10234
|
return;
|
|
9933
10235
|
}
|
|
9934
|
-
const blockers = await
|
|
10236
|
+
const { blockers, successors } = await getChainSignals(this.client, card, this.projectId);
|
|
10237
|
+
chainSuccessors = successors;
|
|
9935
10238
|
if (blockers === null) {
|
|
9936
10239
|
log26.warn(TAG24, `#${card.short_id} blocker check failed — deferring to next tick`);
|
|
9937
10240
|
return;
|
|
@@ -9942,9 +10245,18 @@ class Pool {
|
|
|
9942
10245
|
await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
|
|
9943
10246
|
return;
|
|
9944
10247
|
}
|
|
10248
|
+
} else {
|
|
10249
|
+
const decision = this.budget.checkDailyBudget();
|
|
10250
|
+
if (!decision.allow) {
|
|
10251
|
+
await this.denyDailyBudget(card, decision.detail);
|
|
10252
|
+
return;
|
|
10253
|
+
}
|
|
9945
10254
|
}
|
|
9946
10255
|
const queue = mode === "review" ? this.reviewQueue : this.implQueue;
|
|
9947
|
-
queue.enqueue(card, column, labels, mode
|
|
10256
|
+
queue.enqueue(card, column, labels, mode, {
|
|
10257
|
+
successors: chainSuccessors,
|
|
10258
|
+
now: Date.now()
|
|
10259
|
+
});
|
|
9948
10260
|
this.cardDataCache.set(card.id, { card, column, labels, subtasks, mode });
|
|
9949
10261
|
const workers = mode === "review" ? this.reviewWorkers : this.implWorkers;
|
|
9950
10262
|
const dispatched = this.tryDispatchFor(workers, queue, mode);
|
|
@@ -9957,6 +10269,15 @@ class Pool {
|
|
|
9957
10269
|
this.reservations.delete(card.id);
|
|
9958
10270
|
}
|
|
9959
10271
|
}
|
|
10272
|
+
async denyDailyBudget(card, detail) {
|
|
10273
|
+
const reason = `Daily budget reached — waiting for reset (${detail})`;
|
|
10274
|
+
const line = `#${card.short_id} skipped (daily_budget): ${detail}`;
|
|
10275
|
+
if (this.lastWaitingEmit.get(card.id) === reason)
|
|
10276
|
+
log26.debug(TAG24, line);
|
|
10277
|
+
else
|
|
10278
|
+
log26.warn(TAG24, line);
|
|
10279
|
+
await this.emitWaiting(card.id, reason);
|
|
10280
|
+
}
|
|
9960
10281
|
lastWaitingEmit = new Map;
|
|
9961
10282
|
async emitWaiting(cardId, currentTask) {
|
|
9962
10283
|
if (this.lastWaitingEmit.get(cardId) === currentTask)
|
|
@@ -10019,6 +10340,24 @@ class Pool {
|
|
|
10019
10340
|
isCardActive(cardId) {
|
|
10020
10341
|
return hasParkedRun(this.stateStore, cardId) || this.implWorkers.some((w) => w.cardId === cardId && w.isActive) || this.reviewWorkers.some((w) => w.cardId === cardId && w.isActive);
|
|
10021
10342
|
}
|
|
10343
|
+
hasFreeImplementSlot() {
|
|
10344
|
+
if (this.shuttingDown || this.authPaused)
|
|
10345
|
+
return false;
|
|
10346
|
+
if (this.apiCooldownRemainingMs() > 0)
|
|
10347
|
+
return false;
|
|
10348
|
+
if (this.implQueue.length > 0 || this.reservations.size > 0)
|
|
10349
|
+
return false;
|
|
10350
|
+
return this.implWorkers.some((w) => w.isIdle);
|
|
10351
|
+
}
|
|
10352
|
+
checkBudget(cardId) {
|
|
10353
|
+
return this.budget.check(cardId);
|
|
10354
|
+
}
|
|
10355
|
+
scoreCard(card, column, labels, signals = {}) {
|
|
10356
|
+
return this.implQueue.scoreCard(card, column, labels, signals);
|
|
10357
|
+
}
|
|
10358
|
+
scoreCardBreakdown(card, column, labels, signals = {}) {
|
|
10359
|
+
return scoreCardBreakdown(this.config, card, column, labels, signals);
|
|
10360
|
+
}
|
|
10022
10361
|
isCardKnown(cardId) {
|
|
10023
10362
|
return this.implQueue.has(cardId) || this.reviewQueue.has(cardId) || this.isCardActive(cardId);
|
|
10024
10363
|
}
|
|
@@ -10629,9 +10968,9 @@ var init_recovery = __esm(() => {
|
|
|
10629
10968
|
|
|
10630
10969
|
// src/claim.ts
|
|
10631
10970
|
import { log as log29 } from "@gethmy/harness";
|
|
10632
|
-
async function
|
|
10971
|
+
async function claimUnassignedCard(client, cardId, agentId, opts) {
|
|
10633
10972
|
try {
|
|
10634
|
-
const { claimed } = await client.claimCard(cardId, agentId);
|
|
10973
|
+
const { claimed } = opts ? await client.claimCard(cardId, agentId, opts) : await client.claimCard(cardId, agentId);
|
|
10635
10974
|
log29.debug(TAG27, `claim ${cardId} → ${claimed ? "won" : "lost"}`);
|
|
10636
10975
|
return claimed;
|
|
10637
10976
|
} catch (err) {
|
|
@@ -10672,7 +11011,7 @@ async function reclaimPreReviewStrands(opts) {
|
|
|
10672
11011
|
for (const card of cards) {
|
|
10673
11012
|
if (checked >= maxPerSweep)
|
|
10674
11013
|
break;
|
|
10675
|
-
if (card.archived_at || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
|
|
11014
|
+
if (card.archived_at || card.done || !reviewColIds.has(card.column_id) || card.assigned_agent_id != null || card.assignee_id != null || knownCardIds.has(card.id)) {
|
|
10676
11015
|
continue;
|
|
10677
11016
|
}
|
|
10678
11017
|
const branch = extractBranchFromDescription(card.description);
|
|
@@ -10689,7 +11028,7 @@ async function reclaimPreReviewStrands(opts) {
|
|
|
10689
11028
|
const prUrl = resolvePrUrl2(card.description ?? null, branch, cwd, provider);
|
|
10690
11029
|
if (prUrl)
|
|
10691
11030
|
continue;
|
|
10692
|
-
const won = await
|
|
11031
|
+
const won = await claimUnassignedCard(client, card.id, agentId);
|
|
10693
11032
|
if (!won) {
|
|
10694
11033
|
log30.debug(TAG28, `#${card.short_id} — lost the review claim race, skipping`);
|
|
10695
11034
|
continue;
|
|
@@ -10714,8 +11053,246 @@ var init_strand_recovery = __esm(() => {
|
|
|
10714
11053
|
init_types2();
|
|
10715
11054
|
});
|
|
10716
11055
|
|
|
11056
|
+
// src/sweep.ts
|
|
11057
|
+
import { log as log31 } from "@gethmy/harness";
|
|
11058
|
+
function rankSweepCandidates(opts) {
|
|
11059
|
+
const {
|
|
11060
|
+
agentId,
|
|
11061
|
+
cards,
|
|
11062
|
+
columns,
|
|
11063
|
+
labelMap,
|
|
11064
|
+
pickupConfig,
|
|
11065
|
+
approvedLabel,
|
|
11066
|
+
requireLabel,
|
|
11067
|
+
trustedAuthors,
|
|
11068
|
+
knownCardIds,
|
|
11069
|
+
budgetCheck,
|
|
11070
|
+
score
|
|
11071
|
+
} = opts;
|
|
11072
|
+
const now = opts.now ?? Date.now();
|
|
11073
|
+
const columnMap = new Map(columns.map((c) => [c.id, c]));
|
|
11074
|
+
const candidates = [];
|
|
11075
|
+
const skipped = [];
|
|
11076
|
+
const skip = (card, reason, detail) => skipped.push({ cardId: card.id, shortId: card.short_id, reason, detail });
|
|
11077
|
+
for (const card of cards) {
|
|
11078
|
+
if (card.archived_at)
|
|
11079
|
+
continue;
|
|
11080
|
+
const column = columnMap.get(card.column_id);
|
|
11081
|
+
if (!column)
|
|
11082
|
+
continue;
|
|
11083
|
+
if (!matchesColumn(pickupConfig.pickupColumns, column.name))
|
|
11084
|
+
continue;
|
|
11085
|
+
const route = classifyPickup(card, column.name, pickupConfig);
|
|
11086
|
+
if (!route || route.mode !== "implement")
|
|
11087
|
+
continue;
|
|
11088
|
+
if (card.done) {
|
|
11089
|
+
skip(card, "done", "card is marked done");
|
|
11090
|
+
continue;
|
|
11091
|
+
}
|
|
11092
|
+
if (card.assignee_id != null) {
|
|
11093
|
+
skip(card, "human_assignee", "a human is assigned");
|
|
11094
|
+
continue;
|
|
11095
|
+
}
|
|
11096
|
+
if (card.created_by == null || !trustedAuthors.has(card.created_by)) {
|
|
11097
|
+
skip(card, "untrusted_author", card.created_by == null ? "card has no recorded author" : `author ${card.created_by} is not in sweep.trustedAuthors`);
|
|
11098
|
+
continue;
|
|
11099
|
+
}
|
|
11100
|
+
if (card.assigned_agent_id != null) {
|
|
11101
|
+
skip(card, "already_owned", card.assigned_agent_id === agentId ? "already assigned to this agent" : "assigned to another agent");
|
|
11102
|
+
continue;
|
|
11103
|
+
}
|
|
11104
|
+
if (knownCardIds.has(card.id)) {
|
|
11105
|
+
skip(card, "known_to_pool", "already queued or active in this pool");
|
|
11106
|
+
continue;
|
|
11107
|
+
}
|
|
11108
|
+
const labels = resolveCardLabels(card, labelMap);
|
|
11109
|
+
if (requireLabel && !hasLabel(labels, requireLabel)) {
|
|
11110
|
+
skip(card, "require_label", `lacks required label "${requireLabel}"`);
|
|
11111
|
+
continue;
|
|
11112
|
+
}
|
|
11113
|
+
if (hasLabel(labels, NEED_REVIEW_LABEL)) {
|
|
11114
|
+
skip(card, "need_review_label", `has "${NEED_REVIEW_LABEL}"`);
|
|
11115
|
+
continue;
|
|
11116
|
+
}
|
|
11117
|
+
if (approvedLabel && hasLabel(labels, approvedLabel)) {
|
|
11118
|
+
skip(card, "approved_label", `has "${approvedLabel}"`);
|
|
11119
|
+
continue;
|
|
11120
|
+
}
|
|
11121
|
+
const decision = budgetCheck(card.id);
|
|
11122
|
+
if (!decision.allow) {
|
|
11123
|
+
skip(card, "budget", `${decision.reason}: ${decision.detail}`);
|
|
11124
|
+
continue;
|
|
11125
|
+
}
|
|
11126
|
+
const breakdown = score(card, column, labels, {
|
|
11127
|
+
successors: opts.successorCounts?.get(card.id) ?? 0,
|
|
11128
|
+
now
|
|
11129
|
+
});
|
|
11130
|
+
candidates.push({
|
|
11131
|
+
card,
|
|
11132
|
+
column,
|
|
11133
|
+
labels,
|
|
11134
|
+
score: breakdown.total,
|
|
11135
|
+
breakdown
|
|
11136
|
+
});
|
|
11137
|
+
}
|
|
11138
|
+
candidates.sort((a, b) => {
|
|
11139
|
+
if (b.score !== a.score)
|
|
11140
|
+
return b.score - a.score;
|
|
11141
|
+
const pinned = Number(b.card.pinned === true) - Number(a.card.pinned === true);
|
|
11142
|
+
if (pinned !== 0)
|
|
11143
|
+
return pinned;
|
|
11144
|
+
if (a.card.position !== b.card.position) {
|
|
11145
|
+
return a.card.position - b.card.position;
|
|
11146
|
+
}
|
|
11147
|
+
return a.card.short_id - b.card.short_id;
|
|
11148
|
+
});
|
|
11149
|
+
return { candidates, skipped };
|
|
11150
|
+
}
|
|
11151
|
+
async function sweepForCard(opts) {
|
|
11152
|
+
const { client, projectId, agentId, maxProbes } = opts;
|
|
11153
|
+
const chain2 = new Map;
|
|
11154
|
+
let { candidates, skipped } = rankSweepCandidates(opts);
|
|
11155
|
+
if (candidates.length === 0) {
|
|
11156
|
+
log31.debug(TAG29, `no claimable card (${skipped.length} skipped in a pickup column)`);
|
|
11157
|
+
return { claimed: null, skipped, probed: 0 };
|
|
11158
|
+
}
|
|
11159
|
+
const probeWindow = candidates.slice(0, maxProbes);
|
|
11160
|
+
const successorTermCounts = opts.ranking.successorWeight !== 0 && opts.ranking.successorCap !== 0;
|
|
11161
|
+
if (successorTermCounts && probeWindow.length > 1) {
|
|
11162
|
+
for (const c of probeWindow) {
|
|
11163
|
+
chain2.set(c.card.id, await getChainSignals(client, c.card, projectId, opts.columns));
|
|
11164
|
+
}
|
|
11165
|
+
({ candidates, skipped } = rankSweepCandidates({
|
|
11166
|
+
...opts,
|
|
11167
|
+
successorCounts: successorCounts(chain2)
|
|
11168
|
+
}));
|
|
11169
|
+
} else if (successorTermCounts && candidates.length > 1) {
|
|
11170
|
+
log31.debug(TAG29, `blocker depth not ranked — maxProbesPerTick=${maxProbes} leaves nothing to reorder among ${candidates.length} candidates`);
|
|
11171
|
+
}
|
|
11172
|
+
let probed = 0;
|
|
11173
|
+
for (const candidate of candidates) {
|
|
11174
|
+
if (probed >= maxProbes) {
|
|
11175
|
+
log31.debug(TAG29, `probe budget ${maxProbes} spent — ${candidates.length - probed} candidate(s) deferred to the next tick`);
|
|
11176
|
+
break;
|
|
11177
|
+
}
|
|
11178
|
+
probed++;
|
|
11179
|
+
const { card } = candidate;
|
|
11180
|
+
if (await hasLiveSession(client, card.id)) {
|
|
11181
|
+
skipped.push({
|
|
11182
|
+
cardId: card.id,
|
|
11183
|
+
shortId: card.short_id,
|
|
11184
|
+
reason: "live_session",
|
|
11185
|
+
detail: "a live agent session already holds this card"
|
|
11186
|
+
});
|
|
11187
|
+
continue;
|
|
11188
|
+
}
|
|
11189
|
+
let signals = chain2.get(card.id);
|
|
11190
|
+
if (!signals) {
|
|
11191
|
+
signals = await getChainSignals(client, card, projectId, opts.columns);
|
|
11192
|
+
chain2.set(card.id, signals);
|
|
11193
|
+
}
|
|
11194
|
+
const { blockers } = signals;
|
|
11195
|
+
if (blockers === null) {
|
|
11196
|
+
skipped.push({
|
|
11197
|
+
cardId: card.id,
|
|
11198
|
+
shortId: card.short_id,
|
|
11199
|
+
reason: "blockers_unknown",
|
|
11200
|
+
detail: "blocker lookup failed — deferring to the next tick"
|
|
11201
|
+
});
|
|
11202
|
+
continue;
|
|
11203
|
+
}
|
|
11204
|
+
if (blockers.length > 0) {
|
|
11205
|
+
skipped.push({
|
|
11206
|
+
cardId: card.id,
|
|
11207
|
+
shortId: card.short_id,
|
|
11208
|
+
reason: "blocked",
|
|
11209
|
+
detail: `blocked by ${blockers.map((b) => `#${b.shortId}`).join(", ")}`
|
|
11210
|
+
});
|
|
11211
|
+
continue;
|
|
11212
|
+
}
|
|
11213
|
+
const fresh = await refetchCard(client, card.id);
|
|
11214
|
+
if (!fresh) {
|
|
11215
|
+
skipped.push({
|
|
11216
|
+
cardId: card.id,
|
|
11217
|
+
shortId: card.short_id,
|
|
11218
|
+
reason: "recheck_failed",
|
|
11219
|
+
detail: "card re-read failed — deferring to the next tick"
|
|
11220
|
+
});
|
|
11221
|
+
continue;
|
|
11222
|
+
}
|
|
11223
|
+
const recheck = rankSweepCandidates({
|
|
11224
|
+
...opts,
|
|
11225
|
+
cards: [fresh],
|
|
11226
|
+
successorCounts: successorCounts(chain2)
|
|
11227
|
+
});
|
|
11228
|
+
const verified = recheck.candidates[0];
|
|
11229
|
+
if (!verified) {
|
|
11230
|
+
const reason = recheck.skipped[0];
|
|
11231
|
+
skipped.push({
|
|
11232
|
+
cardId: card.id,
|
|
11233
|
+
shortId: card.short_id,
|
|
11234
|
+
reason: reason?.reason ?? "recheck_out_of_scope",
|
|
11235
|
+
detail: reason ? `changed since the board read — ${reason.detail}` : "no longer in a pickup column"
|
|
11236
|
+
});
|
|
11237
|
+
continue;
|
|
11238
|
+
}
|
|
11239
|
+
const won = await claimUnassignedCard(client, card.id, agentId, {
|
|
11240
|
+
requireUnassigned: true
|
|
11241
|
+
});
|
|
11242
|
+
if (!won) {
|
|
11243
|
+
skipped.push({
|
|
11244
|
+
cardId: card.id,
|
|
11245
|
+
shortId: card.short_id,
|
|
11246
|
+
reason: "claim_lost",
|
|
11247
|
+
detail: "another daemon or a person claimed it first"
|
|
11248
|
+
});
|
|
11249
|
+
continue;
|
|
11250
|
+
}
|
|
11251
|
+
verified.card.assigned_agent_id = agentId;
|
|
11252
|
+
log31.info(TAG29, claimLine(verified, probed, candidates, skipped));
|
|
11253
|
+
return { claimed: verified, skipped, probed };
|
|
11254
|
+
}
|
|
11255
|
+
log31.debug(TAG29, `claimed nothing this sweep (${probed} probed, ${skipped.length} skipped)`);
|
|
11256
|
+
return { claimed: null, skipped, probed };
|
|
11257
|
+
}
|
|
11258
|
+
function claimLine(claimed, rank, candidates, skipped) {
|
|
11259
|
+
const below = candidates[rank];
|
|
11260
|
+
const versus = candidates.length === 1 ? "the only candidate" : below ? `ahead of #${below.card.short_id} (score=${below.score})` : "last in the ranking";
|
|
11261
|
+
const passedOver = rank > 1 ? `, ${rank - 1} higher-ranked passed over` : "";
|
|
11262
|
+
return `claimed #${claimed.card.short_id} "${claimed.card.title}" ` + `in "${claimed.column.name}" — ${formatBreakdown(claimed.breakdown)}, ` + `rank ${rank} of ${candidates.length}${passedOver}, ${versus}; ` + `${skipped.length} skipped`;
|
|
11263
|
+
}
|
|
11264
|
+
function successorCounts(chain2) {
|
|
11265
|
+
return new Map([...chain2].map(([id, signals]) => [id, signals.successors]));
|
|
11266
|
+
}
|
|
11267
|
+
async function refetchCard(client, cardId) {
|
|
11268
|
+
try {
|
|
11269
|
+
const { card } = await client.getCard(cardId);
|
|
11270
|
+
return card ?? null;
|
|
11271
|
+
} catch (err) {
|
|
11272
|
+
log31.warn(TAG29, `re-read failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
11273
|
+
return null;
|
|
11274
|
+
}
|
|
11275
|
+
}
|
|
11276
|
+
async function hasLiveSession(client, cardId) {
|
|
11277
|
+
try {
|
|
11278
|
+
const { session } = await client.getAgentSession(cardId);
|
|
11279
|
+
return session != null;
|
|
11280
|
+
} catch (err) {
|
|
11281
|
+
log31.warn(TAG29, `session lookup failed for ${cardId} — treating as busy: ${err instanceof Error ? err.message : err}`);
|
|
11282
|
+
return true;
|
|
11283
|
+
}
|
|
11284
|
+
}
|
|
11285
|
+
var TAG29 = "sweep";
|
|
11286
|
+
var init_sweep = __esm(() => {
|
|
11287
|
+
init_board_helpers();
|
|
11288
|
+
init_claim();
|
|
11289
|
+
init_queue();
|
|
11290
|
+
init_types2();
|
|
11291
|
+
init_unblock();
|
|
11292
|
+
});
|
|
11293
|
+
|
|
10717
11294
|
// src/reconcile.ts
|
|
10718
|
-
import { detectGitProvider as detectGitProvider5, log as
|
|
11295
|
+
import { detectGitProvider as detectGitProvider5, log as log32 } from "@gethmy/harness";
|
|
10719
11296
|
|
|
10720
11297
|
class Reconciler {
|
|
10721
11298
|
client;
|
|
@@ -10728,16 +11305,19 @@ class Reconciler {
|
|
|
10728
11305
|
intervalMs;
|
|
10729
11306
|
stateStore;
|
|
10730
11307
|
agentConfig;
|
|
11308
|
+
agentUserId;
|
|
11309
|
+
sweepGuard;
|
|
10731
11310
|
timer = null;
|
|
10732
11311
|
lastTickAt = null;
|
|
10733
11312
|
gitProvider = null;
|
|
11313
|
+
lastSweepStopReported = null;
|
|
10734
11314
|
get lastTick() {
|
|
10735
11315
|
return this.lastTickAt;
|
|
10736
11316
|
}
|
|
10737
11317
|
get isRunning() {
|
|
10738
11318
|
return this.timer !== null;
|
|
10739
11319
|
}
|
|
10740
|
-
constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig) {
|
|
11320
|
+
constructor(client, pool, projectId, agentId, pickupColumns, reviewColumns, approvedLabel, intervalMs = 60000, stateStore, agentConfig, agentUserId, sweepGuard) {
|
|
10741
11321
|
this.client = client;
|
|
10742
11322
|
this.pool = pool;
|
|
10743
11323
|
this.projectId = projectId;
|
|
@@ -10748,6 +11328,8 @@ class Reconciler {
|
|
|
10748
11328
|
this.intervalMs = intervalMs;
|
|
10749
11329
|
this.stateStore = stateStore;
|
|
10750
11330
|
this.agentConfig = agentConfig;
|
|
11331
|
+
this.agentUserId = agentUserId;
|
|
11332
|
+
this.sweepGuard = sweepGuard;
|
|
10751
11333
|
}
|
|
10752
11334
|
start() {
|
|
10753
11335
|
this.tick();
|
|
@@ -10758,7 +11340,7 @@ class Reconciler {
|
|
|
10758
11340
|
clearInterval(this.timer);
|
|
10759
11341
|
this.timer = null;
|
|
10760
11342
|
}
|
|
10761
|
-
|
|
11343
|
+
log32.info(TAG30, "Heartbeat stopped");
|
|
10762
11344
|
}
|
|
10763
11345
|
async recoverStaleRuns() {
|
|
10764
11346
|
if (!this.stateStore || !this.agentConfig)
|
|
@@ -10769,7 +11351,7 @@ class Reconciler {
|
|
|
10769
11351
|
const pool = this.pool;
|
|
10770
11352
|
for (const run of active) {
|
|
10771
11353
|
if (isBudgetHeldRun(run)) {
|
|
10772
|
-
|
|
11354
|
+
log32.info(TAG30, `run ${run.runId} (#${run.cardShortId}) is held for a human budget decision — leaving it`);
|
|
10773
11355
|
continue;
|
|
10774
11356
|
}
|
|
10775
11357
|
const foreignDaemon = run.daemonPid !== process.pid;
|
|
@@ -10779,7 +11361,7 @@ class Reconciler {
|
|
|
10779
11361
|
if (!daemonDead && !(heartbeatStale && ourZombie))
|
|
10780
11362
|
continue;
|
|
10781
11363
|
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`;
|
|
10782
|
-
|
|
11364
|
+
log32.warn(TAG30, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
|
|
10783
11365
|
await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
|
|
10784
11366
|
runId: run.runId,
|
|
10785
11367
|
cardId: run.cardId,
|
|
@@ -10806,11 +11388,11 @@ class Reconciler {
|
|
|
10806
11388
|
const stalledAt = Date.parse(card.updated_at ?? "");
|
|
10807
11389
|
if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
|
|
10808
11390
|
continue;
|
|
10809
|
-
|
|
11391
|
+
log32.warn(TAG30, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
|
|
10810
11392
|
try {
|
|
10811
11393
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
10812
11394
|
} catch (err) {
|
|
10813
|
-
|
|
11395
|
+
log32.error(TAG30, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
10814
11396
|
}
|
|
10815
11397
|
}
|
|
10816
11398
|
}
|
|
@@ -10842,7 +11424,7 @@ class Reconciler {
|
|
|
10842
11424
|
return;
|
|
10843
11425
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
10844
11426
|
const subtasks = card.subtasks ?? [];
|
|
10845
|
-
|
|
11427
|
+
log32.info(TAG30, `Enqueuing claimed review card #${card.short_id} (agent-agnostic pickup)`);
|
|
10846
11428
|
await this.pool.enqueue(card, column, cardLabels, subtasks, "review");
|
|
10847
11429
|
}
|
|
10848
11430
|
});
|
|
@@ -10866,14 +11448,63 @@ class Reconciler {
|
|
|
10866
11448
|
const parkedAt = Date.parse(card.updated_at ?? "");
|
|
10867
11449
|
if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
|
|
10868
11450
|
continue;
|
|
10869
|
-
|
|
11451
|
+
log32.warn(TAG30, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
|
|
10870
11452
|
try {
|
|
10871
11453
|
await this.client.moveCard(card.id, pickupCol.id);
|
|
10872
11454
|
} catch (err) {
|
|
10873
|
-
|
|
11455
|
+
log32.error(TAG30, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
|
|
10874
11456
|
}
|
|
10875
11457
|
}
|
|
10876
11458
|
}
|
|
11459
|
+
async sweepForWork(cards, columns, labelMap, pickupConfig) {
|
|
11460
|
+
const sweep = this.agentConfig?.sweep;
|
|
11461
|
+
if (!sweep?.enabled)
|
|
11462
|
+
return;
|
|
11463
|
+
const trustedAuthors = new Set(this.agentUserId ? [...sweep.trustedAuthors, this.agentUserId] : sweep.trustedAuthors);
|
|
11464
|
+
const rankOpts = {
|
|
11465
|
+
agentId: this.agentId,
|
|
11466
|
+
cards,
|
|
11467
|
+
columns,
|
|
11468
|
+
labelMap,
|
|
11469
|
+
pickupConfig,
|
|
11470
|
+
approvedLabel: this.approvedLabel,
|
|
11471
|
+
requireLabel: sweep.requireLabel,
|
|
11472
|
+
trustedAuthors,
|
|
11473
|
+
knownCardIds: this.pool.knownCardIds(),
|
|
11474
|
+
budgetCheck: (cardId) => this.pool.checkBudget(cardId),
|
|
11475
|
+
score: (card, column, labels, signals) => this.pool.scoreCardBreakdown(card, column, labels, signals),
|
|
11476
|
+
now: Date.now()
|
|
11477
|
+
};
|
|
11478
|
+
const verdict = this.sweepGuard?.check() ?? { claiming: true };
|
|
11479
|
+
if (!verdict.claiming) {
|
|
11480
|
+
this.reportSweepStopped(verdict.reason, verdict.detail);
|
|
11481
|
+
return;
|
|
11482
|
+
}
|
|
11483
|
+
this.lastSweepStopReported = null;
|
|
11484
|
+
if (!this.pool.hasFreeImplementSlot()) {
|
|
11485
|
+
log32.debug(TAG30, "sweep skipped — no free implement slot");
|
|
11486
|
+
return;
|
|
11487
|
+
}
|
|
11488
|
+
const { claimed } = await sweepForCard({
|
|
11489
|
+
...rankOpts,
|
|
11490
|
+
client: this.client,
|
|
11491
|
+
projectId: this.projectId,
|
|
11492
|
+
maxProbes: sweep.maxProbesPerTick,
|
|
11493
|
+
ranking: this.agentConfig?.ranking ?? DEFAULT_AGENT_CONFIG.ranking
|
|
11494
|
+
});
|
|
11495
|
+
if (!claimed)
|
|
11496
|
+
return;
|
|
11497
|
+
await this.sweepGuard?.recordClaim();
|
|
11498
|
+
await this.pool.enqueue(claimed.card, claimed.column, claimed.labels, claimed.card.subtasks ?? [], "implement");
|
|
11499
|
+
}
|
|
11500
|
+
reportSweepStopped(reason, detail) {
|
|
11501
|
+
const line = `sweep stopped claiming — ${detail}`;
|
|
11502
|
+
if (this.lastSweepStopReported === reason)
|
|
11503
|
+
log32.debug(TAG30, line);
|
|
11504
|
+
else
|
|
11505
|
+
log32.warn(TAG30, line);
|
|
11506
|
+
this.lastSweepStopReported = reason;
|
|
11507
|
+
}
|
|
10877
11508
|
async tick() {
|
|
10878
11509
|
this.lastTickAt = Date.now();
|
|
10879
11510
|
try {
|
|
@@ -10914,21 +11545,21 @@ class Reconciler {
|
|
|
10914
11545
|
const subtasks = card.subtasks ?? [];
|
|
10915
11546
|
const mode = route.mode;
|
|
10916
11547
|
if (route.stage) {
|
|
10917
|
-
|
|
11548
|
+
log32.info(TAG30, `Stage card #${card.short_id} (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement) regardless of column`);
|
|
10918
11549
|
}
|
|
10919
11550
|
if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
|
|
10920
|
-
|
|
11551
|
+
log32.debug(TAG30, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
|
|
10921
11552
|
continue;
|
|
10922
11553
|
}
|
|
10923
11554
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
10924
|
-
|
|
11555
|
+
log32.debug(TAG30, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
|
|
10925
11556
|
continue;
|
|
10926
11557
|
}
|
|
10927
11558
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
10928
|
-
|
|
11559
|
+
log32.debug(TAG30, `Skipping #${card.short_id} — no branch or PR reference (not qualified for auto-review)`);
|
|
10929
11560
|
continue;
|
|
10930
11561
|
}
|
|
10931
|
-
|
|
11562
|
+
log32.info(TAG30, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
|
|
10932
11563
|
await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
10933
11564
|
}
|
|
10934
11565
|
}
|
|
@@ -10938,30 +11569,36 @@ class Reconciler {
|
|
|
10938
11569
|
try {
|
|
10939
11570
|
await this.pool.drainBudgetDecisions();
|
|
10940
11571
|
} catch (err) {
|
|
10941
|
-
|
|
11572
|
+
log32.error(TAG30, `budget decisions were not drained this tick: ${err instanceof Error ? err.message : err}`);
|
|
10942
11573
|
}
|
|
10943
11574
|
await this.recoverStrandedInProgress(cards, columns, knownCardIds);
|
|
10944
11575
|
await this.recoverStrandedReview(cards, columns, labelMap, knownCardIds);
|
|
10945
11576
|
for (const knownId of knownCardIds) {
|
|
10946
11577
|
if (!allAgentCardIds.has(knownId)) {
|
|
10947
|
-
|
|
11578
|
+
log32.info(TAG30, `Missed unassign: ${knownId} — removing`);
|
|
10948
11579
|
await this.pool.removeCard(knownId);
|
|
10949
11580
|
}
|
|
10950
11581
|
}
|
|
10951
11582
|
await this.releaseStalledApprovals(cards, columns, knownCardIds);
|
|
10952
|
-
|
|
11583
|
+
try {
|
|
11584
|
+
await this.sweepForWork(cards, columns, labelMap, pickupConfig);
|
|
11585
|
+
} catch (err) {
|
|
11586
|
+
log32.error(TAG30, `sweep failed this tick: ${err instanceof Error ? err.message : err}`);
|
|
11587
|
+
}
|
|
11588
|
+
log32.debug(TAG30, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
|
|
10953
11589
|
} catch (err) {
|
|
10954
|
-
|
|
11590
|
+
log32.error(TAG30, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
|
|
10955
11591
|
}
|
|
10956
11592
|
}
|
|
10957
11593
|
}
|
|
10958
|
-
var
|
|
11594
|
+
var TAG30 = "reconcile";
|
|
10959
11595
|
var init_reconcile = __esm(() => {
|
|
10960
11596
|
init_board_helpers();
|
|
10961
11597
|
init_recovery();
|
|
10962
11598
|
init_review_worktree();
|
|
10963
11599
|
init_state_store();
|
|
10964
11600
|
init_strand_recovery();
|
|
11601
|
+
init_sweep();
|
|
10965
11602
|
init_types2();
|
|
10966
11603
|
});
|
|
10967
11604
|
|
|
@@ -10970,7 +11607,7 @@ var exports_startup_banner = {};
|
|
|
10970
11607
|
__export(exports_startup_banner, {
|
|
10971
11608
|
createStartupBanner: () => createStartupBanner
|
|
10972
11609
|
});
|
|
10973
|
-
import { isPretty, log as
|
|
11610
|
+
import { isPretty, log as log33 } from "@gethmy/harness";
|
|
10974
11611
|
function createStartupBanner(config, version) {
|
|
10975
11612
|
return isPretty() ? prettyBanner(config, version) : jsonBanner(config, version);
|
|
10976
11613
|
}
|
|
@@ -10995,7 +11632,7 @@ function prettyBanner(config, version) {
|
|
|
10995
11632
|
checks.push({ kind: "ok", message });
|
|
10996
11633
|
},
|
|
10997
11634
|
warn(message) {
|
|
10998
|
-
|
|
11635
|
+
log33.warn(TAG31, message);
|
|
10999
11636
|
checks.push({ kind: "warn", message: message.split(`
|
|
11000
11637
|
`, 1)[0] });
|
|
11001
11638
|
},
|
|
@@ -11020,25 +11657,25 @@ function prettyBanner(config, version) {
|
|
|
11020
11657
|
};
|
|
11021
11658
|
}
|
|
11022
11659
|
function jsonBanner(config, version) {
|
|
11023
|
-
|
|
11024
|
-
|
|
11660
|
+
log33.info(TAG31, `Harmony Agent Daemon v${version} starting...`);
|
|
11661
|
+
log33.info(TAG31, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
|
|
11025
11662
|
if (config.agent.review.enabled) {
|
|
11026
|
-
|
|
11663
|
+
log33.info(TAG31, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
|
|
11027
11664
|
}
|
|
11028
11665
|
let failed = false;
|
|
11029
11666
|
return {
|
|
11030
11667
|
setProjectName(_name) {},
|
|
11031
11668
|
setGitProvider(provider) {
|
|
11032
|
-
|
|
11669
|
+
log33.info(TAG31, `Git provider: ${provider}`);
|
|
11033
11670
|
},
|
|
11034
11671
|
setHttpPort(port) {
|
|
11035
|
-
|
|
11672
|
+
log33.info(TAG31, `HTTP server on port ${port}`);
|
|
11036
11673
|
},
|
|
11037
11674
|
check(message) {
|
|
11038
|
-
|
|
11675
|
+
log33.info(TAG31, message);
|
|
11039
11676
|
},
|
|
11040
11677
|
warn(message) {
|
|
11041
|
-
|
|
11678
|
+
log33.warn(TAG31, message);
|
|
11042
11679
|
},
|
|
11043
11680
|
fail() {
|
|
11044
11681
|
failed = true;
|
|
@@ -11046,7 +11683,7 @@ function jsonBanner(config, version) {
|
|
|
11046
11683
|
async ready(message) {
|
|
11047
11684
|
if (failed)
|
|
11048
11685
|
return;
|
|
11049
|
-
|
|
11686
|
+
log33.info(TAG31, message);
|
|
11050
11687
|
}
|
|
11051
11688
|
};
|
|
11052
11689
|
}
|
|
@@ -11127,7 +11764,7 @@ function cyan(s) {
|
|
|
11127
11764
|
function yellow(s) {
|
|
11128
11765
|
return `${ANSI.yellow}${s}${ANSI.reset}`;
|
|
11129
11766
|
}
|
|
11130
|
-
var
|
|
11767
|
+
var TAG31 = "daemon", RULE_WIDTH = 70, ANSI;
|
|
11131
11768
|
var init_startup_banner = __esm(() => {
|
|
11132
11769
|
ANSI = {
|
|
11133
11770
|
reset: "\x1B[0m",
|
|
@@ -11228,9 +11865,118 @@ var init_stream_parser_selftest = __esm(() => {
|
|
|
11228
11865
|
init_stream_parser();
|
|
11229
11866
|
});
|
|
11230
11867
|
|
|
11868
|
+
// src/sweep-guard.ts
|
|
11869
|
+
import { log as log34 } from "@gethmy/harness";
|
|
11870
|
+
|
|
11871
|
+
class SweepGuard {
|
|
11872
|
+
config;
|
|
11873
|
+
store;
|
|
11874
|
+
checkDailyBudget;
|
|
11875
|
+
constructor(config, store, checkDailyBudget) {
|
|
11876
|
+
this.config = config;
|
|
11877
|
+
this.store = store;
|
|
11878
|
+
this.checkDailyBudget = checkDailyBudget;
|
|
11879
|
+
}
|
|
11880
|
+
check() {
|
|
11881
|
+
const sweep = this.store.getSweep();
|
|
11882
|
+
if (sweep.haltReason) {
|
|
11883
|
+
return {
|
|
11884
|
+
claiming: false,
|
|
11885
|
+
reason: sweep.haltReason,
|
|
11886
|
+
detail: this.describeStop(sweep.haltReason),
|
|
11887
|
+
latched: true
|
|
11888
|
+
};
|
|
11889
|
+
}
|
|
11890
|
+
const cap = this.config.maxCardsPerSweep;
|
|
11891
|
+
if (cap >= 0 && sweep.claimed >= cap) {
|
|
11892
|
+
return {
|
|
11893
|
+
claiming: false,
|
|
11894
|
+
reason: "card_cap",
|
|
11895
|
+
detail: this.describeStop("card_cap"),
|
|
11896
|
+
latched: true
|
|
11897
|
+
};
|
|
11898
|
+
}
|
|
11899
|
+
const daily = this.checkDailyBudget?.();
|
|
11900
|
+
if (daily && !daily.allow) {
|
|
11901
|
+
return {
|
|
11902
|
+
claiming: false,
|
|
11903
|
+
latched: false,
|
|
11904
|
+
reason: "daily_budget",
|
|
11905
|
+
detail: this.describeStop("daily_budget", daily.detail)
|
|
11906
|
+
};
|
|
11907
|
+
}
|
|
11908
|
+
return { claiming: true };
|
|
11909
|
+
}
|
|
11910
|
+
async recordClaim() {
|
|
11911
|
+
await this.store.recordSweepClaim();
|
|
11912
|
+
const verdict = this.check();
|
|
11913
|
+
if (!verdict.claiming && verdict.latched)
|
|
11914
|
+
await this.halt(verdict.reason);
|
|
11915
|
+
return verdict;
|
|
11916
|
+
}
|
|
11917
|
+
async halt(reason) {
|
|
11918
|
+
const already = this.store.getSweep().haltReason === reason;
|
|
11919
|
+
await this.store.haltSweep(reason);
|
|
11920
|
+
if (!already) {
|
|
11921
|
+
const claimed = this.store.getSweep().claimed;
|
|
11922
|
+
log34.info(TAG32, `claiming stopped (${reason}) after ${claimed} claimed`);
|
|
11923
|
+
}
|
|
11924
|
+
return this.snapshot();
|
|
11925
|
+
}
|
|
11926
|
+
async resume() {
|
|
11927
|
+
const before = this.store.getSweep();
|
|
11928
|
+
const was = before.haltReason ? this.describeStop(before.haltReason) : null;
|
|
11929
|
+
await this.store.resumeSweep();
|
|
11930
|
+
if (was)
|
|
11931
|
+
log34.info(TAG32, `claiming resumed by the operator — was stopped: ${was}`);
|
|
11932
|
+
return this.snapshot();
|
|
11933
|
+
}
|
|
11934
|
+
snapshot() {
|
|
11935
|
+
const sweep = this.store.getSweep();
|
|
11936
|
+
const cap = this.config.maxCardsPerSweep;
|
|
11937
|
+
const verdict = this.check();
|
|
11938
|
+
return {
|
|
11939
|
+
enabled: this.config.enabled,
|
|
11940
|
+
claiming: verdict.claiming,
|
|
11941
|
+
haltReason: verdict.claiming ? null : verdict.reason,
|
|
11942
|
+
haltedAt: sweep.haltedAt,
|
|
11943
|
+
claimed: sweep.claimed,
|
|
11944
|
+
maxCardsPerSweep: cap < 0 ? null : cap,
|
|
11945
|
+
totalClaimed: sweep.totalClaimed,
|
|
11946
|
+
detail: verdict.claiming ? null : verdict.detail
|
|
11947
|
+
};
|
|
11948
|
+
}
|
|
11949
|
+
describeStop(reason, budgetDetail) {
|
|
11950
|
+
const claimed = this.store.getSweep().claimed;
|
|
11951
|
+
const cards = `${claimed} card${claimed === 1 ? "" : "s"}`;
|
|
11952
|
+
const resume = "Resume with `harmony-agent sweep resume`.";
|
|
11953
|
+
switch (reason) {
|
|
11954
|
+
case "card_cap":
|
|
11955
|
+
return `card cap reached — ${cards} claimed this sweep, cap is ${this.config.maxCardsPerSweep}. ${resume}`;
|
|
11956
|
+
case "operator":
|
|
11957
|
+
return `stopped by the operator after ${cards}. ${resume}`;
|
|
11958
|
+
case "daily_budget":
|
|
11959
|
+
return `daily spend cap reached${budgetDetail ? ` (${budgetDetail})` : ""} after ${cards} — claiming starts again on its own when the UTC day rolls over.`;
|
|
11960
|
+
}
|
|
11961
|
+
}
|
|
11962
|
+
}
|
|
11963
|
+
function describeCaps(sweep, dailyBudgetCents) {
|
|
11964
|
+
const cards = sweep.maxCardsPerSweep < 0 ? "no cap" : String(sweep.maxCardsPerSweep);
|
|
11965
|
+
return `cards/sweep ${cards} · spend/day ${formatDailyCap(dailyBudgetCents)}`;
|
|
11966
|
+
}
|
|
11967
|
+
function sweepBannerLine(config) {
|
|
11968
|
+
const sweep = config.sweep;
|
|
11969
|
+
const scope = sweep.requireLabel ? `cards labelled "${sweep.requireLabel}"` : "unassigned cards";
|
|
11970
|
+
const extra = sweep.trustedAuthors.length;
|
|
11971
|
+
const authors = extra ? `you + ${extra} trusted author${extra === 1 ? "" : "s"}` : "you only";
|
|
11972
|
+
return `Sweep ON — claims ${scope} in ${config.pickupColumns.join(", ")}, ` + `authored by ${authors}; caps: ${describeCaps(sweep, config.budget.dailyBudgetCents)}; ` + `${sweep.maxProbesPerTick} probes/tick`;
|
|
11973
|
+
}
|
|
11974
|
+
var TAG32 = "sweep";
|
|
11975
|
+
var init_sweep_guard = () => {};
|
|
11976
|
+
|
|
11231
11977
|
// src/watcher.ts
|
|
11232
11978
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
11233
|
-
import { isPretty as isPretty2, log as
|
|
11979
|
+
import { isPretty as isPretty2, log as log35 } from "@gethmy/harness";
|
|
11234
11980
|
import { createClient } from "@supabase/supabase-js";
|
|
11235
11981
|
|
|
11236
11982
|
class Watcher {
|
|
@@ -11281,7 +12027,7 @@ class Watcher {
|
|
|
11281
12027
|
}
|
|
11282
12028
|
async start() {
|
|
11283
12029
|
if (!isPretty2()) {
|
|
11284
|
-
|
|
12030
|
+
log35.info(TAG33, "Connecting to Supabase realtime (broadcast)...");
|
|
11285
12031
|
}
|
|
11286
12032
|
this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
|
|
11287
12033
|
this.subscribeBroadcast();
|
|
@@ -11294,7 +12040,7 @@ class Watcher {
|
|
|
11294
12040
|
const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
|
|
11295
12041
|
this.presenceChannel = presenceChannel;
|
|
11296
12042
|
presenceChannel.on("presence", { event: "sync" }, () => {
|
|
11297
|
-
|
|
12043
|
+
log35.debug(TAG33, "Presence sync");
|
|
11298
12044
|
}).subscribe(async (status) => {
|
|
11299
12045
|
if (gen !== this.presenceGen)
|
|
11300
12046
|
return;
|
|
@@ -11318,13 +12064,13 @@ class Watcher {
|
|
|
11318
12064
|
if (trackStatus !== "ok") {
|
|
11319
12065
|
this.presenceTracked = false;
|
|
11320
12066
|
if (!this.stopping) {
|
|
11321
|
-
|
|
12067
|
+
log35.warn(TAG33, `Presence track returned "${trackStatus}" — scheduling reconnect`);
|
|
11322
12068
|
this.schedulePresenceReconnect();
|
|
11323
12069
|
}
|
|
11324
12070
|
return;
|
|
11325
12071
|
}
|
|
11326
12072
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
11327
|
-
|
|
12073
|
+
log35.info(TAG33, "Presence tracked on board-presence channel");
|
|
11328
12074
|
}
|
|
11329
12075
|
this.presenceTracked = true;
|
|
11330
12076
|
this.presenceReconnectAttempts = 0;
|
|
@@ -11332,7 +12078,7 @@ class Watcher {
|
|
|
11332
12078
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
11333
12079
|
this.presenceTracked = false;
|
|
11334
12080
|
if (!this.stopping) {
|
|
11335
|
-
|
|
12081
|
+
log35.warn(TAG33, `Presence subscription ${status} — scheduling reconnect`);
|
|
11336
12082
|
this.schedulePresenceReconnect();
|
|
11337
12083
|
}
|
|
11338
12084
|
}
|
|
@@ -11351,7 +12097,7 @@ class Watcher {
|
|
|
11351
12097
|
async reconnectPresence() {
|
|
11352
12098
|
if (this.stopping || !this.supabase)
|
|
11353
12099
|
return;
|
|
11354
|
-
|
|
12100
|
+
log35.warn(TAG33, `Reconnecting presence subscription (attempt ${this.presenceReconnectAttempts})`);
|
|
11355
12101
|
if (this.presenceChannel) {
|
|
11356
12102
|
const old = this.presenceChannel;
|
|
11357
12103
|
this.presenceChannel = null;
|
|
@@ -11369,13 +12115,13 @@ class Watcher {
|
|
|
11369
12115
|
return;
|
|
11370
12116
|
const gen = ++this.broadcastGen;
|
|
11371
12117
|
this.channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
|
|
11372
|
-
|
|
12118
|
+
log35.debug(TAG33, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
|
|
11373
12119
|
this.onCardBroadcast({
|
|
11374
12120
|
event: "card_update",
|
|
11375
12121
|
payload: msg.payload ?? {}
|
|
11376
12122
|
});
|
|
11377
12123
|
}).on("broadcast", { event: "card_created" }, (msg) => {
|
|
11378
|
-
|
|
12124
|
+
log35.debug(TAG33, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
|
|
11379
12125
|
this.onCardBroadcast({
|
|
11380
12126
|
event: "card_created",
|
|
11381
12127
|
payload: msg.payload ?? {}
|
|
@@ -11385,7 +12131,7 @@ class Watcher {
|
|
|
11385
12131
|
const cardId = payload.card_id;
|
|
11386
12132
|
const command = payload.command;
|
|
11387
12133
|
if (cardId && command) {
|
|
11388
|
-
|
|
12134
|
+
log35.info(TAG33, `Broadcast: agent_command ${command} for ${cardId}`);
|
|
11389
12135
|
this.onAgentCommand?.({ cardId, command });
|
|
11390
12136
|
}
|
|
11391
12137
|
}).subscribe((status) => {
|
|
@@ -11395,13 +12141,13 @@ class Watcher {
|
|
|
11395
12141
|
this.connected = true;
|
|
11396
12142
|
this.reconnectAttempts = 0;
|
|
11397
12143
|
if (!isPretty2() || !this.suppressStartupLogs) {
|
|
11398
|
-
|
|
12144
|
+
log35.info(TAG33, "Broadcast subscription active");
|
|
11399
12145
|
}
|
|
11400
12146
|
this.maybeResolveReady();
|
|
11401
12147
|
} else if (status === "CHANNEL_ERROR" || status === "TIMED_OUT" || status === "CLOSED") {
|
|
11402
12148
|
this.connected = false;
|
|
11403
12149
|
if (!this.stopping) {
|
|
11404
|
-
|
|
12150
|
+
log35.warn(TAG33, `Broadcast subscription ${status} — scheduling reconnect`);
|
|
11405
12151
|
this.scheduleReconnect();
|
|
11406
12152
|
}
|
|
11407
12153
|
}
|
|
@@ -11420,7 +12166,7 @@ class Watcher {
|
|
|
11420
12166
|
async reconnectBroadcast() {
|
|
11421
12167
|
if (this.stopping || !this.supabase)
|
|
11422
12168
|
return;
|
|
11423
|
-
|
|
12169
|
+
log35.warn(TAG33, `Reconnecting broadcast subscription (attempt ${this.reconnectAttempts})`);
|
|
11424
12170
|
if (this.channel) {
|
|
11425
12171
|
const old = this.channel;
|
|
11426
12172
|
this.channel = null;
|
|
@@ -11457,10 +12203,10 @@ class Watcher {
|
|
|
11457
12203
|
}
|
|
11458
12204
|
this.connected = false;
|
|
11459
12205
|
this.presenceTracked = false;
|
|
11460
|
-
|
|
12206
|
+
log35.info(TAG33, "Broadcast subscription stopped");
|
|
11461
12207
|
}
|
|
11462
12208
|
}
|
|
11463
|
-
var
|
|
12209
|
+
var TAG33 = "watcher";
|
|
11464
12210
|
var init_watcher = () => {};
|
|
11465
12211
|
|
|
11466
12212
|
// src/worktree-gc.ts
|
|
@@ -11474,7 +12220,7 @@ __export(exports_worktree_gc, {
|
|
|
11474
12220
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
11475
12221
|
import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "node:fs";
|
|
11476
12222
|
import { resolve as resolve2 } from "node:path";
|
|
11477
|
-
import { cleanupWorktree as cleanupWorktree4, log as
|
|
12223
|
+
import { cleanupWorktree as cleanupWorktree4, log as log36 } from "@gethmy/harness";
|
|
11478
12224
|
function isTransientGitNetworkError(message) {
|
|
11479
12225
|
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
11480
12226
|
}
|
|
@@ -11587,10 +12333,10 @@ function runWorktreeGc(basePath, store, opts = {}) {
|
|
|
11587
12333
|
});
|
|
11588
12334
|
} catch {}
|
|
11589
12335
|
if (result.removed.length > 0) {
|
|
11590
|
-
|
|
12336
|
+
log36.info(TAG34, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
|
|
11591
12337
|
}
|
|
11592
12338
|
if (result.errors.length > 0) {
|
|
11593
|
-
|
|
12339
|
+
log36.warn(TAG34, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
|
|
11594
12340
|
}
|
|
11595
12341
|
return result;
|
|
11596
12342
|
}
|
|
@@ -11620,7 +12366,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
11620
12366
|
} catch (err) {
|
|
11621
12367
|
const detail = gitErrorDetail2(err);
|
|
11622
12368
|
if (isTransientGitNetworkError(detail)) {
|
|
11623
|
-
|
|
12369
|
+
log36.debug(TAG34, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
11624
12370
|
return result;
|
|
11625
12371
|
}
|
|
11626
12372
|
result.errors.push({ ref: "fetch", error: detail });
|
|
@@ -11659,7 +12405,7 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
11659
12405
|
continue;
|
|
11660
12406
|
}
|
|
11661
12407
|
if (clock() > sweepDeadline) {
|
|
11662
|
-
|
|
12408
|
+
log36.debug(TAG34, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
11663
12409
|
break;
|
|
11664
12410
|
}
|
|
11665
12411
|
try {
|
|
@@ -11672,17 +12418,17 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
11672
12418
|
} catch (err) {
|
|
11673
12419
|
const detail = gitErrorDetail2(err);
|
|
11674
12420
|
if (isTransientGitNetworkError(detail)) {
|
|
11675
|
-
|
|
12421
|
+
log36.debug(TAG34, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
11676
12422
|
break;
|
|
11677
12423
|
}
|
|
11678
12424
|
result.errors.push({ ref, error: detail });
|
|
11679
12425
|
}
|
|
11680
12426
|
}
|
|
11681
12427
|
if (result.removed.length > 0) {
|
|
11682
|
-
|
|
12428
|
+
log36.info(TAG34, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
|
|
11683
12429
|
}
|
|
11684
12430
|
if (result.errors.length > 0) {
|
|
11685
|
-
|
|
12431
|
+
log36.warn(TAG34, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
|
|
11686
12432
|
}
|
|
11687
12433
|
return result;
|
|
11688
12434
|
}
|
|
@@ -11713,13 +12459,13 @@ class WorktreeGc {
|
|
|
11713
12459
|
try {
|
|
11714
12460
|
runWorktreeGc(this.basePath, this.store);
|
|
11715
12461
|
} catch (err) {
|
|
11716
|
-
|
|
12462
|
+
log36.warn(TAG34, `GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
11717
12463
|
}
|
|
11718
12464
|
if (this.remoteOpts) {
|
|
11719
12465
|
try {
|
|
11720
12466
|
pruneFailedRemoteBranches(this.remoteOpts);
|
|
11721
12467
|
} catch (err) {
|
|
11722
|
-
|
|
12468
|
+
log36.warn(TAG34, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
|
|
11723
12469
|
}
|
|
11724
12470
|
}
|
|
11725
12471
|
}
|
|
@@ -11733,7 +12479,7 @@ function getRepoRoot2() {
|
|
|
11733
12479
|
return null;
|
|
11734
12480
|
}
|
|
11735
12481
|
}
|
|
11736
|
-
var
|
|
12482
|
+
var TAG34 = "worktree-gc", GIT_NETWORK_TIMEOUT_MS = 30000, GIT_SSH_CONNECT_TIMEOUT_SECS = 10, GIT_PRUNE_SWEEP_BUDGET_MS = 60000, GIT_NETWORK_EXEC, TRANSIENT_GIT_NETWORK_ERROR;
|
|
11737
12483
|
var init_worktree_gc = __esm(() => {
|
|
11738
12484
|
GIT_NETWORK_EXEC = {
|
|
11739
12485
|
timeout: GIT_NETWORK_TIMEOUT_MS,
|
|
@@ -11770,7 +12516,7 @@ import { randomUUID as randomUUID3 } from "node:crypto";
|
|
|
11770
12516
|
import { createRequire as createRequire3 } from "node:module";
|
|
11771
12517
|
import {
|
|
11772
12518
|
detectGitProvider as detectGitProvider6,
|
|
11773
|
-
log as
|
|
12519
|
+
log as log37,
|
|
11774
12520
|
validateGitProviderCli
|
|
11775
12521
|
} from "@gethmy/harness";
|
|
11776
12522
|
async function validatePrerequisites(config, banner) {
|
|
@@ -11844,21 +12590,36 @@ async function main() {
|
|
|
11844
12590
|
} catch (err) {
|
|
11845
12591
|
if (err instanceof ConfigValidationError) {
|
|
11846
12592
|
banner.fail();
|
|
11847
|
-
|
|
12593
|
+
log37.error(TAG35, err.message);
|
|
11848
12594
|
process.exit(1);
|
|
11849
12595
|
}
|
|
11850
12596
|
throw err;
|
|
11851
12597
|
}
|
|
11852
12598
|
try {
|
|
11853
12599
|
validateAutoMergeConfig(config.agent);
|
|
12600
|
+
validateBudgetConfig(config.agent);
|
|
12601
|
+
validateSweepConfig(config.agent);
|
|
12602
|
+
validateRankingConfig(config.agent);
|
|
11854
12603
|
} catch (err) {
|
|
11855
12604
|
if (err instanceof ConfigValidationError) {
|
|
11856
12605
|
banner.fail();
|
|
11857
|
-
|
|
12606
|
+
log37.error(TAG35, err.message);
|
|
11858
12607
|
process.exit(1);
|
|
11859
12608
|
}
|
|
11860
12609
|
throw err;
|
|
11861
12610
|
}
|
|
12611
|
+
if (config.agent.sweep.enabled) {
|
|
12612
|
+
banner.check(sweepBannerLine(config.agent));
|
|
12613
|
+
try {
|
|
12614
|
+
const { members } = await client.getWorkspaceMembers(config.workspaceId);
|
|
12615
|
+
const count = Array.isArray(members) ? members.length : 0;
|
|
12616
|
+
if (count > 1) {
|
|
12617
|
+
banner.warn(`Sweep is on and this workspace has ${count} members. Any member can edit a trusted author's card or comment on it, and the swept run executes that text with unrestricted Bash on this machine. Enable sweep only where every member is someone you would hand a shell.`);
|
|
12618
|
+
}
|
|
12619
|
+
} catch (err) {
|
|
12620
|
+
log37.debug(TAG35, `workspace member count unavailable for the sweep warning: ${err instanceof Error ? err.message : err}`);
|
|
12621
|
+
}
|
|
12622
|
+
}
|
|
11862
12623
|
const { agent: registeredAgent } = await client.registerWorkspaceAgent(config.workspaceId, {
|
|
11863
12624
|
identifier: config.agentIdentifier,
|
|
11864
12625
|
name: config.agentName,
|
|
@@ -11879,6 +12640,13 @@ async function main() {
|
|
|
11879
12640
|
const errored = outcomes.filter((o) => o.errors.length).length;
|
|
11880
12641
|
banner.check(`Recovery: ${outcomes.length} orphan(s) handled${errored > 0 ? `, ${errored} with errors` : ""}`);
|
|
11881
12642
|
}
|
|
12643
|
+
const sweepGuard = new SweepGuard(config.agent.sweep, stateStore, () => new BudgetGuard(config.agent.budget, stateStore).checkDailyBudget());
|
|
12644
|
+
if (config.agent.sweep.enabled) {
|
|
12645
|
+
const sweepState = sweepGuard.snapshot();
|
|
12646
|
+
if (!sweepState.claiming && sweepState.detail) {
|
|
12647
|
+
banner.warn(`Sweep is NOT claiming — ${sweepState.detail}`);
|
|
12648
|
+
}
|
|
12649
|
+
}
|
|
11882
12650
|
try {
|
|
11883
12651
|
const undeclared = await findUndeclaredGateMetrics(client, config.projectId, config.agent);
|
|
11884
12652
|
for (const finding of undeclared) {
|
|
@@ -11899,7 +12667,7 @@ async function main() {
|
|
|
11899
12667
|
pool.onCardCompleted = promoteSuccessors;
|
|
11900
12668
|
const reviewColumns = config.agent.review.enabled ? config.agent.review.pickupColumns : [];
|
|
11901
12669
|
const approvedLabel = config.agent.review.enabled ? config.agent.review.approvedLabel : "";
|
|
11902
|
-
const reconciler = new Reconciler(client, pool, config.projectId, agentId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent);
|
|
12670
|
+
const reconciler = new Reconciler(client, pool, config.projectId, agentId, config.agent.pickupColumns, reviewColumns, approvedLabel, config.agent.timing.reconcileIntervalMs, stateStore, config.agent, agentUserId, sweepGuard);
|
|
11903
12671
|
let mergeMonitor = null;
|
|
11904
12672
|
if (config.agent.review.enabled && config.agent.review.mergeMonitor) {
|
|
11905
12673
|
mergeMonitor = new MergeMonitor(client, config.projectId, config.agent);
|
|
@@ -11956,10 +12724,13 @@ async function main() {
|
|
|
11956
12724
|
budget: {
|
|
11957
12725
|
todayCents: stateStore.getDailyCostCents(),
|
|
11958
12726
|
dailyCapCents: config.agent.budget.dailyBudgetCents
|
|
11959
|
-
}
|
|
12727
|
+
},
|
|
12728
|
+
sweep: sweepGuard.snapshot()
|
|
11960
12729
|
};
|
|
11961
12730
|
},
|
|
11962
|
-
handleCommand: (cmd, cardId) => pool.handleAgentCommand(cardId, cmd)
|
|
12731
|
+
handleCommand: (cmd, cardId) => pool.handleAgentCommand(cardId, cmd),
|
|
12732
|
+
handleSweepCommand: (cmd) => cmd === "stop" ? sweepGuard.halt("operator") : sweepGuard.resume(),
|
|
12733
|
+
getSweep: () => sweepGuard.snapshot()
|
|
11963
12734
|
}) : null;
|
|
11964
12735
|
const watcher = new Watcher(realtimeCreds, config.projectId, {
|
|
11965
12736
|
userId: agentUserId,
|
|
@@ -11978,7 +12749,7 @@ async function main() {
|
|
|
11978
12749
|
if (shuttingDown)
|
|
11979
12750
|
return;
|
|
11980
12751
|
shuttingDown = true;
|
|
11981
|
-
|
|
12752
|
+
log37.info(TAG35, `Received ${signal}, shutting down gracefully...`);
|
|
11982
12753
|
reconciler.stop();
|
|
11983
12754
|
mergeMonitor?.stop();
|
|
11984
12755
|
worktreeGc.stop();
|
|
@@ -11989,18 +12760,18 @@ async function main() {
|
|
|
11989
12760
|
}
|
|
11990
12761
|
await watcher.stop();
|
|
11991
12762
|
await pool.shutdown();
|
|
11992
|
-
|
|
12763
|
+
log37.info(TAG35, "Daemon stopped.");
|
|
11993
12764
|
process.exit(exitCode);
|
|
11994
12765
|
};
|
|
11995
12766
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
11996
12767
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
11997
12768
|
process.on("uncaughtException", (err) => {
|
|
11998
|
-
|
|
12769
|
+
log37.error(TAG35, `Uncaught exception: ${err.message}`);
|
|
11999
12770
|
exitCode = 1;
|
|
12000
12771
|
shutdown("uncaughtException");
|
|
12001
12772
|
});
|
|
12002
12773
|
process.on("unhandledRejection", (reason) => {
|
|
12003
|
-
|
|
12774
|
+
log37.error(TAG35, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
|
|
12004
12775
|
exitCode = 1;
|
|
12005
12776
|
shutdown("unhandledRejection");
|
|
12006
12777
|
});
|
|
@@ -12059,29 +12830,29 @@ async function handleBroadcast(event, client, pool, config, agentId) {
|
|
|
12059
12830
|
if (assignedAgentId === undefined)
|
|
12060
12831
|
return;
|
|
12061
12832
|
if (assignedAgentId === agentId) {
|
|
12062
|
-
|
|
12833
|
+
log37.info(TAG35, `Broadcast: card ${cardId} assigned to agent`);
|
|
12063
12834
|
try {
|
|
12064
12835
|
await pool.resetAttemptsForReassign(cardId);
|
|
12065
12836
|
await tryEnqueueCard(cardId, client, pool, config, agentId);
|
|
12066
12837
|
} catch (err) {
|
|
12067
|
-
|
|
12838
|
+
log37.error(TAG35, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
|
|
12068
12839
|
}
|
|
12069
12840
|
} else if (pool.isCardKnown(cardId)) {
|
|
12070
|
-
|
|
12841
|
+
log37.info(TAG35, `Broadcast: card ${cardId} unassigned from agent`);
|
|
12071
12842
|
await pool.removeCard(cardId);
|
|
12072
12843
|
}
|
|
12073
12844
|
}
|
|
12074
12845
|
async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
12075
12846
|
const { card } = await client.getCard(cardId);
|
|
12076
12847
|
if (card.assigned_agent_id !== agentId) {
|
|
12077
|
-
|
|
12848
|
+
log37.debug(TAG35, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
12078
12849
|
return;
|
|
12079
12850
|
}
|
|
12080
12851
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
12081
12852
|
const columns = board.columns;
|
|
12082
12853
|
const column = columns.find((c) => c.id === card.column_id);
|
|
12083
12854
|
if (!column) {
|
|
12084
|
-
|
|
12855
|
+
log37.warn(TAG35, `Column not found for card ${cardId}`);
|
|
12085
12856
|
return;
|
|
12086
12857
|
}
|
|
12087
12858
|
const route = classifyPickup(card, column.name, {
|
|
@@ -12090,31 +12861,31 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
12090
12861
|
playbooks: config.agent.playbooks
|
|
12091
12862
|
});
|
|
12092
12863
|
if (!route) {
|
|
12093
|
-
|
|
12864
|
+
log37.info(TAG35, `Card #${card.short_id} is in "${column.name}", not a pickup/review/stage column — skipping`);
|
|
12094
12865
|
return;
|
|
12095
12866
|
}
|
|
12096
12867
|
if (route.stage) {
|
|
12097
|
-
|
|
12868
|
+
log37.info(TAG35, `Card #${card.short_id} is a playbook stage card (stage "${card.current_stage}") in "${column.name}" — routing to the stage executor (implement pool) regardless of column`);
|
|
12098
12869
|
}
|
|
12099
12870
|
const mode = route.mode;
|
|
12100
12871
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
12101
12872
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
12102
12873
|
const subtasks = card.subtasks ?? [];
|
|
12103
12874
|
if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
|
|
12104
|
-
|
|
12875
|
+
log37.debug(TAG35, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
|
|
12105
12876
|
return;
|
|
12106
12877
|
}
|
|
12107
12878
|
if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
|
|
12108
|
-
|
|
12879
|
+
log37.debug(TAG35, `Card #${card.short_id} has "${NEED_REVIEW_LABEL}" label (needs human) — skipping review`);
|
|
12109
12880
|
return;
|
|
12110
12881
|
}
|
|
12111
12882
|
if (mode === "review" && !qualifiesForAutoReview(card.description)) {
|
|
12112
|
-
|
|
12883
|
+
log37.info(TAG35, `Card #${card.short_id} has no branch or PR reference — skipping auto-review`);
|
|
12113
12884
|
return;
|
|
12114
12885
|
}
|
|
12115
12886
|
await pool.enqueue(card, column, cardLabels, subtasks, mode);
|
|
12116
12887
|
}
|
|
12117
|
-
var
|
|
12888
|
+
var TAG35 = "daemon", BASE_REMOTE = "origin", PKG_VERSION;
|
|
12118
12889
|
var init_src = __esm(() => {
|
|
12119
12890
|
init_base_branch();
|
|
12120
12891
|
init_board_helpers();
|
|
@@ -12132,6 +12903,7 @@ var init_src = __esm(() => {
|
|
|
12132
12903
|
init_startup_banner();
|
|
12133
12904
|
init_state_store();
|
|
12134
12905
|
init_stream_parser_selftest();
|
|
12906
|
+
init_sweep_guard();
|
|
12135
12907
|
init_types2();
|
|
12136
12908
|
init_unblock();
|
|
12137
12909
|
init_watcher();
|