@gethmy/agent 1.37.0 → 1.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +644 -57
- package/dist/index.js +602 -56
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -754,6 +754,9 @@ class BudgetGuard {
|
|
|
754
754
|
const turns = this.checkCardTurns(cardId);
|
|
755
755
|
if (!turns.allow)
|
|
756
756
|
return turns;
|
|
757
|
+
const runs = this.checkCardRuns(cardId);
|
|
758
|
+
if (!runs.allow)
|
|
759
|
+
return runs;
|
|
757
760
|
return this.checkDailyBudget();
|
|
758
761
|
}
|
|
759
762
|
checkCardTurns(cardId) {
|
|
@@ -769,6 +772,19 @@ class BudgetGuard {
|
|
|
769
772
|
detail: `${spent} of ${cap} turns spent on this card`
|
|
770
773
|
};
|
|
771
774
|
}
|
|
775
|
+
checkCardRuns(cardId) {
|
|
776
|
+
const cap = this.config.maxRunsPerCard;
|
|
777
|
+
if (cap < 0)
|
|
778
|
+
return { allow: true };
|
|
779
|
+
const started = this.store.getCardRunStarts(cardId);
|
|
780
|
+
if (started < cap)
|
|
781
|
+
return { allow: true };
|
|
782
|
+
return {
|
|
783
|
+
allow: false,
|
|
784
|
+
reason: "card_runs",
|
|
785
|
+
detail: `${started} of ${cap} run starts spent on this card`
|
|
786
|
+
};
|
|
787
|
+
}
|
|
772
788
|
checkDailyBudget() {
|
|
773
789
|
const cap = this.config.dailyBudgetCents;
|
|
774
790
|
if (cap < 0)
|
|
@@ -798,6 +814,17 @@ function buildTurnCapComment(spentTurns, cap, legs) {
|
|
|
798
814
|
].join(`
|
|
799
815
|
`);
|
|
800
816
|
}
|
|
817
|
+
function buildRunCapComment(runStarts, cap) {
|
|
818
|
+
return [
|
|
819
|
+
"**Run backstop reached — over to you.**",
|
|
820
|
+
`This card has cost ${runStarts} run starts, which is its ceiling of ${cap} (\`budget.maxRunsPerCard\`). I will not pick it up again, and a Continue on it would be refused.`,
|
|
821
|
+
"",
|
|
822
|
+
"This ceiling exists as a backstop against a retry loop the attempt budget cannot see — a card that reaches it has been failing repeatedly for reasons worth a look (the run log and the failure history on this card say what killed each run).",
|
|
823
|
+
"",
|
|
824
|
+
"Two ways on: reassign the card, which clears the count and hands it back to me fresh, or raise `budget.maxRunsPerCard` in the daemon config if this card genuinely needs the room."
|
|
825
|
+
].join(`
|
|
826
|
+
`);
|
|
827
|
+
}
|
|
801
828
|
function buildGaveUpComment(maxAttempts, failures, pauseEnabled) {
|
|
802
829
|
const wayBackIn = pauseEnabled ? "Continue to grant a fresh attempt, or reassign the card." : "Reassign the card to grant a fresh attempt.";
|
|
803
830
|
const lines = [
|
|
@@ -1064,6 +1091,22 @@ function validateAutoMergeConfig(config) {
|
|
|
1064
1091
|
issues.push(`review.autoMerge.waitMinutes: must be an integer between 1 and 55 (got ${w}) — an interactive agent session goes stale at about an hour, and a command that outlives its session reports into nothing`);
|
|
1065
1092
|
}
|
|
1066
1093
|
}
|
|
1094
|
+
if (typeof autoMerge.findingsLoop !== "boolean") {
|
|
1095
|
+
issues.push(`review.autoMerge.findingsLoop: must be true or false (got ${JSON.stringify(autoMerge.findingsLoop)}) — an absent value would switch unattended code editing on or off without saying so`);
|
|
1096
|
+
}
|
|
1097
|
+
const rounds = autoMerge.maxFindingRounds;
|
|
1098
|
+
if (!Number.isInteger(rounds) || rounds < 1) {
|
|
1099
|
+
issues.push(`review.autoMerge.maxFindingRounds: must be an integer >= 1 (got ${rounds}) — the cap is the safety property and has no opt-out`);
|
|
1100
|
+
}
|
|
1101
|
+
if (autoMerge.findingsLoop) {
|
|
1102
|
+
if (!autoMerge.interactive) {
|
|
1103
|
+
issues.push("review.autoMerge.findingsLoop: needs review.autoMerge.interactive — the loop only runs on the interactive merge path, so armed alone it is inert rather than weaker");
|
|
1104
|
+
}
|
|
1105
|
+
const findingAuthors = independent?.findingAuthors;
|
|
1106
|
+
if (!Array.isArray(findingAuthors) || findingAuthors.filter((a) => typeof a === "string" && a.trim()).length === 0) {
|
|
1107
|
+
issues.push("review.autoMerge.independentReview.findingAuthors: must name at least one login while review.autoMerge.findingsLoop is on — it is the only thing separating the review's findings from every other commenter's text, and an empty list parks every round");
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1067
1110
|
if (issues.length > 0) {
|
|
1068
1111
|
throw new ConfigValidationError(`Invalid agent config — ${issues.join("; ")}`, issues);
|
|
1069
1112
|
}
|
|
@@ -1143,6 +1186,12 @@ function validateBudgetConfig(config) {
|
|
|
1143
1186
|
const issue = turns === 0 ? `budget.maxTurnsPerCard: 0 is ambiguous — it could mean "no cap" or "run nothing"` : `budget.maxTurnsPerCard: ${JSON.stringify(turns)} is not a whole number of turns`;
|
|
1144
1187
|
throw new ConfigValidationError(`Invalid agent config — ${issue}.
|
|
1145
1188
|
` + ` Set a positive cap in turns (e.g. 300, ~2.5x the measured median card), or -1 to run with no per-card cap.`, [issue]);
|
|
1189
|
+
}
|
|
1190
|
+
const runs = config.budget.maxRunsPerCard;
|
|
1191
|
+
if (!Number.isInteger(runs) || runs === 0) {
|
|
1192
|
+
const issue = runs === 0 ? `budget.maxRunsPerCard: 0 is ambiguous — it could mean "no cap" or "run nothing"` : `budget.maxRunsPerCard: ${JSON.stringify(runs)} is not a whole number of runs`;
|
|
1193
|
+
throw new ConfigValidationError(`Invalid agent config — ${issue}.
|
|
1194
|
+
` + ` Set a positive cap in run starts (e.g. 30, 10x the give-up budget), or -1 to run with no per-card run backstop.`, [issue]);
|
|
1146
1195
|
}
|
|
1147
1196
|
const perSpawn = config.sdk?.maxBudgetUsd;
|
|
1148
1197
|
if (!Number.isFinite(perSpawn)) {
|
|
@@ -2461,6 +2510,229 @@ var init_reviewTools = __esm(() => {
|
|
|
2461
2510
|
"mcp__harmony__harmony_toggle_subtask"
|
|
2462
2511
|
];
|
|
2463
2512
|
});
|
|
2513
|
+
|
|
2514
|
+
// ../harmony-shared/dist/runEventPayloadLimit.js
|
|
2515
|
+
function runEventPayloadExceedsLimit(payload, maxBytes = MAX_RUN_EVENT_PAYLOAD_BYTES) {
|
|
2516
|
+
if (payload === null || payload === undefined)
|
|
2517
|
+
return false;
|
|
2518
|
+
let serialized;
|
|
2519
|
+
try {
|
|
2520
|
+
serialized = JSON.stringify(payload);
|
|
2521
|
+
} catch {
|
|
2522
|
+
return true;
|
|
2523
|
+
}
|
|
2524
|
+
if (serialized === undefined)
|
|
2525
|
+
return true;
|
|
2526
|
+
return serialized.length > maxBytes;
|
|
2527
|
+
}
|
|
2528
|
+
var MAX_RUN_EVENT_PAYLOAD_BYTES = 16000;
|
|
2529
|
+
|
|
2530
|
+
// ../harmony-shared/dist/runRedaction.js
|
|
2531
|
+
function isSensitivePath(rawPath) {
|
|
2532
|
+
if (typeof rawPath !== "string" || rawPath.length === 0)
|
|
2533
|
+
return false;
|
|
2534
|
+
const path = rawPath.trim().toLowerCase();
|
|
2535
|
+
const segments = path.split(/[\\/]+/).filter((s) => s.length > 0);
|
|
2536
|
+
if (segments.length === 0)
|
|
2537
|
+
return false;
|
|
2538
|
+
for (let i = 0;i < segments.length; i++) {
|
|
2539
|
+
const segment = segments[i];
|
|
2540
|
+
if (!SENSITIVE_SEGMENTS.includes(segment))
|
|
2541
|
+
continue;
|
|
2542
|
+
if (CONFIG_SCOPED_SEGMENTS.has(segment)) {
|
|
2543
|
+
if (i > 0 && segments[i - 1] === ".config")
|
|
2544
|
+
return true;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
return true;
|
|
2548
|
+
}
|
|
2549
|
+
const basename = segments[segments.length - 1];
|
|
2550
|
+
if (SENSITIVE_BASENAMES.has(basename))
|
|
2551
|
+
return true;
|
|
2552
|
+
if (basename === ".env" || basename.startsWith(".env."))
|
|
2553
|
+
return true;
|
|
2554
|
+
if (basename.endsWith(".env"))
|
|
2555
|
+
return true;
|
|
2556
|
+
if (SENSITIVE_EXTENSIONS.some((ext) => basename.endsWith(ext)))
|
|
2557
|
+
return true;
|
|
2558
|
+
if (/service[-_]?account.*\.json$/.test(basename))
|
|
2559
|
+
return true;
|
|
2560
|
+
return false;
|
|
2561
|
+
}
|
|
2562
|
+
function sensitivePathsIn(input, depth = 0) {
|
|
2563
|
+
if (depth > 6)
|
|
2564
|
+
return [];
|
|
2565
|
+
if (typeof input === "string") {
|
|
2566
|
+
return isSensitivePath(input) ? [input] : [];
|
|
2567
|
+
}
|
|
2568
|
+
if (Array.isArray(input)) {
|
|
2569
|
+
return input.flatMap((item) => sensitivePathsIn(item, depth + 1));
|
|
2570
|
+
}
|
|
2571
|
+
if (input !== null && typeof input === "object") {
|
|
2572
|
+
return Object.values(input).flatMap((value) => sensitivePathsIn(value, depth + 1));
|
|
2573
|
+
}
|
|
2574
|
+
return [];
|
|
2575
|
+
}
|
|
2576
|
+
function redactSecrets(text) {
|
|
2577
|
+
if (typeof text !== "string" || text.length === 0)
|
|
2578
|
+
return text;
|
|
2579
|
+
let out = text;
|
|
2580
|
+
for (const { pattern, replace } of SECRET_PATTERNS) {
|
|
2581
|
+
pattern.lastIndex = 0;
|
|
2582
|
+
out = out.replace(pattern, replace);
|
|
2583
|
+
}
|
|
2584
|
+
return out;
|
|
2585
|
+
}
|
|
2586
|
+
function truncate(text, max, originalLength) {
|
|
2587
|
+
const total = originalLength ?? text.length;
|
|
2588
|
+
if (total <= max)
|
|
2589
|
+
return text;
|
|
2590
|
+
return `${text.slice(0, max)}… [+${total - max} chars]`;
|
|
2591
|
+
}
|
|
2592
|
+
function redactThenTruncate(text, max) {
|
|
2593
|
+
const preCap = max * 4 + 64;
|
|
2594
|
+
const scanned = text.length > preCap ? text.slice(0, preCap) : text;
|
|
2595
|
+
return truncate(redactSecrets(scanned), max, text.length);
|
|
2596
|
+
}
|
|
2597
|
+
function redactStructure(value, depth = 0) {
|
|
2598
|
+
if (depth > 6)
|
|
2599
|
+
return REDACTION_MARK;
|
|
2600
|
+
if (typeof value === "string") {
|
|
2601
|
+
return redactThenTruncate(value, MAX_INPUT_STRING_CHARS);
|
|
2602
|
+
}
|
|
2603
|
+
if (Array.isArray(value)) {
|
|
2604
|
+
return value.slice(0, 20).map((item) => redactStructure(item, depth + 1));
|
|
2605
|
+
}
|
|
2606
|
+
if (value !== null && typeof value === "object") {
|
|
2607
|
+
const out = {};
|
|
2608
|
+
for (const [key, item] of Object.entries(value)) {
|
|
2609
|
+
out[key] = redactStructure(item, depth + 1);
|
|
2610
|
+
}
|
|
2611
|
+
return out;
|
|
2612
|
+
}
|
|
2613
|
+
return value;
|
|
2614
|
+
}
|
|
2615
|
+
function redactToolCall(args) {
|
|
2616
|
+
const sensitive = sensitivePathsIn(args.input);
|
|
2617
|
+
if (sensitive.length > 0) {
|
|
2618
|
+
return { withheld: "sensitive-path" };
|
|
2619
|
+
}
|
|
2620
|
+
const result = {};
|
|
2621
|
+
if (args.input !== undefined) {
|
|
2622
|
+
let input = redactStructure(args.input);
|
|
2623
|
+
let serialized;
|
|
2624
|
+
try {
|
|
2625
|
+
serialized = JSON.stringify(input) ?? "";
|
|
2626
|
+
} catch {
|
|
2627
|
+
serialized = "";
|
|
2628
|
+
input = REDACTION_MARK;
|
|
2629
|
+
}
|
|
2630
|
+
if (serialized.length > MAX_INPUT_CHARS) {
|
|
2631
|
+
input = truncate(serialized, MAX_INPUT_CHARS);
|
|
2632
|
+
}
|
|
2633
|
+
result.input = input;
|
|
2634
|
+
}
|
|
2635
|
+
if (typeof args.output === "string" && args.output.length > 0) {
|
|
2636
|
+
result.output = redactThenTruncate(args.output, MAX_OUTPUT_CHARS);
|
|
2637
|
+
}
|
|
2638
|
+
return result;
|
|
2639
|
+
}
|
|
2640
|
+
var MAX_INPUT_CHARS = 2000, MAX_OUTPUT_CHARS = 4000, MAX_INPUT_STRING_CHARS = 600, REDACTION_MARK = "«redacted»", SENSITIVE_SEGMENTS, CONFIG_SCOPED_SEGMENTS, SENSITIVE_BASENAMES, SENSITIVE_EXTENSIONS, SECRET_PATTERNS;
|
|
2641
|
+
var init_runRedaction = __esm(() => {
|
|
2642
|
+
SENSITIVE_SEGMENTS = [
|
|
2643
|
+
".ssh",
|
|
2644
|
+
".gnupg",
|
|
2645
|
+
".aws",
|
|
2646
|
+
".codex",
|
|
2647
|
+
".gemini",
|
|
2648
|
+
".docker",
|
|
2649
|
+
".kube",
|
|
2650
|
+
".hmy",
|
|
2651
|
+
".harmony-mcp",
|
|
2652
|
+
".password-store",
|
|
2653
|
+
".claude",
|
|
2654
|
+
"gh",
|
|
2655
|
+
"gcloud",
|
|
2656
|
+
"op",
|
|
2657
|
+
"anthropic"
|
|
2658
|
+
];
|
|
2659
|
+
CONFIG_SCOPED_SEGMENTS = new Set([
|
|
2660
|
+
"gh",
|
|
2661
|
+
"gcloud",
|
|
2662
|
+
"op",
|
|
2663
|
+
"anthropic"
|
|
2664
|
+
]);
|
|
2665
|
+
SENSITIVE_BASENAMES = new Set([
|
|
2666
|
+
".netrc",
|
|
2667
|
+
"_netrc",
|
|
2668
|
+
".npmrc",
|
|
2669
|
+
".pgpass",
|
|
2670
|
+
".git-credentials",
|
|
2671
|
+
".htpasswd",
|
|
2672
|
+
".claude.json",
|
|
2673
|
+
"credentials",
|
|
2674
|
+
".credentials",
|
|
2675
|
+
"credentials.json",
|
|
2676
|
+
".credentials.json",
|
|
2677
|
+
"credentials.yml",
|
|
2678
|
+
"credentials.yaml",
|
|
2679
|
+
"auth.json",
|
|
2680
|
+
".auth.json",
|
|
2681
|
+
"secrets",
|
|
2682
|
+
"secrets.json",
|
|
2683
|
+
"secrets.yaml",
|
|
2684
|
+
"secrets.yml",
|
|
2685
|
+
"id_rsa",
|
|
2686
|
+
"id_dsa",
|
|
2687
|
+
"id_ecdsa",
|
|
2688
|
+
"id_ed25519",
|
|
2689
|
+
"known_hosts"
|
|
2690
|
+
]);
|
|
2691
|
+
SENSITIVE_EXTENSIONS = [
|
|
2692
|
+
".pem",
|
|
2693
|
+
".key",
|
|
2694
|
+
".p12",
|
|
2695
|
+
".pfx",
|
|
2696
|
+
".keystore",
|
|
2697
|
+
".jks",
|
|
2698
|
+
".asc",
|
|
2699
|
+
".gpg"
|
|
2700
|
+
];
|
|
2701
|
+
SECRET_PATTERNS = [
|
|
2702
|
+
{
|
|
2703
|
+
pattern: /-----BEGIN[^-]*PRIVATE KEY-----[\s\S]*?-----END[^-]*-----/g,
|
|
2704
|
+
replace: REDACTION_MARK
|
|
2705
|
+
},
|
|
2706
|
+
{ pattern: /\bhmy_at_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
2707
|
+
{ pattern: /\bhmy_[A-Za-z0-9_-]{8,}/g, replace: REDACTION_MARK },
|
|
2708
|
+
{ pattern: /\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}/g, replace: REDACTION_MARK },
|
|
2709
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{16,}/g, replace: REDACTION_MARK },
|
|
2710
|
+
{ pattern: /\bgithub_pat_[A-Za-z0-9_]{20,}/g, replace: REDACTION_MARK },
|
|
2711
|
+
{ pattern: /\bxox[abprs]-[A-Za-z0-9-]{10,}/g, replace: REDACTION_MARK },
|
|
2712
|
+
{ pattern: /\bAKIA[0-9A-Z]{16}\b/g, replace: REDACTION_MARK },
|
|
2713
|
+
{ pattern: /\bAIza[0-9A-Za-z_-]{20,}/g, replace: REDACTION_MARK },
|
|
2714
|
+
{
|
|
2715
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
|
|
2716
|
+
replace: REDACTION_MARK
|
|
2717
|
+
},
|
|
2718
|
+
{
|
|
2719
|
+
pattern: /\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+/=-]{12,}/gi,
|
|
2720
|
+
replace: `$1 ${REDACTION_MARK}`
|
|
2721
|
+
},
|
|
2722
|
+
{
|
|
2723
|
+
pattern: /(\w{1,32}:\/\/)[^/\s:@]+:[^/\s@]+@/g,
|
|
2724
|
+
replace: `$1${REDACTION_MARK}@`
|
|
2725
|
+
},
|
|
2726
|
+
{
|
|
2727
|
+
pattern: /\b([A-Za-z0-9_]{0,40}(?:TOKEN|SECRET|PASSWORD|PASSWD|APIKEY|API_KEY|ACCESS_KEY|PRIVATE_KEY|CREDENTIAL|AUTH)[A-Za-z0-9_]{0,40})\s*[=:]\s*(?:"[^"]*"|'[^']*'|`[^`]*`|[^\s,;)}\]]+)/gi,
|
|
2728
|
+
replace: `$1=${REDACTION_MARK}`
|
|
2729
|
+
},
|
|
2730
|
+
{
|
|
2731
|
+
pattern: /(--?(?:password|passwd|token|api-?key|secret|auth)(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi,
|
|
2732
|
+
replace: `$1${REDACTION_MARK}`
|
|
2733
|
+
}
|
|
2734
|
+
];
|
|
2735
|
+
});
|
|
2464
2736
|
// ../harmony-shared/dist/stageHandoff.js
|
|
2465
2737
|
function isFanoutHandoffItem(value) {
|
|
2466
2738
|
if (typeof value !== "object" || value === null)
|
|
@@ -2655,6 +2927,7 @@ var init_dist = __esm(() => {
|
|
|
2655
2927
|
init_projectTemplates();
|
|
2656
2928
|
init_realtimeChannel();
|
|
2657
2929
|
init_reviewTools();
|
|
2930
|
+
init_runRedaction();
|
|
2658
2931
|
init_stageHandoff();
|
|
2659
2932
|
init_types();
|
|
2660
2933
|
});
|
|
@@ -2967,11 +3240,15 @@ function agentIdentifier(workerId) {
|
|
|
2967
3240
|
function endStatusForCancel(reason) {
|
|
2968
3241
|
return reason === "human_stop" ? "cancelled" : "paused";
|
|
2969
3242
|
}
|
|
2970
|
-
var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
|
|
3243
|
+
var DEFAULT_FINDING_AUTHORS, DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL = "Need Review", NEED_REVIEW_LABEL_COLOR = "#f59e0b", AGENT_NAME = "Harmony Agent", DAEMON_IDENTIFIER_BASE = "harmony-daemon", NOTICE_IDENTIFIER;
|
|
2971
3244
|
var init_types2 = __esm(() => {
|
|
2972
3245
|
init_board_review();
|
|
2973
3246
|
init_contract_phase();
|
|
2974
3247
|
init_plan_phase();
|
|
3248
|
+
DEFAULT_FINDING_AUTHORS = [
|
|
3249
|
+
"claude",
|
|
3250
|
+
"github-actions[bot]"
|
|
3251
|
+
];
|
|
2975
3252
|
DEFAULT_AGENT_CONFIG = {
|
|
2976
3253
|
poolSize: 6,
|
|
2977
3254
|
maxTimeout: 5400000,
|
|
@@ -3050,6 +3327,8 @@ var init_types2 = __esm(() => {
|
|
|
3050
3327
|
enabled: false,
|
|
3051
3328
|
interactive: false,
|
|
3052
3329
|
waitMinutes: 20,
|
|
3330
|
+
findingsLoop: false,
|
|
3331
|
+
maxFindingRounds: 2,
|
|
3053
3332
|
strategy: "squash",
|
|
3054
3333
|
deleteBranch: true,
|
|
3055
3334
|
requireGreenCi: true,
|
|
@@ -3069,7 +3348,8 @@ var init_types2 = __esm(() => {
|
|
|
3069
3348
|
enabled: true,
|
|
3070
3349
|
label: "ci-review",
|
|
3071
3350
|
checkName: "review",
|
|
3072
|
-
verdict: { enabled: true, checkName: "review-verdict" }
|
|
3351
|
+
verdict: { enabled: true, checkName: "review-verdict" },
|
|
3352
|
+
findingAuthors: [...DEFAULT_FINDING_AUTHORS]
|
|
3073
3353
|
}
|
|
3074
3354
|
}
|
|
3075
3355
|
},
|
|
@@ -3077,6 +3357,7 @@ var init_types2 = __esm(() => {
|
|
|
3077
3357
|
maxAttemptsPerCard: 3,
|
|
3078
3358
|
dailyBudgetCents: 40000,
|
|
3079
3359
|
maxTurnsPerCard: 300,
|
|
3360
|
+
maxRunsPerCard: 30,
|
|
3080
3361
|
pause: {
|
|
3081
3362
|
enabled: true,
|
|
3082
3363
|
waitHours: 24,
|
|
@@ -3290,6 +3571,7 @@ var init_config = __esm(() => {
|
|
|
3290
3571
|
// src/config-dir-migration.ts
|
|
3291
3572
|
import {
|
|
3292
3573
|
existsSync,
|
|
3574
|
+
lstatSync,
|
|
3293
3575
|
mkdirSync,
|
|
3294
3576
|
readdirSync,
|
|
3295
3577
|
renameSync,
|
|
@@ -3324,27 +3606,84 @@ function migrateConfigDir(opts = {}) {
|
|
|
3324
3606
|
log6.info(TAG6, `Moved ${from} to ${to}`);
|
|
3325
3607
|
return { outcome: "moved", ...empty };
|
|
3326
3608
|
}
|
|
3327
|
-
|
|
3328
|
-
const target = join2(to, entry);
|
|
3329
|
-
if (existsSync(target)) {
|
|
3330
|
-
skipped.push(entry);
|
|
3331
|
-
continue;
|
|
3332
|
-
}
|
|
3333
|
-
renameSync(join2(from, entry), target);
|
|
3334
|
-
moved.push(entry);
|
|
3335
|
-
}
|
|
3609
|
+
mergeDirInto(from, to, "", moved, skipped, 0);
|
|
3336
3610
|
if (moved.length === 0 && skipped.length === 0) {
|
|
3337
3611
|
return { outcome: "nothing-to-do", moved, skipped };
|
|
3338
3612
|
}
|
|
3339
|
-
log6.info(TAG6, `Merged ${from} into ${to}: ${moved.length} moved` + (skipped.length > 0 ? `, ${skipped.length} left in place (already present at the target): ${skipped
|
|
3613
|
+
log6.info(TAG6, `Merged ${from} into ${to}: ${moved.length} moved` + (skipped.length > 0 ? `, ${skipped.length} left in place (already present at the target): ${nameSample(skipped)}` : ""));
|
|
3340
3614
|
return { outcome: "merged", moved, skipped };
|
|
3341
3615
|
} catch (err) {
|
|
3342
3616
|
const error = err instanceof Error ? err.message : String(err);
|
|
3343
|
-
log6.warn(TAG6, `Could not migrate ${from} to ${to}: ${error}.` + (moved.length > 0 ? ` ${moved.length} entr${moved.length === 1 ? "y" : "ies"} already moved: ${moved
|
|
3617
|
+
log6.warn(TAG6, `Could not migrate ${from} to ${to}: ${error}.` + (moved.length > 0 ? ` ${moved.length} entr${moved.length === 1 ? "y" : "ies"} already moved: ${nameSample(moved)}. Re-run to finish; nothing is deleted.` : ` Nothing was moved; the daemon reads the old config through the fallback.`));
|
|
3344
3618
|
return { outcome: "failed", moved, skipped, error };
|
|
3345
3619
|
}
|
|
3346
3620
|
}
|
|
3347
|
-
|
|
3621
|
+
function mergeDirInto(fromDir, toDir, rel, moved, skipped, depth) {
|
|
3622
|
+
for (const entry of readdirSync(fromDir, { withFileTypes: true })) {
|
|
3623
|
+
const source = join2(fromDir, entry.name);
|
|
3624
|
+
const target = join2(toDir, entry.name);
|
|
3625
|
+
const relPath = join2(rel, entry.name);
|
|
3626
|
+
if (entry.isDirectory()) {
|
|
3627
|
+
if (!lexists(target)) {
|
|
3628
|
+
renameSync(source, target);
|
|
3629
|
+
if (recordMovedLeaves(target, relPath, moved) === 0) {
|
|
3630
|
+
moved.push(`${relPath}/`);
|
|
3631
|
+
}
|
|
3632
|
+
continue;
|
|
3633
|
+
}
|
|
3634
|
+
const targetIsDir = isRealDirectory(target);
|
|
3635
|
+
if (!targetIsDir || depth >= MAX_MERGE_DEPTH) {
|
|
3636
|
+
skipped.push(`${relPath}/`);
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
mergeDirInto(source, target, relPath, moved, skipped, depth + 1);
|
|
3640
|
+
continue;
|
|
3641
|
+
}
|
|
3642
|
+
if (lexists(target)) {
|
|
3643
|
+
skipped.push(relPath);
|
|
3644
|
+
continue;
|
|
3645
|
+
}
|
|
3646
|
+
renameSync(source, target);
|
|
3647
|
+
moved.push(relPath);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
function recordMovedLeaves(dir, rel, moved) {
|
|
3651
|
+
let leaves = 0;
|
|
3652
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
3653
|
+
const relPath = join2(rel, entry.name);
|
|
3654
|
+
if (entry.isDirectory()) {
|
|
3655
|
+
const below = recordMovedLeaves(join2(dir, entry.name), relPath, moved);
|
|
3656
|
+
if (below === 0)
|
|
3657
|
+
moved.push(`${relPath}/`);
|
|
3658
|
+
leaves += below;
|
|
3659
|
+
continue;
|
|
3660
|
+
}
|
|
3661
|
+
moved.push(relPath);
|
|
3662
|
+
leaves += 1;
|
|
3663
|
+
}
|
|
3664
|
+
return leaves;
|
|
3665
|
+
}
|
|
3666
|
+
function lexists(path) {
|
|
3667
|
+
try {
|
|
3668
|
+
lstatSync(path);
|
|
3669
|
+
return true;
|
|
3670
|
+
} catch {
|
|
3671
|
+
return false;
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
function isRealDirectory(path) {
|
|
3675
|
+
try {
|
|
3676
|
+
return lstatSync(path).isDirectory();
|
|
3677
|
+
} catch {
|
|
3678
|
+
return false;
|
|
3679
|
+
}
|
|
3680
|
+
}
|
|
3681
|
+
function nameSample(paths, limit = 10) {
|
|
3682
|
+
if (paths.length <= limit)
|
|
3683
|
+
return paths.join(", ");
|
|
3684
|
+
return `${paths.slice(0, limit).join(", ")} … and ${paths.length - limit} more`;
|
|
3685
|
+
}
|
|
3686
|
+
var TAG6 = "config-migration", MAX_MERGE_DEPTH = 32;
|
|
3348
3687
|
var init_config_dir_migration = __esm(() => {
|
|
3349
3688
|
init_paths();
|
|
3350
3689
|
});
|
|
@@ -3614,7 +3953,7 @@ function resolveDaemonUserId(userEmail, members, apiKeyUserId) {
|
|
|
3614
3953
|
// src/ci-patch-trees.ts
|
|
3615
3954
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
3616
3955
|
import {
|
|
3617
|
-
lstatSync,
|
|
3956
|
+
lstatSync as lstatSync2,
|
|
3618
3957
|
mkdirSync as mkdirSync2,
|
|
3619
3958
|
mkdtempSync,
|
|
3620
3959
|
readdirSync as readdirSync2,
|
|
@@ -3747,7 +4086,7 @@ function safeDestination(clean, path) {
|
|
|
3747
4086
|
for (const part of parts.slice(0, -1)) {
|
|
3748
4087
|
probe = join3(probe, part);
|
|
3749
4088
|
try {
|
|
3750
|
-
if (
|
|
4089
|
+
if (lstatSync2(probe).isSymbolicLink()) {
|
|
3751
4090
|
throw new Error(`the repair's destination passes through a symlink in the checkout (${path}) — refusing to write through it`);
|
|
3752
4091
|
}
|
|
3753
4092
|
} catch (err) {
|
|
@@ -3773,6 +4112,7 @@ import {
|
|
|
3773
4112
|
extractPrUrl,
|
|
3774
4113
|
GIT_NO_HOOKS as GIT_NO_HOOKS3,
|
|
3775
4114
|
installCommand,
|
|
4115
|
+
isPrUrlOnOrigin,
|
|
3776
4116
|
log as log9,
|
|
3777
4117
|
removeWorktreeHoldingBranch,
|
|
3778
4118
|
resolvePrHeadBranch
|
|
@@ -3863,9 +4203,48 @@ async function resolveReviewBranch(description, cwd) {
|
|
|
3863
4203
|
if (!prUrl)
|
|
3864
4204
|
return { kind: "none" };
|
|
3865
4205
|
const provider = detectGitProvider(cwd);
|
|
4206
|
+
if (!isPrUrlOnOrigin(prUrl, cwd, provider)) {
|
|
4207
|
+
return {
|
|
4208
|
+
kind: "skip",
|
|
4209
|
+
reason: `cannot prove ${prUrl} is a pull request on this checkout's own origin (provider: ${provider}) — the PR: line is editable board data, so nothing was read from it`
|
|
4210
|
+
};
|
|
4211
|
+
}
|
|
3866
4212
|
const resolved = await resolvePrHeadBranch(prUrl, cwd, provider);
|
|
3867
4213
|
return resolved;
|
|
3868
4214
|
}
|
|
4215
|
+
function assessPrReviewContinuity(opts) {
|
|
4216
|
+
const { repoRoot, branch, reviewedSha } = opts;
|
|
4217
|
+
if (!reviewedSha)
|
|
4218
|
+
return { kind: "ok" };
|
|
4219
|
+
let head;
|
|
4220
|
+
try {
|
|
4221
|
+
execFileSync3("git", [...GIT_NO_HOOKS3, "fetch", "origin", branch], {
|
|
4222
|
+
cwd: repoRoot,
|
|
4223
|
+
stdio: "pipe",
|
|
4224
|
+
timeout: 60000
|
|
4225
|
+
});
|
|
4226
|
+
head = execFileSync3("git", [...GIT_NO_HOOKS3, "rev-parse", "FETCH_HEAD"], {
|
|
4227
|
+
cwd: repoRoot,
|
|
4228
|
+
encoding: "utf-8",
|
|
4229
|
+
stdio: "pipe",
|
|
4230
|
+
timeout: 1e4
|
|
4231
|
+
}).trim();
|
|
4232
|
+
} catch (err) {
|
|
4233
|
+
return {
|
|
4234
|
+
kind: "skip",
|
|
4235
|
+
reason: `could not read origin/${branch} to check review continuity: ${gitErrorDetail(err)}`
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
try {
|
|
4239
|
+
execFileSync3("git", [...GIT_NO_HOOKS3, "merge-base", "--is-ancestor", reviewedSha, head], { cwd: repoRoot, stdio: "pipe", timeout: 1e4 });
|
|
4240
|
+
return { kind: "ok" };
|
|
4241
|
+
} catch (err) {
|
|
4242
|
+
return {
|
|
4243
|
+
kind: "skip",
|
|
4244
|
+
reason: `the recorded Reviewed-SHA ${reviewedSha} is not contained in origin/${branch} (head ${head}) — the PR: line points at a head this card's review history never built (${gitErrorDetail(err) || "not an ancestor"})`
|
|
4245
|
+
};
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
3869
4248
|
function reviewedFromPrUrl(description) {
|
|
3870
4249
|
if (extractBranchFromDescription(description))
|
|
3871
4250
|
return null;
|
|
@@ -4509,12 +4888,15 @@ var init_ci_repair = __esm(() => {
|
|
|
4509
4888
|
|
|
4510
4889
|
// src/auto-merge.ts
|
|
4511
4890
|
import {
|
|
4891
|
+
countBlockingReviewVerdicts,
|
|
4512
4892
|
extractCiReviewRequestedSha,
|
|
4513
4893
|
extractReviewedSha,
|
|
4514
4894
|
getPrStatus,
|
|
4515
4895
|
isPrOnOrigin as isPrOnOrigin2,
|
|
4516
4896
|
log as log13,
|
|
4517
4897
|
mergePullRequest,
|
|
4898
|
+
readHeadCommitTime,
|
|
4899
|
+
readPrFindings,
|
|
4518
4900
|
requestIndependentReview,
|
|
4519
4901
|
upsertCiReviewRequestedSha
|
|
4520
4902
|
} from "@gethmy/harness";
|
|
@@ -4563,6 +4945,15 @@ function decideAutoMergeAction(input) {
|
|
|
4563
4945
|
case "clean":
|
|
4564
4946
|
break;
|
|
4565
4947
|
case "blocking":
|
|
4948
|
+
if (config.findingsLoop) {
|
|
4949
|
+
const rounds = input.blockingVerdictRounds;
|
|
4950
|
+
if (rounds !== undefined && rounds <= config.maxFindingRounds) {
|
|
4951
|
+
return "report-findings";
|
|
4952
|
+
}
|
|
4953
|
+
if (rounds !== undefined && Number.isFinite(rounds)) {
|
|
4954
|
+
return "stamp-findings-exhausted";
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4566
4957
|
return "stamp-review-findings";
|
|
4567
4958
|
case "pending":
|
|
4568
4959
|
return "wait";
|
|
@@ -4591,6 +4982,9 @@ async function stampCiFailure(client, card) {
|
|
|
4591
4982
|
async function stampReviewFindings(client, card) {
|
|
4592
4983
|
await stampForHumanOnce(client, card, "Independent review posted blocking findings");
|
|
4593
4984
|
}
|
|
4985
|
+
async function stampFindingsExhausted(client, card, maxRounds) {
|
|
4986
|
+
await stampForHumanOnce(client, card, `Findings loop spent its ${maxRounds} round${maxRounds === 1 ? "" : "s"} and the review still objects`);
|
|
4987
|
+
}
|
|
4594
4988
|
async function stampReviewIncomplete(client, card) {
|
|
4595
4989
|
await stampForHumanOnce(client, card, "Independent review did not complete (no verdict)");
|
|
4596
4990
|
}
|
|
@@ -4609,7 +5003,7 @@ async function attemptAutoMerge(deps) {
|
|
|
4609
5003
|
const gate = driver === "interactive" ? autoMerge.interactive : autoMerge.enabled;
|
|
4610
5004
|
if (!gate)
|
|
4611
5005
|
return "disabled";
|
|
4612
|
-
if (
|
|
5006
|
+
if (!isPrOnOrigin2(prUrl, cwd)) {
|
|
4613
5007
|
log13.warn(TAG13, `#${card.short_id} refusing to merge ${prUrl} — it is not a pull request on this checkout's own origin. The card's PR: line is editable board data; nothing was read from that URL.`);
|
|
4614
5008
|
return "foreign-pr";
|
|
4615
5009
|
}
|
|
@@ -4637,6 +5031,9 @@ async function attemptAutoMerge(deps) {
|
|
|
4637
5031
|
enabled: true,
|
|
4638
5032
|
...driver === "interactive" && independent.enabled ? { reReviewOnBranchChange: false } : {}
|
|
4639
5033
|
};
|
|
5034
|
+
const countRounds = driver === "interactive" && autoMerge.findingsLoop && independent.enabled && independent.verdict.enabled && reviewVerdict === "blocking";
|
|
5035
|
+
const blockingVerdictRounds = countRounds ? await countBlockingReviewVerdicts(prUrl, cwd, provider, independent.verdict.checkName) : undefined;
|
|
5036
|
+
const headCommitTime = countRounds ? await readHeadCommitTime(prUrl, cwd, provider) : null;
|
|
4640
5037
|
const action = decideAutoMergeAction({
|
|
4641
5038
|
ciStatus,
|
|
4642
5039
|
headSha,
|
|
@@ -4644,7 +5041,8 @@ async function attemptAutoMerge(deps) {
|
|
|
4644
5041
|
config: decisionConfig,
|
|
4645
5042
|
independentReview,
|
|
4646
5043
|
reviewVerdict,
|
|
4647
|
-
reviewRequestedSha
|
|
5044
|
+
reviewRequestedSha,
|
|
5045
|
+
blockingVerdictRounds
|
|
4648
5046
|
});
|
|
4649
5047
|
switch (action) {
|
|
4650
5048
|
case "wait":
|
|
@@ -4672,6 +5070,28 @@ async function attemptAutoMerge(deps) {
|
|
|
4672
5070
|
log13.info(TAG13, `#${card.short_id} the independent review posted blocking findings ("${independent.verdict.checkName}" is red) — holding the merge and flagging for a human. The findings are on the PR: fix them and push (the new head is reviewed afresh), or merge it yourself once you have judged them.`);
|
|
4673
5071
|
await stampReviewFindings(client, card);
|
|
4674
5072
|
return action;
|
|
5073
|
+
case "report-findings": {
|
|
5074
|
+
const report = await readPrFindings(prUrl, cwd, provider, {
|
|
5075
|
+
authors: independent.findingAuthors ?? [],
|
|
5076
|
+
...headCommitTime ? { since: headCommitTime } : {}
|
|
5077
|
+
});
|
|
5078
|
+
if (!report.readable) {
|
|
5079
|
+
log13.warn(TAG13, `#${card.short_id} the independent review posted blocking findings but none could be read from ${independent.findingAuthors?.join(", ") || "no configured reviewer"} — holding the merge and flagging for a human instead of acting on a partial or empty list.`);
|
|
5080
|
+
await stampReviewFindings(client, card);
|
|
5081
|
+
return "stamp-review-findings";
|
|
5082
|
+
}
|
|
5083
|
+
log13.info(TAG13, `#${card.short_id} the independent review posted blocking findings ("${independent.verdict.checkName}" is red) — reporting ${report.items.length} finding(s) for verification, round ${blockingVerdictRounds} of ${autoMerge.maxFindingRounds}.`);
|
|
5084
|
+
deps.onFindings?.({
|
|
5085
|
+
report,
|
|
5086
|
+
round: blockingVerdictRounds ?? 0,
|
|
5087
|
+
maxRounds: autoMerge.maxFindingRounds
|
|
5088
|
+
});
|
|
5089
|
+
return action;
|
|
5090
|
+
}
|
|
5091
|
+
case "stamp-findings-exhausted":
|
|
5092
|
+
log13.info(TAG13, `#${card.short_id} the findings loop spent its ${autoMerge.maxFindingRounds} round(s) and "${independent.verdict.checkName}" is still red — holding the merge for a person. The findings were read and acted on; what ran out is the round budget, so pushing another fix will park again.`);
|
|
5093
|
+
await stampFindingsExhausted(client, card, autoMerge.maxFindingRounds);
|
|
5094
|
+
return action;
|
|
4675
5095
|
case "stamp-review-incomplete":
|
|
4676
5096
|
log13.info(TAG13, `#${card.short_id} the independent review did not complete ("${independent.checkName}" is red, no verdict) — holding the merge and flagging for a human. Re-request it with a remove-then-add of "${independent.label}"; the per-head ledger stops this daemon buying a second pass itself.`);
|
|
4677
5097
|
await stampReviewIncomplete(client, card);
|
|
@@ -6169,7 +6589,7 @@ var init_completion = __esm(() => {
|
|
|
6169
6589
|
|
|
6170
6590
|
// src/progress-tracker.ts
|
|
6171
6591
|
import { log as log21 } from "@gethmy/harness";
|
|
6172
|
-
function
|
|
6592
|
+
function truncate2(str, max) {
|
|
6173
6593
|
return str.length > max ? `${str.slice(0, max - 3)}...` : str;
|
|
6174
6594
|
}
|
|
6175
6595
|
|
|
@@ -6354,7 +6774,7 @@ class ProgressTracker {
|
|
|
6354
6774
|
const firstLine = (end === -1 ? trimmed : trimmed.slice(0, end)).trim();
|
|
6355
6775
|
if (firstLine.length >= 10 && firstLine.length <= 200) {
|
|
6356
6776
|
if (ACTION_PREFIX.test(firstLine)) {
|
|
6357
|
-
this.lastAction =
|
|
6777
|
+
this.lastAction = truncate2(firstLine, MAX_TASK_LENGTH);
|
|
6358
6778
|
}
|
|
6359
6779
|
}
|
|
6360
6780
|
}
|
|
@@ -6413,16 +6833,16 @@ class ProgressTracker {
|
|
|
6413
6833
|
}
|
|
6414
6834
|
case "Grep": {
|
|
6415
6835
|
const pattern = this.extractString(input, "pattern");
|
|
6416
|
-
return pattern ? `Searching for "${
|
|
6836
|
+
return pattern ? `Searching for "${truncate2(pattern, 40)}"` : "Searching code";
|
|
6417
6837
|
}
|
|
6418
6838
|
case "Bash": {
|
|
6419
6839
|
const cmd = this.extractString(input, "command");
|
|
6420
|
-
return cmd ? `Running: ${
|
|
6840
|
+
return cmd ? `Running: ${truncate2(cmd.split(`
|
|
6421
6841
|
`)[0], 80)}` : "Running command";
|
|
6422
6842
|
}
|
|
6423
6843
|
case "Agent": {
|
|
6424
6844
|
const desc = this.extractString(input, "description");
|
|
6425
|
-
return desc ? `Sub-agent: ${
|
|
6845
|
+
return desc ? `Sub-agent: ${truncate2(desc, 60)}` : "Delegating to sub-agent";
|
|
6426
6846
|
}
|
|
6427
6847
|
default: {
|
|
6428
6848
|
if (name.startsWith("mcp__harmony__harmony_")) {
|
|
@@ -6471,7 +6891,7 @@ class ProgressTracker {
|
|
|
6471
6891
|
agentIdentifier: this.sessionIdentifier,
|
|
6472
6892
|
agentName: AGENT_NAME,
|
|
6473
6893
|
status: "working",
|
|
6474
|
-
currentTask:
|
|
6894
|
+
currentTask: truncate2(currentTask, MAX_TASK_LENGTH),
|
|
6475
6895
|
progressPercent: this.progress,
|
|
6476
6896
|
phase: this.phase,
|
|
6477
6897
|
filesChanged: this.filesEdited.size,
|
|
@@ -6489,7 +6909,7 @@ class ProgressTracker {
|
|
|
6489
6909
|
this.lastEmittedProgress = this.progress;
|
|
6490
6910
|
this.runEventSink.recordProgress({
|
|
6491
6911
|
progressPercent: this.progress,
|
|
6492
|
-
currentTask:
|
|
6912
|
+
currentTask: truncate2(currentTask, MAX_TASK_LENGTH),
|
|
6493
6913
|
phase: this.phase,
|
|
6494
6914
|
filesChanged: this.filesEdited.size
|
|
6495
6915
|
});
|
|
@@ -6502,7 +6922,7 @@ class ProgressTracker {
|
|
|
6502
6922
|
this.heartbeatTimer = setTimeout(() => {
|
|
6503
6923
|
if (!this.stopped) {
|
|
6504
6924
|
const task = this.lastAction ? `Still working — ${this.lastAction}` : "Still working...";
|
|
6505
|
-
this.sendUpdate(
|
|
6925
|
+
this.sendUpdate(truncate2(task, MAX_TASK_LENGTH));
|
|
6506
6926
|
this.startHeartbeat();
|
|
6507
6927
|
}
|
|
6508
6928
|
}, HEARTBEAT_MS);
|
|
@@ -7802,6 +8222,7 @@ class StateStore {
|
|
|
7802
8222
|
const rec = this.ensureCard(cardId);
|
|
7803
8223
|
rec.attempts += 1;
|
|
7804
8224
|
rec.totalAttempts = (rec.totalAttempts ?? 0) + 1;
|
|
8225
|
+
rec.runStarts = (rec.runStarts ?? 0) + 1;
|
|
7805
8226
|
rec.lastAttemptAt = Date.now();
|
|
7806
8227
|
await this.persist();
|
|
7807
8228
|
return rec.attempts;
|
|
@@ -7888,7 +8309,7 @@ class StateStore {
|
|
|
7888
8309
|
const rec = this.getCard(cardId);
|
|
7889
8310
|
if (!rec)
|
|
7890
8311
|
return;
|
|
7891
|
-
if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && (!clearTurns || !rec.totalTurns)) {
|
|
8312
|
+
if (rec.attempts === 0 && !rec.ciRepairAttempts && rec.ciRepairParkedAt == null && (!clearTurns || !rec.totalTurns && !rec.runStarts)) {
|
|
7892
8313
|
return;
|
|
7893
8314
|
}
|
|
7894
8315
|
rec.attempts = 0;
|
|
@@ -7896,6 +8317,8 @@ class StateStore {
|
|
|
7896
8317
|
rec.ciRepairParkedAt = null;
|
|
7897
8318
|
if (clearTurns)
|
|
7898
8319
|
rec.totalTurns = 0;
|
|
8320
|
+
if (clearTurns)
|
|
8321
|
+
rec.runStarts = 0;
|
|
7899
8322
|
await this.persist();
|
|
7900
8323
|
}
|
|
7901
8324
|
getCiRepairAttempts(cardId) {
|
|
@@ -7968,6 +8391,9 @@ class StateStore {
|
|
|
7968
8391
|
getCardTurns(cardId) {
|
|
7969
8392
|
return this.getCard(cardId)?.totalTurns ?? 0;
|
|
7970
8393
|
}
|
|
8394
|
+
getCardRunStarts(cardId) {
|
|
8395
|
+
return this.getCard(cardId)?.runStarts ?? 0;
|
|
8396
|
+
}
|
|
7971
8397
|
getCardCostCents(cardId) {
|
|
7972
8398
|
return this.getCard(cardId)?.totalCostCents ?? 0;
|
|
7973
8399
|
}
|
|
@@ -8313,6 +8739,7 @@ import {
|
|
|
8313
8739
|
collectGateEvidence,
|
|
8314
8740
|
DevServerReadinessError,
|
|
8315
8741
|
devServerLaunch,
|
|
8742
|
+
extractReviewedSha as extractReviewedSha2,
|
|
8316
8743
|
formatDiffSummary,
|
|
8317
8744
|
GIT_NO_HOOKS as GIT_NO_HOOKS7,
|
|
8318
8745
|
implementRunContainmentCliArgs,
|
|
@@ -8528,6 +8955,18 @@ class ReviewWorker {
|
|
|
8528
8955
|
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
8529
8956
|
return;
|
|
8530
8957
|
}
|
|
8958
|
+
if (this.config.review.autoMerge.enabled && reviewedFromPrUrl(card.description)) {
|
|
8959
|
+
const continuity = assessPrReviewContinuity({
|
|
8960
|
+
repoRoot,
|
|
8961
|
+
branch: resolution.branch,
|
|
8962
|
+
reviewedSha: extractReviewedSha2(card.description ?? null)
|
|
8963
|
+
});
|
|
8964
|
+
if (continuity.kind === "skip") {
|
|
8965
|
+
log28.warn(this.tag, `#${card.short_id} refusing the PR-derived re-review (${continuity.reason}) — marking for human review (staying in Review)`);
|
|
8966
|
+
await addLabelByName(this.client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
|
|
8967
|
+
return;
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8531
8970
|
this.branchName = resolution.branch;
|
|
8532
8971
|
log28.info(this.tag, `Review branch: ${this.branchName}`);
|
|
8533
8972
|
let reviewSession;
|
|
@@ -9291,7 +9730,7 @@ var init_unblock = () => {};
|
|
|
9291
9730
|
// src/cli-agent-runner.ts
|
|
9292
9731
|
import { log as log31 } from "@gethmy/harness";
|
|
9293
9732
|
function truncateOutput(value) {
|
|
9294
|
-
return value === undefined ? undefined : value.slice(0,
|
|
9733
|
+
return value === undefined ? undefined : value.slice(0, MAX_OUTPUT_CHARS);
|
|
9295
9734
|
}
|
|
9296
9735
|
|
|
9297
9736
|
class CliAgentRunner {
|
|
@@ -9302,6 +9741,10 @@ class CliAgentRunner {
|
|
|
9302
9741
|
flushTimer = null;
|
|
9303
9742
|
flushing = false;
|
|
9304
9743
|
stopped = false;
|
|
9744
|
+
batchAttempts = 0;
|
|
9745
|
+
nextBatchSize = MAX_BUFFER;
|
|
9746
|
+
withheldToolUses = new Map;
|
|
9747
|
+
pendingWithholdWithoutId;
|
|
9305
9748
|
constructor(client, cardId, sessionId) {
|
|
9306
9749
|
this.client = client;
|
|
9307
9750
|
this.cardId = cardId;
|
|
@@ -9311,21 +9754,43 @@ class CliAgentRunner {
|
|
|
9311
9754
|
if (this.stopped)
|
|
9312
9755
|
return;
|
|
9313
9756
|
parser.on("tool_start", (name, input, toolUseId) => {
|
|
9757
|
+
const redacted = redactToolCall({ input });
|
|
9758
|
+
if (redacted.withheld) {
|
|
9759
|
+
if (toolUseId) {
|
|
9760
|
+
this.withheldToolUses.set(toolUseId, redacted.withheld);
|
|
9761
|
+
} else {
|
|
9762
|
+
this.pendingWithholdWithoutId = redacted.withheld;
|
|
9763
|
+
}
|
|
9764
|
+
}
|
|
9314
9765
|
this.enqueue({
|
|
9315
9766
|
kind: "tool_started",
|
|
9316
9767
|
source: "agent",
|
|
9317
|
-
payload: { toolName: name, toolUseId, input }
|
|
9768
|
+
payload: redacted.withheld ? { toolName: name, toolUseId, withheld: redacted.withheld } : { toolName: name, toolUseId, input: redacted.input }
|
|
9318
9769
|
});
|
|
9319
9770
|
});
|
|
9320
9771
|
parser.on("tool_end", (name, toolUseId, content) => {
|
|
9772
|
+
const withheld = (toolUseId ? this.withheldToolUses.get(toolUseId) : undefined) ?? this.pendingWithholdWithoutId;
|
|
9773
|
+
if (withheld) {
|
|
9774
|
+
if (toolUseId)
|
|
9775
|
+
this.withheldToolUses.delete(toolUseId);
|
|
9776
|
+
this.pendingWithholdWithoutId = undefined;
|
|
9777
|
+
this.enqueue({
|
|
9778
|
+
kind: "tool_ended",
|
|
9779
|
+
source: "agent",
|
|
9780
|
+
payload: {
|
|
9781
|
+
toolName: name,
|
|
9782
|
+
toolUseId,
|
|
9783
|
+
output: `[withheld: ${withheld}]`,
|
|
9784
|
+
withheld
|
|
9785
|
+
}
|
|
9786
|
+
});
|
|
9787
|
+
return;
|
|
9788
|
+
}
|
|
9789
|
+
const redacted = redactToolCall({ output: content });
|
|
9321
9790
|
this.enqueue({
|
|
9322
9791
|
kind: "tool_ended",
|
|
9323
9792
|
source: "agent",
|
|
9324
|
-
payload: {
|
|
9325
|
-
toolName: name,
|
|
9326
|
-
toolUseId,
|
|
9327
|
-
output: content === undefined ? undefined : content.slice(0, MAX_OUTPUT_LEN)
|
|
9328
|
-
}
|
|
9793
|
+
payload: { toolName: name, toolUseId, output: redacted.output }
|
|
9329
9794
|
});
|
|
9330
9795
|
});
|
|
9331
9796
|
parser.on("text", (content) => {
|
|
@@ -9426,6 +9891,10 @@ class CliAgentRunner {
|
|
|
9426
9891
|
enqueue(body) {
|
|
9427
9892
|
if (this.stopped)
|
|
9428
9893
|
return;
|
|
9894
|
+
if (runEventPayloadExceedsLimit(body.payload)) {
|
|
9895
|
+
log31.warn(TAG28, `Dropping oversized ${body.kind} event: payload exceeds ${MAX_RUN_EVENT_PAYLOAD_BYTES} bytes`);
|
|
9896
|
+
return;
|
|
9897
|
+
}
|
|
9429
9898
|
this.buffer.push({ ...body, createdAt: new Date().toISOString() });
|
|
9430
9899
|
if (this.buffer.length >= MAX_BUFFER)
|
|
9431
9900
|
this.flush();
|
|
@@ -9440,27 +9909,49 @@ class CliAgentRunner {
|
|
|
9440
9909
|
if (this.flushing || this.buffer.length === 0)
|
|
9441
9910
|
return;
|
|
9442
9911
|
this.flushing = true;
|
|
9443
|
-
const batch = this.buffer.splice(0, MAX_BUFFER);
|
|
9912
|
+
const batch = this.buffer.splice(0, Math.min(this.nextBatchSize, MAX_BUFFER));
|
|
9444
9913
|
try {
|
|
9445
9914
|
await this.client.appendAgentRunEvents(this.cardId, {
|
|
9446
9915
|
sessionId: this.sessionId,
|
|
9447
9916
|
events: batch
|
|
9448
9917
|
});
|
|
9918
|
+
this.batchAttempts = 0;
|
|
9919
|
+
this.nextBatchSize = MAX_BUFFER;
|
|
9449
9920
|
} catch (err) {
|
|
9450
|
-
|
|
9451
|
-
this.
|
|
9452
|
-
|
|
9453
|
-
this.
|
|
9921
|
+
this.batchAttempts++;
|
|
9922
|
+
if (this.batchAttempts >= MAX_BATCH_ATTEMPTS) {
|
|
9923
|
+
this.batchAttempts = 0;
|
|
9924
|
+
this.requeue(batch);
|
|
9925
|
+
if (batch.length > 1) {
|
|
9926
|
+
this.nextBatchSize = Math.ceil(batch.length / 2);
|
|
9927
|
+
log31.warn(TAG28, `Flush of ${batch.length} run event(s) failed ${MAX_BATCH_ATTEMPTS}×; bisecting to ${this.nextBatchSize}: ${err}`);
|
|
9928
|
+
return;
|
|
9929
|
+
}
|
|
9930
|
+
const dropped = this.buffer.shift();
|
|
9931
|
+
this.nextBatchSize = MAX_BUFFER;
|
|
9932
|
+
log31.warn(TAG28, `Dropping one ${dropped?.kind} event the API refused ${MAX_BATCH_ATTEMPTS}×: ${err}`);
|
|
9933
|
+
return;
|
|
9454
9934
|
}
|
|
9935
|
+
log31.warn(TAG28, `Failed to flush run events (attempt ${this.batchAttempts}/${MAX_BATCH_ATTEMPTS}): ${err}`);
|
|
9936
|
+
this.requeue(batch);
|
|
9455
9937
|
} finally {
|
|
9456
9938
|
this.flushing = false;
|
|
9457
9939
|
}
|
|
9458
9940
|
}
|
|
9941
|
+
requeue(batch) {
|
|
9942
|
+
this.buffer.unshift(...batch);
|
|
9943
|
+
if (this.buffer.length > MAX_BUFFER) {
|
|
9944
|
+
this.buffer.length = MAX_BUFFER;
|
|
9945
|
+
}
|
|
9946
|
+
}
|
|
9459
9947
|
async flushFinal() {
|
|
9460
|
-
for (let i = 0;this.buffer.length > 0 && i <
|
|
9948
|
+
for (let i = 0;this.buffer.length > 0 && i < FINAL_FLUSH_ATTEMPTS; i++) {
|
|
9461
9949
|
const before = this.buffer.length;
|
|
9950
|
+
const beforeBatchSize = this.nextBatchSize;
|
|
9951
|
+
const beforeAttempts = this.batchAttempts;
|
|
9462
9952
|
await this.flush();
|
|
9463
|
-
|
|
9953
|
+
const inert = this.buffer.length >= before && this.nextBatchSize >= beforeBatchSize && this.batchAttempts <= beforeAttempts;
|
|
9954
|
+
if (inert)
|
|
9464
9955
|
break;
|
|
9465
9956
|
}
|
|
9466
9957
|
}
|
|
@@ -9484,8 +9975,10 @@ function mapCost(cost) {
|
|
|
9484
9975
|
durationMs: cost.durationMs
|
|
9485
9976
|
};
|
|
9486
9977
|
}
|
|
9487
|
-
var TAG28 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000,
|
|
9488
|
-
var init_cli_agent_runner = () => {
|
|
9978
|
+
var TAG28 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_BATCH_ATTEMPTS = 3, FINAL_FLUSH_ATTEMPTS = 40;
|
|
9979
|
+
var init_cli_agent_runner = __esm(() => {
|
|
9980
|
+
init_dist();
|
|
9981
|
+
});
|
|
9489
9982
|
|
|
9490
9983
|
// src/fanout.ts
|
|
9491
9984
|
import { log as log32 } from "@gethmy/harness";
|
|
@@ -10684,6 +11177,19 @@ class Worker {
|
|
|
10684
11177
|
log34.info(this.tag, resuming ? `Resuming #${card.short_id} "${card.title}" with ${this.grantedTurns ?? "the default"} more turns` : `Preparing #${card.short_id} "${card.title}"`);
|
|
10685
11178
|
const attemptCount = await this.stateStore.incrementAttempt(card.id);
|
|
10686
11179
|
const isRework = attemptCount > 1;
|
|
11180
|
+
const runStarts = this.stateStore.getCardRunStarts(card.id);
|
|
11181
|
+
const runCap = this.config.budget.maxRunsPerCard;
|
|
11182
|
+
if (crossedTurnCap(runStarts - 1, runStarts, runCap)) {
|
|
11183
|
+
log34.warn(this.tag, `#${card.short_id} reached its per-card run backstop — ${runStarts} of ${runCap} run starts. It will not be picked up again until it is reassigned or the cap is raised.`);
|
|
11184
|
+
try {
|
|
11185
|
+
await this.client.addComment(card.id, buildRunCapComment(runStarts, runCap), {
|
|
11186
|
+
commentType: "blocker",
|
|
11187
|
+
agentSessionId: this.sessionId ?? undefined
|
|
11188
|
+
});
|
|
11189
|
+
} catch (err) {
|
|
11190
|
+
log34.warn(this.tag, `Could not post the run-backstop notice for ${card.id}: ${err instanceof Error ? err.message : err}`);
|
|
11191
|
+
}
|
|
11192
|
+
}
|
|
10687
11193
|
const recordedBranch = recordedBranchForCard(card.description, card.short_id, [
|
|
10688
11194
|
this.config.worktree.failedBranchPrefix,
|
|
10689
11195
|
this.config.worktree.approvedBranchPrefix
|
|
@@ -10935,7 +11441,8 @@ class Worker {
|
|
|
10935
11441
|
}
|
|
10936
11442
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10937
11443
|
const rawStderr = err?.stderr;
|
|
10938
|
-
const errClass = classifyRunError2(typeof rawStderr === "string" && rawStderr ?
|
|
11444
|
+
const errClass = classifyRunError2(typeof rawStderr === "string" && rawStderr ? msg + `
|
|
11445
|
+
` + rawStderr : msg);
|
|
10939
11446
|
const sdkKind = err?.errorKind;
|
|
10940
11447
|
if (errClass.kind === null && sdkKind != null)
|
|
10941
11448
|
errClass.kind = sdkKind;
|
|
@@ -10943,11 +11450,6 @@ class Worker {
|
|
|
10943
11450
|
try {
|
|
10944
11451
|
this.onApiError?.(errClass);
|
|
10945
11452
|
} catch {}
|
|
10946
|
-
try {
|
|
10947
|
-
await this.stateStore.decrementAttempt(card.id);
|
|
10948
|
-
} catch (dErr) {
|
|
10949
|
-
log34.warn(this.tag, `Failed to roll back the attempt: ${dErr}`);
|
|
10950
|
-
}
|
|
10951
11453
|
await this.parkForDecision(card, "account_limit");
|
|
10952
11454
|
return;
|
|
10953
11455
|
}
|
|
@@ -12683,6 +13185,10 @@ class Pool {
|
|
|
12683
13185
|
}
|
|
12684
13186
|
}
|
|
12685
13187
|
async enqueue(card, column, labels, subtasks, mode = "implement") {
|
|
13188
|
+
if (card.done || card.archived_at) {
|
|
13189
|
+
log35.debug(TAG32, `#${card.short_id} skipped — ${card.done ? "done" : "archived"} (finished work is never enqueued, #1102)`);
|
|
13190
|
+
return;
|
|
13191
|
+
}
|
|
12686
13192
|
if (this.isCardKnown(card.id) || this.reservations.has(card.id)) {
|
|
12687
13193
|
log35.debug(TAG32, `Card ${card.id} already queued, active, or reserved, skipping`);
|
|
12688
13194
|
return;
|
|
@@ -13080,9 +13586,23 @@ I am handing the card back rather than holding it parked. Reassign it — that c
|
|
|
13080
13586
|
await this.stopRun(run, "Continue refused: the card is at its per-card turn cap.");
|
|
13081
13587
|
return;
|
|
13082
13588
|
}
|
|
13083
|
-
|
|
13084
|
-
|
|
13589
|
+
const runsSpent = this.budget.checkCardRuns(run.cardId);
|
|
13590
|
+
if (!runsSpent.allow) {
|
|
13591
|
+
log35.warn(TAG32, `Refusing a Continue on ${run.cardId} — ${runsSpent.detail}. Handing the card back: reassign it to clear the count, or raise budget.maxRunsPerCard.`);
|
|
13592
|
+
try {
|
|
13593
|
+
await this.client.addComment(run.cardId, `**That Continue cannot be granted.** ${runsSpent.detail}, which is this card's run backstop (\`budget.maxRunsPerCard\`), so the implement pipeline would refuse it again the moment it was dispatched.
|
|
13594
|
+
|
|
13595
|
+
I am handing the card back rather than holding it parked. Reassign it — that clears the count and starts it fresh — or raise the cap in the daemon config if this card genuinely needs the room. The branch is pushed, so nothing committed is lost.`, {
|
|
13596
|
+
commentType: "blocker",
|
|
13597
|
+
agentSessionId: run.sessionId ?? undefined
|
|
13598
|
+
});
|
|
13599
|
+
} catch (err) {
|
|
13600
|
+
log35.warn(TAG32, `Could not explain the refused Continue on ${run.cardId}: ${err instanceof Error ? err.message : err}`);
|
|
13601
|
+
}
|
|
13602
|
+
await this.stopRun(run, "Continue refused: the card is at its per-card run backstop.");
|
|
13603
|
+
return;
|
|
13085
13604
|
}
|
|
13605
|
+
await this.stateStore.decrementAttempt(run.cardId);
|
|
13086
13606
|
const grantedTurns = opts.extraTurns > 0 ? Math.min(opts.extraTurns, MAX_GRANTED_TURNS) : this.config.budget.pause.extraTurns ?? this.defaultTurnsFor(run.pipeline);
|
|
13087
13607
|
const canSpendConsumption = run.pipeline === "implement" && this.config.runner === "sdk";
|
|
13088
13608
|
const grantedBudgetUsd = !canSpendConsumption ? null : opts.extraBudgetUsd != null && opts.extraBudgetUsd > 0 ? Math.min(opts.extraBudgetUsd, MAX_GRANTED_BUDGET_USD) : this.config.budget.pause.extraBudgetUsd ?? this.config.sdk?.maxBudgetUsd ?? null;
|
|
@@ -13126,6 +13646,7 @@ I am handing the card back rather than holding it parked. Reassign it — that c
|
|
|
13126
13646
|
daemonPid: process.pid,
|
|
13127
13647
|
lastHeartbeatAt: Date.now()
|
|
13128
13648
|
});
|
|
13649
|
+
await this.stateStore.decrementAttempt(run.cardId);
|
|
13129
13650
|
try {
|
|
13130
13651
|
await this.client.updateAgentProgress(run.cardId, {
|
|
13131
13652
|
...this.sessionIdentityFor(run),
|
|
@@ -13214,6 +13735,20 @@ Reassign the card — that clears the turn count and starts it fresh — or rais
|
|
|
13214
13735
|
await this.stopAttemptCap(cardId, "Attempt budget cleared, but the card is at its per-card turn cap.");
|
|
13215
13736
|
return;
|
|
13216
13737
|
}
|
|
13738
|
+
const runsSpent = this.budget.checkCardRuns(cardId);
|
|
13739
|
+
if (!runsSpent.allow) {
|
|
13740
|
+
log35.warn(TAG32, `Attempt grant on ${cardId} cannot start a run — ${runsSpent.detail}. Clearing the attempt budget anyway; reassign the card to clear the run count, or raise budget.maxRunsPerCard.`);
|
|
13741
|
+
try {
|
|
13742
|
+
await this.client.addComment(cardId, `**Attempt budget cleared, but this card is at its run backstop.** ${runsSpent.detail}, which is its ceiling (\`budget.maxRunsPerCard\`), so I will not pick it up even with attempts restored.
|
|
13743
|
+
|
|
13744
|
+
Reassign the card — that clears the run count and starts it fresh — or raise the cap in the daemon config.`, { commentType: "blocker" });
|
|
13745
|
+
} catch (err) {
|
|
13746
|
+
log35.warn(TAG32, `Could not explain the capped attempt grant on ${cardId}: ${err instanceof Error ? err.message : err}`);
|
|
13747
|
+
}
|
|
13748
|
+
await this.stateStore.resetAttempts(cardId, { clearTurns: false });
|
|
13749
|
+
await this.stopAttemptCap(cardId, "Attempt budget cleared, but the card is at its per-card run backstop.");
|
|
13750
|
+
return;
|
|
13751
|
+
}
|
|
13217
13752
|
const identity = this.attemptCapIdentityFor(cardId);
|
|
13218
13753
|
await this.stateStore.resetAttempts(cardId, { clearTurns: false });
|
|
13219
13754
|
await this.stateStore.clearAwaitingDecision(cardId);
|
|
@@ -13274,6 +13809,10 @@ Reassign the card — that clears the turn count and starts it fresh — or rais
|
|
|
13274
13809
|
await this.retireUnroutableRuns(card.short_id, cardId);
|
|
13275
13810
|
return;
|
|
13276
13811
|
}
|
|
13812
|
+
if (card.done || card.archived_at) {
|
|
13813
|
+
await this.retireUnroutableRuns(card.short_id, cardId, `card is ${card.done ? "done" : "archived"} — finished work is never enqueued (#1102)`);
|
|
13814
|
+
return;
|
|
13815
|
+
}
|
|
13277
13816
|
const labelMap = buildLabelMap(board.labels ?? []);
|
|
13278
13817
|
const cardLabels = resolveCardLabels(card, labelMap);
|
|
13279
13818
|
const subtasks = card.subtasks ?? [];
|
|
@@ -13282,12 +13821,12 @@ Reassign the card — that clears the turn count and starts it fresh — or rais
|
|
|
13282
13821
|
log35.error(TAG32, `Failed to re-enqueue ${cardId} after a budget decision: ${err}`);
|
|
13283
13822
|
}
|
|
13284
13823
|
}
|
|
13285
|
-
async retireUnroutableRuns(shortId, cardId) {
|
|
13824
|
+
async retireUnroutableRuns(shortId, cardId, reason = "column not found on this board") {
|
|
13286
13825
|
const unended = this.stateStore.getRunsForCard(cardId).filter((r) => r.endedAt === null);
|
|
13287
|
-
log35.warn(TAG32, `#${shortId}:
|
|
13826
|
+
log35.warn(TAG32, `#${shortId}: ${reason} — retiring ${unended.length} run record(s) instead of re-enqueueing`);
|
|
13288
13827
|
for (const run of unended) {
|
|
13289
13828
|
await this.stateStore.endRun(run.runId, "orphaned", {
|
|
13290
|
-
errorMessage:
|
|
13829
|
+
errorMessage: `${reason} at re-enqueue — retired as unroutable (#1057/#1102)`
|
|
13291
13830
|
});
|
|
13292
13831
|
}
|
|
13293
13832
|
}
|
|
@@ -14569,7 +15108,7 @@ class Reconciler {
|
|
|
14569
15108
|
}
|
|
14570
15109
|
};
|
|
14571
15110
|
const assignedCards = cards.filter((c) => {
|
|
14572
|
-
if (c.assigned_agent_id !== this.agentId || c.archived_at)
|
|
15111
|
+
if (c.assigned_agent_id !== this.agentId || c.archived_at || c.done)
|
|
14573
15112
|
return false;
|
|
14574
15113
|
const col = columnMap.get(c.column_id);
|
|
14575
15114
|
return col ? classifyPickup(c, col.name, pickupConfig) !== null : false;
|
|
@@ -14878,7 +15417,8 @@ function mergeCardRecords(target, legacy) {
|
|
|
14878
15417
|
attempts: Math.max(target.attempts ?? 0, legacy.attempts ?? 0),
|
|
14879
15418
|
totalAttempts: Math.max(target.totalAttempts ?? 0, legacy.totalAttempts ?? 0),
|
|
14880
15419
|
totalCostCents: Math.max(target.totalCostCents ?? 0, legacy.totalCostCents ?? 0),
|
|
14881
|
-
totalTurns: Math.max(target.totalTurns ?? 0, legacy.totalTurns ?? 0)
|
|
15420
|
+
totalTurns: Math.max(target.totalTurns ?? 0, legacy.totalTurns ?? 0),
|
|
15421
|
+
runStarts: Math.max(target.runStarts ?? 0, legacy.runStarts ?? 0)
|
|
14882
15422
|
};
|
|
14883
15423
|
}
|
|
14884
15424
|
function mergeDaily(target, legacy) {
|
|
@@ -15729,6 +16269,7 @@ var init_worktree_gc = __esm(() => {
|
|
|
15729
16269
|
var exports_src = {};
|
|
15730
16270
|
__export(exports_src, {
|
|
15731
16271
|
validatePrerequisites: () => validatePrerequisites,
|
|
16272
|
+
tryEnqueueCard: () => tryEnqueueCard,
|
|
15732
16273
|
main: () => main
|
|
15733
16274
|
});
|
|
15734
16275
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
@@ -16111,6 +16652,10 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
|
|
|
16111
16652
|
log48.debug(TAG45, `Card ${cardId} no longer assigned to agent — skipping`);
|
|
16112
16653
|
return;
|
|
16113
16654
|
}
|
|
16655
|
+
if (card.done || card.archived_at) {
|
|
16656
|
+
log48.debug(TAG45, `Card ${cardId} is finished — skipping`);
|
|
16657
|
+
return;
|
|
16658
|
+
}
|
|
16114
16659
|
const board = await client.getBoard(config.projectId, { summary: true });
|
|
16115
16660
|
const columns = board.columns;
|
|
16116
16661
|
const column = columns.find((c) => c.id === card.column_id);
|
|
@@ -16182,5 +16727,6 @@ init_src();
|
|
|
16182
16727
|
|
|
16183
16728
|
export {
|
|
16184
16729
|
validatePrerequisites,
|
|
16730
|
+
tryEnqueueCard,
|
|
16185
16731
|
main
|
|
16186
16732
|
};
|