@gethmy/agent 1.10.1 → 1.10.3
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 +1 -1
- package/dist/cli.js +392 -189
- package/dist/index.js +388 -187
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -396,8 +396,12 @@ var init_types = __esm(() => {
|
|
|
396
396
|
},
|
|
397
397
|
claude: {
|
|
398
398
|
model: "opus",
|
|
399
|
+
escalateModel: "claude-fable-5",
|
|
400
|
+
escalateAfterAttempts: 2,
|
|
399
401
|
reviewModel: "sonnet",
|
|
400
|
-
maxTurns:
|
|
402
|
+
maxTurns: 80,
|
|
403
|
+
reviewMaxTurns: 60,
|
|
404
|
+
leanSettingSources: "local,user",
|
|
401
405
|
additionalArgs: []
|
|
402
406
|
},
|
|
403
407
|
worktree: {
|
|
@@ -2006,6 +2010,54 @@ function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES2) {
|
|
|
2006
2010
|
}
|
|
2007
2011
|
return { files, insertions, deletions };
|
|
2008
2012
|
}
|
|
2013
|
+
function summarizeUnifiedDiff(diff) {
|
|
2014
|
+
const files = [];
|
|
2015
|
+
let current = null;
|
|
2016
|
+
let totalAdded = 0;
|
|
2017
|
+
let totalRemoved = 0;
|
|
2018
|
+
for (const line of diff.split(`
|
|
2019
|
+
`)) {
|
|
2020
|
+
if (line.startsWith("diff --git")) {
|
|
2021
|
+
const m = line.match(/ b\/(.+)$/);
|
|
2022
|
+
current = {
|
|
2023
|
+
path: m ? m[1] : line.slice("diff --git ".length),
|
|
2024
|
+
added: 0,
|
|
2025
|
+
removed: 0
|
|
2026
|
+
};
|
|
2027
|
+
files.push(current);
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (!current)
|
|
2031
|
+
continue;
|
|
2032
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
2033
|
+
continue;
|
|
2034
|
+
if (line.startsWith("+")) {
|
|
2035
|
+
current.added++;
|
|
2036
|
+
totalAdded++;
|
|
2037
|
+
} else if (line.startsWith("-")) {
|
|
2038
|
+
current.removed++;
|
|
2039
|
+
totalRemoved++;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return { files, totalAdded, totalRemoved };
|
|
2043
|
+
}
|
|
2044
|
+
function formatDiffSummary(diff, maxFiles = 100) {
|
|
2045
|
+
const trimmed = diff.trim();
|
|
2046
|
+
if (!trimmed || diff === "(unable to retrieve diff)") {
|
|
2047
|
+
return trimmed ? diff : "(no diff available)";
|
|
2048
|
+
}
|
|
2049
|
+
const { files, totalAdded, totalRemoved } = summarizeUnifiedDiff(diff);
|
|
2050
|
+
if (files.length === 0)
|
|
2051
|
+
return "(no file changes detected in diff)";
|
|
2052
|
+
const shown = files.slice(0, maxFiles);
|
|
2053
|
+
const lines = shown.map((f) => ` ${f.path} | +${f.added} -${f.removed}`);
|
|
2054
|
+
if (files.length > maxFiles) {
|
|
2055
|
+
lines.push(` ... and ${files.length - maxFiles} more file(s)`);
|
|
2056
|
+
}
|
|
2057
|
+
lines.push(` ${files.length} file(s) changed, ${totalAdded} insertion(s)(+), ${totalRemoved} deletion(s)(-)`);
|
|
2058
|
+
return lines.join(`
|
|
2059
|
+
`);
|
|
2060
|
+
}
|
|
2009
2061
|
function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES2) {
|
|
2010
2062
|
try {
|
|
2011
2063
|
const raw = execFileSync5("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
|
|
@@ -2122,7 +2174,17 @@ async function runDeepReview(worktreePath, config, workerId) {
|
|
|
2122
2174
|
"```"
|
|
2123
2175
|
].join(`
|
|
2124
2176
|
`);
|
|
2125
|
-
const
|
|
2177
|
+
const leanSources = config.claude.leanSettingSources;
|
|
2178
|
+
const output = execFileSync6("claude", [
|
|
2179
|
+
"--print",
|
|
2180
|
+
"--model",
|
|
2181
|
+
"sonnet",
|
|
2182
|
+
"--max-turns",
|
|
2183
|
+
"10",
|
|
2184
|
+
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
2185
|
+
"--",
|
|
2186
|
+
reviewPrompt
|
|
2187
|
+
], {
|
|
2126
2188
|
cwd: worktreePath,
|
|
2127
2189
|
encoding: "utf-8",
|
|
2128
2190
|
timeout: config.verification.timeout,
|
|
@@ -2153,6 +2215,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2153
2215
|
"```"
|
|
2154
2216
|
].join(`
|
|
2155
2217
|
`);
|
|
2218
|
+
const leanSources = config.claude.leanSettingSources;
|
|
2156
2219
|
const args = [
|
|
2157
2220
|
"--print",
|
|
2158
2221
|
"--model",
|
|
@@ -2161,6 +2224,7 @@ function attemptAutoFix(worktreePath, config, errors) {
|
|
|
2161
2224
|
"50",
|
|
2162
2225
|
"--allowedTools",
|
|
2163
2226
|
"Bash,Read,Write,Edit,Glob,Grep",
|
|
2227
|
+
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
2164
2228
|
"--",
|
|
2165
2229
|
fixPrompt
|
|
2166
2230
|
];
|
|
@@ -2317,7 +2381,8 @@ function buildTokenPayload(stats) {
|
|
|
2317
2381
|
outputTokens: stats.cost.totalOutputTokens,
|
|
2318
2382
|
cacheCreationInputTokens: stats.cost.totalCacheCreationInputTokens,
|
|
2319
2383
|
cacheReadInputTokens: stats.cost.totalCacheReadInputTokens,
|
|
2320
|
-
modelName: stats.cost.modelName
|
|
2384
|
+
modelName: stats.cost.modelName,
|
|
2385
|
+
numTurns: stats.cost.numTurns
|
|
2321
2386
|
};
|
|
2322
2387
|
}
|
|
2323
2388
|
async function runCompletion(client, card, branchName, worktreePath, config, workerId, sessionStats, workspaceId, agentSessionId, stateStore, onMovedToCompletion) {
|
|
@@ -2903,7 +2968,8 @@ class ProgressTracker {
|
|
|
2903
2968
|
outputTokens: this.lastCost?.totalOutputTokens ?? 0,
|
|
2904
2969
|
cacheCreationInputTokens: this.lastCost?.totalCacheCreationInputTokens ?? 0,
|
|
2905
2970
|
cacheReadInputTokens: this.lastCost?.totalCacheReadInputTokens ?? 0,
|
|
2906
|
-
modelName: this.lastCost?.modelName
|
|
2971
|
+
modelName: this.lastCost?.modelName,
|
|
2972
|
+
numTurns: this.lastCost?.numTurns ?? 0
|
|
2907
2973
|
}).catch((err) => {
|
|
2908
2974
|
log.warn(TAG14, `Failed to send progress update: ${err}`);
|
|
2909
2975
|
});
|
|
@@ -3365,7 +3431,120 @@ var init_review_completion = __esm(() => {
|
|
|
3365
3431
|
init_worktree();
|
|
3366
3432
|
});
|
|
3367
3433
|
|
|
3368
|
-
//
|
|
3434
|
+
// ../harmony-shared/dist/cardLinks.js
|
|
3435
|
+
var init_cardLinks = () => {};
|
|
3436
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
3437
|
+
function sanitizeHeaderField(value) {
|
|
3438
|
+
return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
|
|
3439
|
+
}
|
|
3440
|
+
function authorLabel(c) {
|
|
3441
|
+
if (c.author_type === "agent")
|
|
3442
|
+
return "AI agent";
|
|
3443
|
+
const raw = c.author?.full_name || "teammate";
|
|
3444
|
+
return sanitizeHeaderField(raw);
|
|
3445
|
+
}
|
|
3446
|
+
function criticalIds(comments) {
|
|
3447
|
+
const keep = new Set;
|
|
3448
|
+
for (const c of comments) {
|
|
3449
|
+
if (c.comment_type === "decision")
|
|
3450
|
+
keep.add(c.id);
|
|
3451
|
+
if (c.supersedes_id) {
|
|
3452
|
+
keep.add(c.id);
|
|
3453
|
+
keep.add(c.supersedes_id);
|
|
3454
|
+
}
|
|
3455
|
+
if (c.confirms_id) {
|
|
3456
|
+
keep.add(c.id);
|
|
3457
|
+
keep.add(c.confirms_id);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
return keep;
|
|
3461
|
+
}
|
|
3462
|
+
function serializeCommentThread(comments, options = {}) {
|
|
3463
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
3464
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
3465
|
+
if (visible.length === 0)
|
|
3466
|
+
return "";
|
|
3467
|
+
const indexById = new Map;
|
|
3468
|
+
visible.forEach((c, i) => {
|
|
3469
|
+
indexById.set(c.id, i + 1);
|
|
3470
|
+
});
|
|
3471
|
+
let rendered = visible;
|
|
3472
|
+
let elidedCount = 0;
|
|
3473
|
+
if (maxComments && visible.length > maxComments) {
|
|
3474
|
+
const keep = criticalIds(visible);
|
|
3475
|
+
const recentThreshold = visible.length - maxComments;
|
|
3476
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
3477
|
+
elidedCount = visible.length - rendered.length;
|
|
3478
|
+
}
|
|
3479
|
+
const ref = (id) => {
|
|
3480
|
+
const n = indexById.get(id);
|
|
3481
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
3482
|
+
};
|
|
3483
|
+
const lines = [];
|
|
3484
|
+
if (elidedCount > 0) {
|
|
3485
|
+
lines.push({
|
|
3486
|
+
at: visible[0]?.created_at ?? "",
|
|
3487
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
3488
|
+
});
|
|
3489
|
+
}
|
|
3490
|
+
for (const c of rendered) {
|
|
3491
|
+
const tags = [];
|
|
3492
|
+
if (c.edited_at)
|
|
3493
|
+
tags.push("edited");
|
|
3494
|
+
if (c.supersedes_id)
|
|
3495
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
3496
|
+
if (c.confirms_id)
|
|
3497
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
3498
|
+
if (c.resolved_at)
|
|
3499
|
+
tags.push("resolved");
|
|
3500
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
3501
|
+
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
3502
|
+
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
3503
|
+
lines.push({
|
|
3504
|
+
at: c.created_at,
|
|
3505
|
+
text: `${header}
|
|
3506
|
+
<comment-body>
|
|
3507
|
+
${fencedBody}
|
|
3508
|
+
</comment-body>`
|
|
3509
|
+
});
|
|
3510
|
+
}
|
|
3511
|
+
for (const a of activity) {
|
|
3512
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
3513
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
3514
|
+
}
|
|
3515
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
3516
|
+
const body = lines.map((l) => l.text).join(`
|
|
3517
|
+
|
|
3518
|
+
`);
|
|
3519
|
+
const instruction = includeInstructions ? `
|
|
3520
|
+
|
|
3521
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
3522
|
+
return `## ${heading} (oldest → newest)
|
|
3523
|
+
|
|
3524
|
+
${body}${instruction}`;
|
|
3525
|
+
}
|
|
3526
|
+
var CONFLICT_INSTRUCTION;
|
|
3527
|
+
var init_commentSerializer = __esm(() => {
|
|
3528
|
+
CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
3529
|
+
});
|
|
3530
|
+
|
|
3531
|
+
// ../harmony-shared/dist/constants.js
|
|
3532
|
+
var TIMINGS;
|
|
3533
|
+
var init_constants = __esm(() => {
|
|
3534
|
+
TIMINGS = {
|
|
3535
|
+
SEARCH_DEBOUNCE: 300,
|
|
3536
|
+
AUTOSAVE_DEBOUNCE: 1000,
|
|
3537
|
+
TOAST_DURATION: 3000,
|
|
3538
|
+
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
3539
|
+
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
3540
|
+
};
|
|
3541
|
+
});
|
|
3542
|
+
// ../harmony-shared/dist/logger.js
|
|
3543
|
+
var init_logger = () => {};
|
|
3544
|
+
// ../harmony-shared/dist/projectTemplates.js
|
|
3545
|
+
var init_projectTemplates = () => {};
|
|
3546
|
+
|
|
3547
|
+
// ../harmony-shared/dist/reviewMethodology.js
|
|
3369
3548
|
var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
|
|
3370
3549
|
Report findings; do NOT fix them. This is a read-only review.
|
|
3371
3550
|
|
|
@@ -3441,7 +3620,41 @@ For each page affected by the changes:
|
|
|
3441
3620
|
- Use snapshot for navigation — client-side routes may not appear in link lists.
|
|
3442
3621
|
- Check for stale state: navigate away and back — does data refresh correctly?
|
|
3443
3622
|
- Test browser back/forward — does the app handle history correctly?
|
|
3444
|
-
- Watch for hydration errors or layout shifts after dynamic content loads
|
|
3623
|
+
- Watch for hydration errors or layout shifts after dynamic content loads.`, REVIEW_VERDICT_SCHEMA = `{
|
|
3624
|
+
"verdict": "approved" | "rejected",
|
|
3625
|
+
"summary": "Brief overall assessment",
|
|
3626
|
+
"scopeCheck": {
|
|
3627
|
+
"status": "clean" | "drift" | "missing",
|
|
3628
|
+
"notes": "Optional explanation of scope issues"
|
|
3629
|
+
},
|
|
3630
|
+
"findings": [
|
|
3631
|
+
{
|
|
3632
|
+
"severity": "critical" | "major" | "minor",
|
|
3633
|
+
"category": "sql-safety | race-condition | llm-trust | enum-completeness | visual | functional | ux | console | scope | other",
|
|
3634
|
+
"title": "Short title",
|
|
3635
|
+
"description": "Detailed description of the issue",
|
|
3636
|
+
"location": "file:line (if applicable)"
|
|
3637
|
+
}
|
|
3638
|
+
]
|
|
3639
|
+
}`, REVIEW_DECISION_RULES = `- **rejected**: Any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
|
|
3640
|
+
- **approved**: No critical findings, at most 1 major finding with minor findings OK.`;
|
|
3641
|
+
// ../harmony-shared/dist/types.js
|
|
3642
|
+
var init_types2 = () => {};
|
|
3643
|
+
|
|
3644
|
+
// ../harmony-shared/dist/index.js
|
|
3645
|
+
var init_dist = __esm(() => {
|
|
3646
|
+
init_cardLinks();
|
|
3647
|
+
init_commentSerializer();
|
|
3648
|
+
init_constants();
|
|
3649
|
+
init_logger();
|
|
3650
|
+
init_projectTemplates();
|
|
3651
|
+
init_types2();
|
|
3652
|
+
});
|
|
3653
|
+
|
|
3654
|
+
// src/review-knowledge.ts
|
|
3655
|
+
var init_review_knowledge = __esm(() => {
|
|
3656
|
+
init_dist();
|
|
3657
|
+
});
|
|
3445
3658
|
|
|
3446
3659
|
// src/review-prompt.ts
|
|
3447
3660
|
function buildReviewSystemPrompt() {
|
|
@@ -3452,15 +3665,13 @@ ${REVIEW_SYSTEM_PROMPT}
|
|
|
3452
3665
|
|
|
3453
3666
|
${QA_VISUAL_CHECKLIST}`;
|
|
3454
3667
|
}
|
|
3455
|
-
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl,
|
|
3668
|
+
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diffSummary, baseBranch) {
|
|
3456
3669
|
const { card, labels, subtasks } = enriched;
|
|
3457
3670
|
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
3458
3671
|
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
|
|
3459
3672
|
`) : "No subtasks defined.";
|
|
3460
3673
|
const description = card.description?.trim() || "No description provided.";
|
|
3461
|
-
const
|
|
3462
|
-
|
|
3463
|
-
... (diff truncated at 80K characters)` : diff;
|
|
3674
|
+
const diffRange = branchName ? `origin/${baseBranch}..HEAD` : "HEAD";
|
|
3464
3675
|
const branchLine = branchName ? `**Branch**: ${branchName}` : `**Mode**: Local review (no branch — reviewing working tree changes)`;
|
|
3465
3676
|
return `## Card: #${card.short_id} - ${card.title}
|
|
3466
3677
|
**Labels**: ${labelStr}
|
|
@@ -3472,10 +3683,15 @@ ${description}
|
|
|
3472
3683
|
## Subtasks (Acceptance Criteria)
|
|
3473
3684
|
${subtaskStr}
|
|
3474
3685
|
|
|
3475
|
-
##
|
|
3476
|
-
\`\`\`diff
|
|
3477
|
-
${truncatedDiff}
|
|
3686
|
+
## Changed Files (git diff --stat ${diffRange})
|
|
3478
3687
|
\`\`\`
|
|
3688
|
+
${diffSummary}
|
|
3689
|
+
\`\`\`
|
|
3690
|
+
|
|
3691
|
+
The full diff is intentionally NOT inlined here. Inspect the changes yourself —
|
|
3692
|
+
you have Read, Grep, Glob, and read-only Bash:
|
|
3693
|
+
- Run \`git diff ${diffRange}\` (or \`git diff ${diffRange} -- <file>\`) for the full hunks.
|
|
3694
|
+
- Read / Grep the changed files above and trace new values through their consumers.
|
|
3479
3695
|
|
|
3480
3696
|
## Review Steps
|
|
3481
3697
|
|
|
@@ -3506,33 +3722,18 @@ Use the \`/browse\` skill to navigate to ${previewUrl} and apply the visual QA c
|
|
|
3506
3722
|
After completing all steps, output EXACTLY one JSON block (and nothing else after it):
|
|
3507
3723
|
|
|
3508
3724
|
\`\`\`json
|
|
3509
|
-
{
|
|
3510
|
-
"verdict": "approved" | "rejected",
|
|
3511
|
-
"summary": "Brief overall assessment",
|
|
3512
|
-
"scopeCheck": {
|
|
3513
|
-
"status": "clean" | "drift" | "missing",
|
|
3514
|
-
"notes": "Optional explanation of scope issues"
|
|
3515
|
-
},
|
|
3516
|
-
"findings": [
|
|
3517
|
-
{
|
|
3518
|
-
"severity": "critical" | "major" | "minor",
|
|
3519
|
-
"category": "sql-safety | race-condition | llm-trust | enum-completeness | visual | functional | ux | console | scope | other",
|
|
3520
|
-
"title": "Short title",
|
|
3521
|
-
"description": "Detailed description of the issue",
|
|
3522
|
-
"location": "file:line (if applicable)"
|
|
3523
|
-
}
|
|
3524
|
-
]
|
|
3525
|
-
}
|
|
3725
|
+
${REVIEW_VERDICT_SCHEMA}
|
|
3526
3726
|
\`\`\`
|
|
3527
3727
|
|
|
3528
3728
|
**Decision rules:**
|
|
3529
|
-
|
|
3530
|
-
- **approved**: No critical findings, at most 1 major finding with minor findings OK.
|
|
3729
|
+
${REVIEW_DECISION_RULES}
|
|
3531
3730
|
|
|
3532
3731
|
**Do NOT modify any code.** This is a read-only review.
|
|
3533
3732
|
${branchName ? `You are reviewing code in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.` : `You are reviewing local changes in the repository at \`${worktreePath}\`.`}`;
|
|
3534
3733
|
}
|
|
3535
|
-
var init_review_prompt = () => {
|
|
3734
|
+
var init_review_prompt = __esm(() => {
|
|
3735
|
+
init_review_knowledge();
|
|
3736
|
+
});
|
|
3536
3737
|
|
|
3537
3738
|
// src/run-log.ts
|
|
3538
3739
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
@@ -3674,13 +3875,19 @@ class StateStore {
|
|
|
3674
3875
|
heartbeat(runId) {
|
|
3675
3876
|
return this.updateRun(runId, { lastHeartbeatAt: Date.now() });
|
|
3676
3877
|
}
|
|
3677
|
-
endRun(runId, status,
|
|
3878
|
+
endRun(runId, status, opts = {}) {
|
|
3678
3879
|
const patch = {
|
|
3679
3880
|
status,
|
|
3680
3881
|
endedAt: Date.now()
|
|
3681
3882
|
};
|
|
3682
|
-
if (errorMessage !== undefined)
|
|
3683
|
-
patch.errorMessage = errorMessage;
|
|
3883
|
+
if (opts.errorMessage !== undefined)
|
|
3884
|
+
patch.errorMessage = opts.errorMessage;
|
|
3885
|
+
if (opts.costCents !== undefined && opts.costCents > 0) {
|
|
3886
|
+
patch.costCents = opts.costCents;
|
|
3887
|
+
}
|
|
3888
|
+
if (opts.numTurns !== undefined && opts.numTurns > 0) {
|
|
3889
|
+
patch.numTurns = opts.numTurns;
|
|
3890
|
+
}
|
|
3684
3891
|
return this.updateRun(runId, patch);
|
|
3685
3892
|
}
|
|
3686
3893
|
getRun(runId) {
|
|
@@ -4112,6 +4319,19 @@ class ReviewWorker {
|
|
|
4112
4319
|
get isActive() {
|
|
4113
4320
|
return this.state === "preparing" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
4114
4321
|
}
|
|
4322
|
+
get costCents() {
|
|
4323
|
+
const cost = (this.progressTracker?.stats ?? this.lastSessionStats)?.cost;
|
|
4324
|
+
return cost ? Math.round(cost.totalCostUsd * 100) : 0;
|
|
4325
|
+
}
|
|
4326
|
+
endLedger() {
|
|
4327
|
+
const cost = (this.lastSessionStats ?? this.progressTracker?.stats)?.cost;
|
|
4328
|
+
if (!cost)
|
|
4329
|
+
return { costCents: 0, numTurns: 0 };
|
|
4330
|
+
return {
|
|
4331
|
+
costCents: Math.round(cost.totalCostUsd * 100),
|
|
4332
|
+
numTurns: cost.numTurns
|
|
4333
|
+
};
|
|
4334
|
+
}
|
|
4115
4335
|
async run(card, column, labels, subtasks) {
|
|
4116
4336
|
this.aborted = false;
|
|
4117
4337
|
this.timedOut = false;
|
|
@@ -4137,7 +4357,8 @@ class ReviewWorker {
|
|
|
4137
4357
|
lastHeartbeatAt: this.startedAt,
|
|
4138
4358
|
endedAt: null,
|
|
4139
4359
|
status: "active",
|
|
4140
|
-
costCents: 0
|
|
4360
|
+
costCents: 0,
|
|
4361
|
+
numTurns: 0
|
|
4141
4362
|
});
|
|
4142
4363
|
this.branchName = extractBranchFromDescription(card.description);
|
|
4143
4364
|
const localMode = !this.branchName;
|
|
@@ -4216,6 +4437,7 @@ class ReviewWorker {
|
|
|
4216
4437
|
} catch {
|
|
4217
4438
|
diff = "(unable to retrieve diff)";
|
|
4218
4439
|
}
|
|
4440
|
+
const diffSummary = formatDiffSummary(diff);
|
|
4219
4441
|
const previewUrl = `http://localhost:${port}`;
|
|
4220
4442
|
const enriched = {
|
|
4221
4443
|
card,
|
|
@@ -4225,7 +4447,7 @@ class ReviewWorker {
|
|
|
4225
4447
|
mode: "review"
|
|
4226
4448
|
};
|
|
4227
4449
|
const systemPrompt = buildReviewSystemPrompt();
|
|
4228
|
-
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl,
|
|
4450
|
+
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch);
|
|
4229
4451
|
try {
|
|
4230
4452
|
await this.client.recordPromptHistory({
|
|
4231
4453
|
cardId: card.id,
|
|
@@ -4308,7 +4530,7 @@ class ReviewWorker {
|
|
|
4308
4530
|
}
|
|
4309
4531
|
if (this.runId) {
|
|
4310
4532
|
try {
|
|
4311
|
-
await this.stateStore.endRun(this.runId, this.state === "error" ? "paused" : "completed");
|
|
4533
|
+
await this.stateStore.endRun(this.runId, this.state === "error" ? "paused" : "completed", this.endLedger());
|
|
4312
4534
|
} catch {}
|
|
4313
4535
|
}
|
|
4314
4536
|
} finally {
|
|
@@ -4317,7 +4539,7 @@ class ReviewWorker {
|
|
|
4317
4539
|
const run = this.stateStore.getRun(this.runId);
|
|
4318
4540
|
if (run && run.endedAt === null) {
|
|
4319
4541
|
const status = this.timedOut ? "failed" : this.state === "error" || this.aborted ? "paused" : "completed";
|
|
4320
|
-
await this.stateStore.endRun(this.runId, status);
|
|
4542
|
+
await this.stateStore.endRun(this.runId, status, this.endLedger());
|
|
4321
4543
|
}
|
|
4322
4544
|
} catch {}
|
|
4323
4545
|
}
|
|
@@ -4402,6 +4624,7 @@ class ReviewWorker {
|
|
|
4402
4624
|
}
|
|
4403
4625
|
spawnClaude(prompt, systemPrompt, tracker, shortId) {
|
|
4404
4626
|
return new Promise((resolve3, reject) => {
|
|
4627
|
+
const leanSources = this.config.claude.leanSettingSources;
|
|
4405
4628
|
const args = [
|
|
4406
4629
|
"--output-format",
|
|
4407
4630
|
"stream-json",
|
|
@@ -4409,9 +4632,10 @@ class ReviewWorker {
|
|
|
4409
4632
|
"--model",
|
|
4410
4633
|
this.config.claude.reviewModel,
|
|
4411
4634
|
"--max-turns",
|
|
4412
|
-
String(this.config.claude.
|
|
4635
|
+
String(this.config.claude.reviewMaxTurns),
|
|
4413
4636
|
"--allowedTools",
|
|
4414
4637
|
"Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
|
|
4638
|
+
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
4415
4639
|
...systemPrompt ? ["--append-system-prompt", systemPrompt] : [],
|
|
4416
4640
|
...this.config.claude.additionalArgs,
|
|
4417
4641
|
"--",
|
|
@@ -4557,6 +4781,7 @@ var TAG19 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEO
|
|
|
4557
4781
|
var init_review_worker = __esm(() => {
|
|
4558
4782
|
init_board_helpers();
|
|
4559
4783
|
init_completion();
|
|
4784
|
+
init_git_diff_stat();
|
|
4560
4785
|
init_log();
|
|
4561
4786
|
init_process_group();
|
|
4562
4787
|
init_progress_tracker();
|
|
@@ -4703,130 +4928,12 @@ var init_unblock = __esm(() => {
|
|
|
4703
4928
|
init_log();
|
|
4704
4929
|
});
|
|
4705
4930
|
|
|
4706
|
-
//
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
return
|
|
4711
|
-
}
|
|
4712
|
-
function authorLabel(c) {
|
|
4713
|
-
if (c.author_type === "agent")
|
|
4714
|
-
return "AI agent";
|
|
4715
|
-
const raw = c.author?.full_name || "teammate";
|
|
4716
|
-
return sanitizeHeaderField(raw);
|
|
4931
|
+
// src/model-tier.ts
|
|
4932
|
+
function chooseImplementModel(claude, priority, attempts) {
|
|
4933
|
+
const highPriority = priority === "high" || priority === "urgent";
|
|
4934
|
+
const escalated = highPriority || attempts >= claude.escalateAfterAttempts;
|
|
4935
|
+
return { model: escalated ? claude.escalateModel : claude.model, escalated };
|
|
4717
4936
|
}
|
|
4718
|
-
function criticalIds(comments) {
|
|
4719
|
-
const keep = new Set;
|
|
4720
|
-
for (const c of comments) {
|
|
4721
|
-
if (c.comment_type === "decision")
|
|
4722
|
-
keep.add(c.id);
|
|
4723
|
-
if (c.supersedes_id) {
|
|
4724
|
-
keep.add(c.id);
|
|
4725
|
-
keep.add(c.supersedes_id);
|
|
4726
|
-
}
|
|
4727
|
-
if (c.confirms_id) {
|
|
4728
|
-
keep.add(c.id);
|
|
4729
|
-
keep.add(c.confirms_id);
|
|
4730
|
-
}
|
|
4731
|
-
}
|
|
4732
|
-
return keep;
|
|
4733
|
-
}
|
|
4734
|
-
function serializeCommentThread(comments, options = {}) {
|
|
4735
|
-
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
4736
|
-
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
4737
|
-
if (visible.length === 0)
|
|
4738
|
-
return "";
|
|
4739
|
-
const indexById = new Map;
|
|
4740
|
-
visible.forEach((c, i) => {
|
|
4741
|
-
indexById.set(c.id, i + 1);
|
|
4742
|
-
});
|
|
4743
|
-
let rendered = visible;
|
|
4744
|
-
let elidedCount = 0;
|
|
4745
|
-
if (maxComments && visible.length > maxComments) {
|
|
4746
|
-
const keep = criticalIds(visible);
|
|
4747
|
-
const recentThreshold = visible.length - maxComments;
|
|
4748
|
-
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
4749
|
-
elidedCount = visible.length - rendered.length;
|
|
4750
|
-
}
|
|
4751
|
-
const ref = (id) => {
|
|
4752
|
-
const n = indexById.get(id);
|
|
4753
|
-
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
4754
|
-
};
|
|
4755
|
-
const lines = [];
|
|
4756
|
-
if (elidedCount > 0) {
|
|
4757
|
-
lines.push({
|
|
4758
|
-
at: visible[0]?.created_at ?? "",
|
|
4759
|
-
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
4760
|
-
});
|
|
4761
|
-
}
|
|
4762
|
-
for (const c of rendered) {
|
|
4763
|
-
const tags = [];
|
|
4764
|
-
if (c.edited_at)
|
|
4765
|
-
tags.push("edited");
|
|
4766
|
-
if (c.supersedes_id)
|
|
4767
|
-
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
4768
|
-
if (c.confirms_id)
|
|
4769
|
-
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
4770
|
-
if (c.resolved_at)
|
|
4771
|
-
tags.push("resolved");
|
|
4772
|
-
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
4773
|
-
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
4774
|
-
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
4775
|
-
lines.push({
|
|
4776
|
-
at: c.created_at,
|
|
4777
|
-
text: `${header}
|
|
4778
|
-
<comment-body>
|
|
4779
|
-
${fencedBody}
|
|
4780
|
-
</comment-body>`
|
|
4781
|
-
});
|
|
4782
|
-
}
|
|
4783
|
-
for (const a of activity) {
|
|
4784
|
-
const actor = a.actor ? `${a.actor} ` : "";
|
|
4785
|
-
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
4786
|
-
}
|
|
4787
|
-
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
4788
|
-
const body = lines.map((l) => l.text).join(`
|
|
4789
|
-
|
|
4790
|
-
`);
|
|
4791
|
-
const instruction = includeInstructions ? `
|
|
4792
|
-
|
|
4793
|
-
${CONFLICT_INSTRUCTION}` : "";
|
|
4794
|
-
return `## ${heading} (oldest → newest)
|
|
4795
|
-
|
|
4796
|
-
${body}${instruction}`;
|
|
4797
|
-
}
|
|
4798
|
-
var CONFLICT_INSTRUCTION;
|
|
4799
|
-
var init_commentSerializer = __esm(() => {
|
|
4800
|
-
CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
4801
|
-
});
|
|
4802
|
-
|
|
4803
|
-
// ../harmony-shared/dist/constants.js
|
|
4804
|
-
var TIMINGS;
|
|
4805
|
-
var init_constants = __esm(() => {
|
|
4806
|
-
TIMINGS = {
|
|
4807
|
-
SEARCH_DEBOUNCE: 300,
|
|
4808
|
-
AUTOSAVE_DEBOUNCE: 1000,
|
|
4809
|
-
TOAST_DURATION: 3000,
|
|
4810
|
-
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
4811
|
-
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
4812
|
-
};
|
|
4813
|
-
});
|
|
4814
|
-
// ../harmony-shared/dist/logger.js
|
|
4815
|
-
var init_logger = () => {};
|
|
4816
|
-
// ../harmony-shared/dist/projectTemplates.js
|
|
4817
|
-
var init_projectTemplates = () => {};
|
|
4818
|
-
// ../harmony-shared/dist/types.js
|
|
4819
|
-
var init_types2 = () => {};
|
|
4820
|
-
|
|
4821
|
-
// ../harmony-shared/dist/index.js
|
|
4822
|
-
var init_dist = __esm(() => {
|
|
4823
|
-
init_cardLinks();
|
|
4824
|
-
init_commentSerializer();
|
|
4825
|
-
init_constants();
|
|
4826
|
-
init_logger();
|
|
4827
|
-
init_projectTemplates();
|
|
4828
|
-
init_types2();
|
|
4829
|
-
});
|
|
4830
4937
|
|
|
4831
4938
|
// src/prompt.ts
|
|
4832
4939
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
@@ -4988,6 +5095,8 @@ class Worker {
|
|
|
4988
5095
|
verificationFailed = false;
|
|
4989
5096
|
sessionId = null;
|
|
4990
5097
|
runId = null;
|
|
5098
|
+
runCostCents = 0;
|
|
5099
|
+
runTurns = 0;
|
|
4991
5100
|
constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
|
|
4992
5101
|
this.config = config;
|
|
4993
5102
|
this.client = client;
|
|
@@ -5040,10 +5149,20 @@ class Worker {
|
|
|
5040
5149
|
get isActive() {
|
|
5041
5150
|
return this.state === "preparing" || this.state === "planning" || this.state === "running" || this.state === "verifying" || this.state === "completing";
|
|
5042
5151
|
}
|
|
5152
|
+
get costCents() {
|
|
5153
|
+
const live = this.progressTracker?.stats.cost;
|
|
5154
|
+
const liveCents = live ? Math.round(live.totalCostUsd * 100) : 0;
|
|
5155
|
+
return this.runCostCents + liveCents;
|
|
5156
|
+
}
|
|
5157
|
+
runLedger() {
|
|
5158
|
+
return { costCents: this.runCostCents, numTurns: this.runTurns };
|
|
5159
|
+
}
|
|
5043
5160
|
async run(card, column, labels, subtasks) {
|
|
5044
5161
|
this.aborted = false;
|
|
5045
5162
|
this.timedOut = false;
|
|
5046
5163
|
this.verificationFailed = false;
|
|
5164
|
+
this.runCostCents = 0;
|
|
5165
|
+
this.runTurns = 0;
|
|
5047
5166
|
this.cardId = card.id;
|
|
5048
5167
|
this.startedAt = Date.now();
|
|
5049
5168
|
this.runId = newRunId();
|
|
@@ -5068,7 +5187,8 @@ class Worker {
|
|
|
5068
5187
|
lastHeartbeatAt: this.startedAt,
|
|
5069
5188
|
endedAt: null,
|
|
5070
5189
|
status: "active",
|
|
5071
|
-
costCents: 0
|
|
5190
|
+
costCents: 0,
|
|
5191
|
+
numTurns: 0
|
|
5072
5192
|
});
|
|
5073
5193
|
const { session } = await this.client.startAgentSession(card.id, {
|
|
5074
5194
|
agentIdentifier: agentIdentifier(this.id),
|
|
@@ -5126,7 +5246,9 @@ class Worker {
|
|
|
5126
5246
|
this.timedOut = true;
|
|
5127
5247
|
this.cancel();
|
|
5128
5248
|
}, this.config.maxTimeout);
|
|
5129
|
-
await this.spawnClaude(prompt, card, subtasks
|
|
5249
|
+
await this.spawnClaude(prompt, card, subtasks, {
|
|
5250
|
+
model: this.selectImplementModel(card)
|
|
5251
|
+
});
|
|
5130
5252
|
if (this.aborted)
|
|
5131
5253
|
return;
|
|
5132
5254
|
if (this.timeoutTimer) {
|
|
@@ -5184,7 +5306,10 @@ class Worker {
|
|
|
5184
5306
|
}
|
|
5185
5307
|
if (this.runId) {
|
|
5186
5308
|
try {
|
|
5187
|
-
await this.stateStore.endRun(this.runId, "failed",
|
|
5309
|
+
await this.stateStore.endRun(this.runId, "failed", {
|
|
5310
|
+
errorMessage: errClass.kind ?? msg,
|
|
5311
|
+
...this.runLedger()
|
|
5312
|
+
});
|
|
5188
5313
|
} catch {}
|
|
5189
5314
|
if (apiError) {
|
|
5190
5315
|
await this.stateStore.decrementAttempt(card.id);
|
|
@@ -5196,7 +5321,7 @@ class Worker {
|
|
|
5196
5321
|
const succeeded = this.runId && this.state !== "error" && !this.aborted && !this.verificationFailed;
|
|
5197
5322
|
if (succeeded) {
|
|
5198
5323
|
try {
|
|
5199
|
-
await this.stateStore.endRun(this.runId, "completed");
|
|
5324
|
+
await this.stateStore.endRun(this.runId, "completed", this.runLedger());
|
|
5200
5325
|
} catch {}
|
|
5201
5326
|
await this.recordOutcome(card.id, "success");
|
|
5202
5327
|
} else if (this.runId && this.timedOut) {
|
|
@@ -5222,16 +5347,25 @@ class Worker {
|
|
|
5222
5347
|
log.error(this.tag, `timeout transition failed on #${card.short_id}: ${tErr instanceof TransitionError ? tErr.detail : tErr}`);
|
|
5223
5348
|
}
|
|
5224
5349
|
try {
|
|
5225
|
-
await this.stateStore.endRun(this.runId, "failed",
|
|
5350
|
+
await this.stateStore.endRun(this.runId, "failed", {
|
|
5351
|
+
errorMessage: "timeout",
|
|
5352
|
+
...this.runLedger()
|
|
5353
|
+
});
|
|
5226
5354
|
} catch {}
|
|
5227
5355
|
await this.recordOutcome(card.id, "failure");
|
|
5228
5356
|
} else if (this.runId && this.aborted) {
|
|
5229
5357
|
try {
|
|
5230
|
-
await this.stateStore.endRun(this.runId, "paused",
|
|
5358
|
+
await this.stateStore.endRun(this.runId, "paused", {
|
|
5359
|
+
errorMessage: "cancelled",
|
|
5360
|
+
...this.runLedger()
|
|
5361
|
+
});
|
|
5231
5362
|
} catch {}
|
|
5232
5363
|
} else if (this.runId && this.verificationFailed) {
|
|
5233
5364
|
try {
|
|
5234
|
-
await this.stateStore.endRun(this.runId, "paused",
|
|
5365
|
+
await this.stateStore.endRun(this.runId, "paused", {
|
|
5366
|
+
errorMessage: "verification",
|
|
5367
|
+
...this.runLedger()
|
|
5368
|
+
});
|
|
5235
5369
|
} catch {}
|
|
5236
5370
|
await this.recordOutcome(card.id, "failure");
|
|
5237
5371
|
}
|
|
@@ -5240,6 +5374,14 @@ class Worker {
|
|
|
5240
5374
|
this.onDone(this);
|
|
5241
5375
|
}
|
|
5242
5376
|
}
|
|
5377
|
+
selectImplementModel(card) {
|
|
5378
|
+
const attempts = this.stateStore.getCard(card.id)?.attempts ?? 1;
|
|
5379
|
+
const { model, escalated } = chooseImplementModel(this.config.claude, card.priority, attempts);
|
|
5380
|
+
if (escalated) {
|
|
5381
|
+
log.info(this.tag, `Escalating implement model to "${model}" (attempts=${attempts}, priority=${card.priority ?? "none"})`);
|
|
5382
|
+
}
|
|
5383
|
+
return model;
|
|
5384
|
+
}
|
|
5243
5385
|
async recordOutcome(cardId, outcome) {
|
|
5244
5386
|
try {
|
|
5245
5387
|
const cost = this.lastSessionStats?.cost;
|
|
@@ -5510,6 +5652,11 @@ class Worker {
|
|
|
5510
5652
|
this.process.on("close", (code) => {
|
|
5511
5653
|
this.process = null;
|
|
5512
5654
|
this.lastSessionStats = this.progressTracker?.stats;
|
|
5655
|
+
const spawnCost = this.lastSessionStats?.cost;
|
|
5656
|
+
if (spawnCost) {
|
|
5657
|
+
this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
|
|
5658
|
+
this.runTurns += spawnCost.numTurns;
|
|
5659
|
+
}
|
|
5513
5660
|
this.progressTracker?.flushFinal();
|
|
5514
5661
|
this.progressTracker?.stop();
|
|
5515
5662
|
this.progressTracker = null;
|
|
@@ -5557,6 +5704,8 @@ class Worker {
|
|
|
5557
5704
|
this.startedAt = null;
|
|
5558
5705
|
this.runId = null;
|
|
5559
5706
|
this.sessionId = null;
|
|
5707
|
+
this.runCostCents = 0;
|
|
5708
|
+
this.runTurns = 0;
|
|
5560
5709
|
}
|
|
5561
5710
|
}
|
|
5562
5711
|
var TAG23 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
|
|
@@ -5779,7 +5928,8 @@ class Pool {
|
|
|
5779
5928
|
cardId: w.cardId,
|
|
5780
5929
|
cardShortId: null,
|
|
5781
5930
|
startedAt: w.startedAt,
|
|
5782
|
-
branchName: w.branchName
|
|
5931
|
+
branchName: w.branchName,
|
|
5932
|
+
costCents: w.costCents
|
|
5783
5933
|
});
|
|
5784
5934
|
}
|
|
5785
5935
|
for (const w of this.reviewWorkers) {
|
|
@@ -5790,7 +5940,8 @@ class Pool {
|
|
|
5790
5940
|
cardId: w.cardId,
|
|
5791
5941
|
cardShortId: null,
|
|
5792
5942
|
startedAt: w.startedAt,
|
|
5793
|
-
branchName: w.branchName
|
|
5943
|
+
branchName: w.branchName,
|
|
5944
|
+
costCents: w.costCents
|
|
5794
5945
|
});
|
|
5795
5946
|
}
|
|
5796
5947
|
return out;
|
|
@@ -6017,7 +6168,9 @@ async function recoverRun(run, store, client, config, outcome) {
|
|
|
6017
6168
|
}
|
|
6018
6169
|
}
|
|
6019
6170
|
try {
|
|
6020
|
-
await store.endRun(run.runId, "orphaned",
|
|
6171
|
+
await store.endRun(run.runId, "orphaned", {
|
|
6172
|
+
errorMessage: "recovered after daemon restart"
|
|
6173
|
+
});
|
|
6021
6174
|
} catch (err) {
|
|
6022
6175
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6023
6176
|
outcome.errors.push(`endRun: ${msg}`);
|
|
@@ -6337,9 +6490,11 @@ function configRows(config, projectName, gitProvider, httpPort) {
|
|
|
6337
6490
|
const reviewEnabled = config.agent.review.enabled;
|
|
6338
6491
|
const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
|
|
6339
6492
|
const flowDesc = reviewEnabled ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}` : `Pickup ${config.agent.pickupColumns.join(", ")}`;
|
|
6493
|
+
const { model, escalateModel, reviewModel } = config.agent.claude;
|
|
6494
|
+
const modelDesc = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
|
|
6340
6495
|
rows.push({
|
|
6341
6496
|
label: "Model",
|
|
6342
|
-
value: `${
|
|
6497
|
+
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
6343
6498
|
});
|
|
6344
6499
|
const tail = [];
|
|
6345
6500
|
if (gitProvider)
|
|
@@ -6610,11 +6765,23 @@ var exports_worktree_gc = {};
|
|
|
6610
6765
|
__export(exports_worktree_gc, {
|
|
6611
6766
|
runWorktreeGc: () => runWorktreeGc,
|
|
6612
6767
|
pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
|
|
6768
|
+
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
6613
6769
|
WorktreeGc: () => WorktreeGc
|
|
6614
6770
|
});
|
|
6615
6771
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
6616
6772
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
6617
6773
|
import { resolve as resolve3 } from "node:path";
|
|
6774
|
+
function isTransientGitNetworkError(message) {
|
|
6775
|
+
return TRANSIENT_GIT_NETWORK_ERROR.test(message);
|
|
6776
|
+
}
|
|
6777
|
+
function gitErrorDetail(err) {
|
|
6778
|
+
if (err && typeof err === "object" && "stderr" in err) {
|
|
6779
|
+
const stderr = String(err.stderr ?? "").trim();
|
|
6780
|
+
if (stderr)
|
|
6781
|
+
return stderr;
|
|
6782
|
+
}
|
|
6783
|
+
return err instanceof Error ? err.message : String(err);
|
|
6784
|
+
}
|
|
6618
6785
|
function runWorktreeGc(basePath, store, opts = {}) {
|
|
6619
6786
|
const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1000;
|
|
6620
6787
|
const now = (opts.now ?? Date.now)();
|
|
@@ -6702,13 +6869,16 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6702
6869
|
try {
|
|
6703
6870
|
execFileSync9("git", ["fetch", "--prune", "origin"], {
|
|
6704
6871
|
cwd: repoRoot,
|
|
6705
|
-
stdio: "pipe"
|
|
6872
|
+
stdio: "pipe",
|
|
6873
|
+
...GIT_NETWORK_EXEC
|
|
6706
6874
|
});
|
|
6707
6875
|
} catch (err) {
|
|
6708
|
-
|
|
6709
|
-
|
|
6710
|
-
|
|
6711
|
-
|
|
6876
|
+
const detail = gitErrorDetail(err);
|
|
6877
|
+
if (isTransientGitNetworkError(detail)) {
|
|
6878
|
+
log.debug(TAG30, `Remote branch GC skipped — remote unreachable: ${detail}`);
|
|
6879
|
+
return result;
|
|
6880
|
+
}
|
|
6881
|
+
result.errors.push({ ref: "fetch", error: detail });
|
|
6712
6882
|
}
|
|
6713
6883
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
6714
6884
|
let listing = "";
|
|
@@ -6725,7 +6895,9 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6725
6895
|
});
|
|
6726
6896
|
return result;
|
|
6727
6897
|
}
|
|
6728
|
-
const
|
|
6898
|
+
const clock = opts.now ?? Date.now;
|
|
6899
|
+
const cutoffSecs = clock() / 1000 - opts.retentionDays * 24 * 60 * 60;
|
|
6900
|
+
const sweepDeadline = clock() + GIT_PRUNE_SWEEP_BUDGET_MS;
|
|
6729
6901
|
for (const line of listing.split(`
|
|
6730
6902
|
`)) {
|
|
6731
6903
|
const trimmed = line.trim();
|
|
@@ -6741,17 +6913,24 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6741
6913
|
result.skipped.push(ref);
|
|
6742
6914
|
continue;
|
|
6743
6915
|
}
|
|
6916
|
+
if (clock() > sweepDeadline) {
|
|
6917
|
+
log.debug(TAG30, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
|
|
6918
|
+
break;
|
|
6919
|
+
}
|
|
6744
6920
|
try {
|
|
6745
6921
|
execFileSync9("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
6746
6922
|
cwd: repoRoot,
|
|
6747
|
-
stdio: "pipe"
|
|
6923
|
+
stdio: "pipe",
|
|
6924
|
+
...GIT_NETWORK_EXEC
|
|
6748
6925
|
});
|
|
6749
6926
|
result.removed.push(ref);
|
|
6750
6927
|
} catch (err) {
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6928
|
+
const detail = gitErrorDetail(err);
|
|
6929
|
+
if (isTransientGitNetworkError(detail)) {
|
|
6930
|
+
log.debug(TAG30, `Remote branch GC interrupted — remote unreachable: ${detail}`);
|
|
6931
|
+
break;
|
|
6932
|
+
}
|
|
6933
|
+
result.errors.push({ ref, error: detail });
|
|
6755
6934
|
}
|
|
6756
6935
|
}
|
|
6757
6936
|
if (result.removed.length > 0) {
|
|
@@ -6809,10 +6988,32 @@ function getRepoRoot2() {
|
|
|
6809
6988
|
return null;
|
|
6810
6989
|
}
|
|
6811
6990
|
}
|
|
6812
|
-
var TAG30 = "worktree-gc";
|
|
6991
|
+
var TAG30 = "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;
|
|
6813
6992
|
var init_worktree_gc = __esm(() => {
|
|
6814
6993
|
init_log();
|
|
6815
6994
|
init_worktree();
|
|
6995
|
+
GIT_NETWORK_EXEC = {
|
|
6996
|
+
timeout: GIT_NETWORK_TIMEOUT_MS,
|
|
6997
|
+
killSignal: "SIGKILL",
|
|
6998
|
+
env: {
|
|
6999
|
+
...process.env,
|
|
7000
|
+
GIT_SSH_COMMAND: `${process.env.GIT_SSH_COMMAND ?? "ssh"} -o ConnectTimeout=${GIT_SSH_CONNECT_TIMEOUT_SECS} -o BatchMode=yes`,
|
|
7001
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
7002
|
+
}
|
|
7003
|
+
};
|
|
7004
|
+
TRANSIENT_GIT_NETWORK_ERROR = new RegExp([
|
|
7005
|
+
"timed out",
|
|
7006
|
+
"connection refused",
|
|
7007
|
+
"connection reset",
|
|
7008
|
+
"could not resolve host",
|
|
7009
|
+
"temporary failure in name resolution",
|
|
7010
|
+
"ssh: connect to host",
|
|
7011
|
+
"kex_exchange_identification",
|
|
7012
|
+
"network is unreachable",
|
|
7013
|
+
"etimedout",
|
|
7014
|
+
"enotfound",
|
|
7015
|
+
"enetunreach"
|
|
7016
|
+
].join("|"), "i");
|
|
6816
7017
|
});
|
|
6817
7018
|
|
|
6818
7019
|
// src/index.ts
|