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