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