@gethmy/agent 1.10.2 → 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 +313 -170
- package/dist/index.js +313 -170
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ npx @gethmy/mcp setup
|
|
|
45
45
|
"agent": {
|
|
46
46
|
"poolSize": 3,
|
|
47
47
|
"pickupColumns": ["To Do"],
|
|
48
|
-
"claude": { "model": "opus", "reviewModel": "sonnet" },
|
|
48
|
+
"claude": { "model": "opus", "escalateModel": "claude-fable-5", "reviewModel": "sonnet" },
|
|
49
49
|
"review": {
|
|
50
50
|
"enabled": true,
|
|
51
51
|
"poolSize": 2,
|
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
|
];
|
|
@@ -3368,7 +3432,120 @@ var init_review_completion = __esm(() => {
|
|
|
3368
3432
|
init_worktree();
|
|
3369
3433
|
});
|
|
3370
3434
|
|
|
3371
|
-
//
|
|
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
|
|
3372
3549
|
var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
|
|
3373
3550
|
Report findings; do NOT fix them. This is a read-only review.
|
|
3374
3551
|
|
|
@@ -3444,7 +3621,41 @@ For each page affected by the changes:
|
|
|
3444
3621
|
- Use snapshot for navigation — client-side routes may not appear in link lists.
|
|
3445
3622
|
- Check for stale state: navigate away and back — does data refresh correctly?
|
|
3446
3623
|
- Test browser back/forward — does the app handle history correctly?
|
|
3447
|
-
- 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
|
+
});
|
|
3448
3659
|
|
|
3449
3660
|
// src/review-prompt.ts
|
|
3450
3661
|
function buildReviewSystemPrompt() {
|
|
@@ -3455,15 +3666,13 @@ ${REVIEW_SYSTEM_PROMPT}
|
|
|
3455
3666
|
|
|
3456
3667
|
${QA_VISUAL_CHECKLIST}`;
|
|
3457
3668
|
}
|
|
3458
|
-
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl,
|
|
3669
|
+
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diffSummary, baseBranch) {
|
|
3459
3670
|
const { card, labels, subtasks } = enriched;
|
|
3460
3671
|
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
3461
3672
|
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
|
|
3462
3673
|
`) : "No subtasks defined.";
|
|
3463
3674
|
const description = card.description?.trim() || "No description provided.";
|
|
3464
|
-
const
|
|
3465
|
-
|
|
3466
|
-
... (diff truncated at 80K characters)` : diff;
|
|
3675
|
+
const diffRange = branchName ? `origin/${baseBranch}..HEAD` : "HEAD";
|
|
3467
3676
|
const branchLine = branchName ? `**Branch**: ${branchName}` : `**Mode**: Local review (no branch — reviewing working tree changes)`;
|
|
3468
3677
|
return `## Card: #${card.short_id} - ${card.title}
|
|
3469
3678
|
**Labels**: ${labelStr}
|
|
@@ -3475,10 +3684,15 @@ ${description}
|
|
|
3475
3684
|
## Subtasks (Acceptance Criteria)
|
|
3476
3685
|
${subtaskStr}
|
|
3477
3686
|
|
|
3478
|
-
##
|
|
3479
|
-
\`\`\`diff
|
|
3480
|
-
${truncatedDiff}
|
|
3687
|
+
## Changed Files (git diff --stat ${diffRange})
|
|
3481
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.
|
|
3482
3696
|
|
|
3483
3697
|
## Review Steps
|
|
3484
3698
|
|
|
@@ -3509,33 +3723,18 @@ Use the \`/browse\` skill to navigate to ${previewUrl} and apply the visual QA c
|
|
|
3509
3723
|
After completing all steps, output EXACTLY one JSON block (and nothing else after it):
|
|
3510
3724
|
|
|
3511
3725
|
\`\`\`json
|
|
3512
|
-
{
|
|
3513
|
-
"verdict": "approved" | "rejected",
|
|
3514
|
-
"summary": "Brief overall assessment",
|
|
3515
|
-
"scopeCheck": {
|
|
3516
|
-
"status": "clean" | "drift" | "missing",
|
|
3517
|
-
"notes": "Optional explanation of scope issues"
|
|
3518
|
-
},
|
|
3519
|
-
"findings": [
|
|
3520
|
-
{
|
|
3521
|
-
"severity": "critical" | "major" | "minor",
|
|
3522
|
-
"category": "sql-safety | race-condition | llm-trust | enum-completeness | visual | functional | ux | console | scope | other",
|
|
3523
|
-
"title": "Short title",
|
|
3524
|
-
"description": "Detailed description of the issue",
|
|
3525
|
-
"location": "file:line (if applicable)"
|
|
3526
|
-
}
|
|
3527
|
-
]
|
|
3528
|
-
}
|
|
3726
|
+
${REVIEW_VERDICT_SCHEMA}
|
|
3529
3727
|
\`\`\`
|
|
3530
3728
|
|
|
3531
3729
|
**Decision rules:**
|
|
3532
|
-
|
|
3533
|
-
- **approved**: No critical findings, at most 1 major finding with minor findings OK.
|
|
3730
|
+
${REVIEW_DECISION_RULES}
|
|
3534
3731
|
|
|
3535
3732
|
**Do NOT modify any code.** This is a read-only review.
|
|
3536
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}\`.`}`;
|
|
3537
3734
|
}
|
|
3538
|
-
var init_review_prompt = () => {
|
|
3735
|
+
var init_review_prompt = __esm(() => {
|
|
3736
|
+
init_review_knowledge();
|
|
3737
|
+
});
|
|
3539
3738
|
|
|
3540
3739
|
// src/run-log.ts
|
|
3541
3740
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
@@ -4239,6 +4438,7 @@ class ReviewWorker {
|
|
|
4239
4438
|
} catch {
|
|
4240
4439
|
diff = "(unable to retrieve diff)";
|
|
4241
4440
|
}
|
|
4441
|
+
const diffSummary = formatDiffSummary(diff);
|
|
4242
4442
|
const previewUrl = `http://localhost:${port}`;
|
|
4243
4443
|
const enriched = {
|
|
4244
4444
|
card,
|
|
@@ -4248,7 +4448,7 @@ class ReviewWorker {
|
|
|
4248
4448
|
mode: "review"
|
|
4249
4449
|
};
|
|
4250
4450
|
const systemPrompt = buildReviewSystemPrompt();
|
|
4251
|
-
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl,
|
|
4451
|
+
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch);
|
|
4252
4452
|
try {
|
|
4253
4453
|
await this.client.recordPromptHistory({
|
|
4254
4454
|
cardId: card.id,
|
|
@@ -4425,6 +4625,7 @@ class ReviewWorker {
|
|
|
4425
4625
|
}
|
|
4426
4626
|
spawnClaude(prompt, systemPrompt, tracker, shortId) {
|
|
4427
4627
|
return new Promise((resolve3, reject) => {
|
|
4628
|
+
const leanSources = this.config.claude.leanSettingSources;
|
|
4428
4629
|
const args = [
|
|
4429
4630
|
"--output-format",
|
|
4430
4631
|
"stream-json",
|
|
@@ -4432,9 +4633,10 @@ class ReviewWorker {
|
|
|
4432
4633
|
"--model",
|
|
4433
4634
|
this.config.claude.reviewModel,
|
|
4434
4635
|
"--max-turns",
|
|
4435
|
-
String(this.config.claude.
|
|
4636
|
+
String(this.config.claude.reviewMaxTurns),
|
|
4436
4637
|
"--allowedTools",
|
|
4437
4638
|
"Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
|
|
4639
|
+
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
4438
4640
|
...systemPrompt ? ["--append-system-prompt", systemPrompt] : [],
|
|
4439
4641
|
...this.config.claude.additionalArgs,
|
|
4440
4642
|
"--",
|
|
@@ -4580,6 +4782,7 @@ var TAG19 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEO
|
|
|
4580
4782
|
var init_review_worker = __esm(() => {
|
|
4581
4783
|
init_board_helpers();
|
|
4582
4784
|
init_completion();
|
|
4785
|
+
init_git_diff_stat();
|
|
4583
4786
|
init_log();
|
|
4584
4787
|
init_process_group();
|
|
4585
4788
|
init_progress_tracker();
|
|
@@ -4726,130 +4929,12 @@ var init_unblock = __esm(() => {
|
|
|
4726
4929
|
init_log();
|
|
4727
4930
|
});
|
|
4728
4931
|
|
|
4729
|
-
//
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
return
|
|
4734
|
-
}
|
|
4735
|
-
function authorLabel(c) {
|
|
4736
|
-
if (c.author_type === "agent")
|
|
4737
|
-
return "AI agent";
|
|
4738
|
-
const raw = c.author?.full_name || "teammate";
|
|
4739
|
-
return sanitizeHeaderField(raw);
|
|
4740
|
-
}
|
|
4741
|
-
function criticalIds(comments) {
|
|
4742
|
-
const keep = new Set;
|
|
4743
|
-
for (const c of comments) {
|
|
4744
|
-
if (c.comment_type === "decision")
|
|
4745
|
-
keep.add(c.id);
|
|
4746
|
-
if (c.supersedes_id) {
|
|
4747
|
-
keep.add(c.id);
|
|
4748
|
-
keep.add(c.supersedes_id);
|
|
4749
|
-
}
|
|
4750
|
-
if (c.confirms_id) {
|
|
4751
|
-
keep.add(c.id);
|
|
4752
|
-
keep.add(c.confirms_id);
|
|
4753
|
-
}
|
|
4754
|
-
}
|
|
4755
|
-
return keep;
|
|
4756
|
-
}
|
|
4757
|
-
function serializeCommentThread(comments, options = {}) {
|
|
4758
|
-
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
4759
|
-
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
4760
|
-
if (visible.length === 0)
|
|
4761
|
-
return "";
|
|
4762
|
-
const indexById = new Map;
|
|
4763
|
-
visible.forEach((c, i) => {
|
|
4764
|
-
indexById.set(c.id, i + 1);
|
|
4765
|
-
});
|
|
4766
|
-
let rendered = visible;
|
|
4767
|
-
let elidedCount = 0;
|
|
4768
|
-
if (maxComments && visible.length > maxComments) {
|
|
4769
|
-
const keep = criticalIds(visible);
|
|
4770
|
-
const recentThreshold = visible.length - maxComments;
|
|
4771
|
-
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
4772
|
-
elidedCount = visible.length - rendered.length;
|
|
4773
|
-
}
|
|
4774
|
-
const ref = (id) => {
|
|
4775
|
-
const n = indexById.get(id);
|
|
4776
|
-
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
4777
|
-
};
|
|
4778
|
-
const lines = [];
|
|
4779
|
-
if (elidedCount > 0) {
|
|
4780
|
-
lines.push({
|
|
4781
|
-
at: visible[0]?.created_at ?? "",
|
|
4782
|
-
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
4783
|
-
});
|
|
4784
|
-
}
|
|
4785
|
-
for (const c of rendered) {
|
|
4786
|
-
const tags = [];
|
|
4787
|
-
if (c.edited_at)
|
|
4788
|
-
tags.push("edited");
|
|
4789
|
-
if (c.supersedes_id)
|
|
4790
|
-
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
4791
|
-
if (c.confirms_id)
|
|
4792
|
-
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
4793
|
-
if (c.resolved_at)
|
|
4794
|
-
tags.push("resolved");
|
|
4795
|
-
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
4796
|
-
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
4797
|
-
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
4798
|
-
lines.push({
|
|
4799
|
-
at: c.created_at,
|
|
4800
|
-
text: `${header}
|
|
4801
|
-
<comment-body>
|
|
4802
|
-
${fencedBody}
|
|
4803
|
-
</comment-body>`
|
|
4804
|
-
});
|
|
4805
|
-
}
|
|
4806
|
-
for (const a of activity) {
|
|
4807
|
-
const actor = a.actor ? `${a.actor} ` : "";
|
|
4808
|
-
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
4809
|
-
}
|
|
4810
|
-
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
4811
|
-
const body = lines.map((l) => l.text).join(`
|
|
4812
|
-
|
|
4813
|
-
`);
|
|
4814
|
-
const instruction = includeInstructions ? `
|
|
4815
|
-
|
|
4816
|
-
${CONFLICT_INSTRUCTION}` : "";
|
|
4817
|
-
return `## ${heading} (oldest → newest)
|
|
4818
|
-
|
|
4819
|
-
${body}${instruction}`;
|
|
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 };
|
|
4820
4937
|
}
|
|
4821
|
-
var CONFLICT_INSTRUCTION;
|
|
4822
|
-
var init_commentSerializer = __esm(() => {
|
|
4823
|
-
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.";
|
|
4824
|
-
});
|
|
4825
|
-
|
|
4826
|
-
// ../harmony-shared/dist/constants.js
|
|
4827
|
-
var TIMINGS;
|
|
4828
|
-
var init_constants = __esm(() => {
|
|
4829
|
-
TIMINGS = {
|
|
4830
|
-
SEARCH_DEBOUNCE: 300,
|
|
4831
|
-
AUTOSAVE_DEBOUNCE: 1000,
|
|
4832
|
-
TOAST_DURATION: 3000,
|
|
4833
|
-
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
4834
|
-
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
4835
|
-
};
|
|
4836
|
-
});
|
|
4837
|
-
// ../harmony-shared/dist/logger.js
|
|
4838
|
-
var init_logger = () => {};
|
|
4839
|
-
// ../harmony-shared/dist/projectTemplates.js
|
|
4840
|
-
var init_projectTemplates = () => {};
|
|
4841
|
-
// ../harmony-shared/dist/types.js
|
|
4842
|
-
var init_types2 = () => {};
|
|
4843
|
-
|
|
4844
|
-
// ../harmony-shared/dist/index.js
|
|
4845
|
-
var init_dist = __esm(() => {
|
|
4846
|
-
init_cardLinks();
|
|
4847
|
-
init_commentSerializer();
|
|
4848
|
-
init_constants();
|
|
4849
|
-
init_logger();
|
|
4850
|
-
init_projectTemplates();
|
|
4851
|
-
init_types2();
|
|
4852
|
-
});
|
|
4853
4938
|
|
|
4854
4939
|
// src/prompt.ts
|
|
4855
4940
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
@@ -5162,7 +5247,9 @@ class Worker {
|
|
|
5162
5247
|
this.timedOut = true;
|
|
5163
5248
|
this.cancel();
|
|
5164
5249
|
}, this.config.maxTimeout);
|
|
5165
|
-
await this.spawnClaude(prompt, card, subtasks
|
|
5250
|
+
await this.spawnClaude(prompt, card, subtasks, {
|
|
5251
|
+
model: this.selectImplementModel(card)
|
|
5252
|
+
});
|
|
5166
5253
|
if (this.aborted)
|
|
5167
5254
|
return;
|
|
5168
5255
|
if (this.timeoutTimer) {
|
|
@@ -5288,6 +5375,14 @@ class Worker {
|
|
|
5288
5375
|
this.onDone(this);
|
|
5289
5376
|
}
|
|
5290
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
|
+
}
|
|
5291
5386
|
async recordOutcome(cardId, outcome) {
|
|
5292
5387
|
try {
|
|
5293
5388
|
const cost = this.lastSessionStats?.cost;
|
|
@@ -6396,9 +6491,11 @@ function configRows(config, projectName, gitProvider, httpPort) {
|
|
|
6396
6491
|
const reviewEnabled = config.agent.review.enabled;
|
|
6397
6492
|
const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
|
|
6398
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}`;
|
|
6399
6496
|
rows.push({
|
|
6400
6497
|
label: "Model",
|
|
6401
|
-
value: `${
|
|
6498
|
+
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
6402
6499
|
});
|
|
6403
6500
|
const tail = [];
|
|
6404
6501
|
if (gitProvider)
|
|
@@ -6669,11 +6766,23 @@ var exports_worktree_gc = {};
|
|
|
6669
6766
|
__export(exports_worktree_gc, {
|
|
6670
6767
|
runWorktreeGc: () => runWorktreeGc,
|
|
6671
6768
|
pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
|
|
6769
|
+
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
6672
6770
|
WorktreeGc: () => WorktreeGc
|
|
6673
6771
|
});
|
|
6674
6772
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
6675
6773
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
6676
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
|
+
}
|
|
6677
6786
|
function runWorktreeGc(basePath, store, opts = {}) {
|
|
6678
6787
|
const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1000;
|
|
6679
6788
|
const now = (opts.now ?? Date.now)();
|
|
@@ -6761,13 +6870,16 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6761
6870
|
try {
|
|
6762
6871
|
execFileSync9("git", ["fetch", "--prune", "origin"], {
|
|
6763
6872
|
cwd: repoRoot,
|
|
6764
|
-
stdio: "pipe"
|
|
6873
|
+
stdio: "pipe",
|
|
6874
|
+
...GIT_NETWORK_EXEC
|
|
6765
6875
|
});
|
|
6766
6876
|
} catch (err) {
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
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 });
|
|
6771
6883
|
}
|
|
6772
6884
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
6773
6885
|
let listing = "";
|
|
@@ -6784,7 +6896,9 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6784
6896
|
});
|
|
6785
6897
|
return result;
|
|
6786
6898
|
}
|
|
6787
|
-
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;
|
|
6788
6902
|
for (const line of listing.split(`
|
|
6789
6903
|
`)) {
|
|
6790
6904
|
const trimmed = line.trim();
|
|
@@ -6800,17 +6914,24 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6800
6914
|
result.skipped.push(ref);
|
|
6801
6915
|
continue;
|
|
6802
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
|
+
}
|
|
6803
6921
|
try {
|
|
6804
6922
|
execFileSync9("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
6805
6923
|
cwd: repoRoot,
|
|
6806
|
-
stdio: "pipe"
|
|
6924
|
+
stdio: "pipe",
|
|
6925
|
+
...GIT_NETWORK_EXEC
|
|
6807
6926
|
});
|
|
6808
6927
|
result.removed.push(ref);
|
|
6809
6928
|
} catch (err) {
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
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 });
|
|
6814
6935
|
}
|
|
6815
6936
|
}
|
|
6816
6937
|
if (result.removed.length > 0) {
|
|
@@ -6868,10 +6989,32 @@ function getRepoRoot2() {
|
|
|
6868
6989
|
return null;
|
|
6869
6990
|
}
|
|
6870
6991
|
}
|
|
6871
|
-
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;
|
|
6872
6993
|
var init_worktree_gc = __esm(() => {
|
|
6873
6994
|
init_log();
|
|
6874
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");
|
|
6875
7018
|
});
|
|
6876
7019
|
|
|
6877
7020
|
// src/index.ts
|
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
|
];
|
|
@@ -3367,7 +3431,120 @@ var init_review_completion = __esm(() => {
|
|
|
3367
3431
|
init_worktree();
|
|
3368
3432
|
});
|
|
3369
3433
|
|
|
3370
|
-
//
|
|
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
|
|
3371
3548
|
var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
|
|
3372
3549
|
Report findings; do NOT fix them. This is a read-only review.
|
|
3373
3550
|
|
|
@@ -3443,7 +3620,41 @@ For each page affected by the changes:
|
|
|
3443
3620
|
- Use snapshot for navigation — client-side routes may not appear in link lists.
|
|
3444
3621
|
- Check for stale state: navigate away and back — does data refresh correctly?
|
|
3445
3622
|
- Test browser back/forward — does the app handle history correctly?
|
|
3446
|
-
- 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
|
+
});
|
|
3447
3658
|
|
|
3448
3659
|
// src/review-prompt.ts
|
|
3449
3660
|
function buildReviewSystemPrompt() {
|
|
@@ -3454,15 +3665,13 @@ ${REVIEW_SYSTEM_PROMPT}
|
|
|
3454
3665
|
|
|
3455
3666
|
${QA_VISUAL_CHECKLIST}`;
|
|
3456
3667
|
}
|
|
3457
|
-
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl,
|
|
3668
|
+
function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diffSummary, baseBranch) {
|
|
3458
3669
|
const { card, labels, subtasks } = enriched;
|
|
3459
3670
|
const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
|
|
3460
3671
|
const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
|
|
3461
3672
|
`) : "No subtasks defined.";
|
|
3462
3673
|
const description = card.description?.trim() || "No description provided.";
|
|
3463
|
-
const
|
|
3464
|
-
|
|
3465
|
-
... (diff truncated at 80K characters)` : diff;
|
|
3674
|
+
const diffRange = branchName ? `origin/${baseBranch}..HEAD` : "HEAD";
|
|
3466
3675
|
const branchLine = branchName ? `**Branch**: ${branchName}` : `**Mode**: Local review (no branch — reviewing working tree changes)`;
|
|
3467
3676
|
return `## Card: #${card.short_id} - ${card.title}
|
|
3468
3677
|
**Labels**: ${labelStr}
|
|
@@ -3474,10 +3683,15 @@ ${description}
|
|
|
3474
3683
|
## Subtasks (Acceptance Criteria)
|
|
3475
3684
|
${subtaskStr}
|
|
3476
3685
|
|
|
3477
|
-
##
|
|
3478
|
-
\`\`\`diff
|
|
3479
|
-
${truncatedDiff}
|
|
3686
|
+
## Changed Files (git diff --stat ${diffRange})
|
|
3480
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.
|
|
3481
3695
|
|
|
3482
3696
|
## Review Steps
|
|
3483
3697
|
|
|
@@ -3508,33 +3722,18 @@ Use the \`/browse\` skill to navigate to ${previewUrl} and apply the visual QA c
|
|
|
3508
3722
|
After completing all steps, output EXACTLY one JSON block (and nothing else after it):
|
|
3509
3723
|
|
|
3510
3724
|
\`\`\`json
|
|
3511
|
-
{
|
|
3512
|
-
"verdict": "approved" | "rejected",
|
|
3513
|
-
"summary": "Brief overall assessment",
|
|
3514
|
-
"scopeCheck": {
|
|
3515
|
-
"status": "clean" | "drift" | "missing",
|
|
3516
|
-
"notes": "Optional explanation of scope issues"
|
|
3517
|
-
},
|
|
3518
|
-
"findings": [
|
|
3519
|
-
{
|
|
3520
|
-
"severity": "critical" | "major" | "minor",
|
|
3521
|
-
"category": "sql-safety | race-condition | llm-trust | enum-completeness | visual | functional | ux | console | scope | other",
|
|
3522
|
-
"title": "Short title",
|
|
3523
|
-
"description": "Detailed description of the issue",
|
|
3524
|
-
"location": "file:line (if applicable)"
|
|
3525
|
-
}
|
|
3526
|
-
]
|
|
3527
|
-
}
|
|
3725
|
+
${REVIEW_VERDICT_SCHEMA}
|
|
3528
3726
|
\`\`\`
|
|
3529
3727
|
|
|
3530
3728
|
**Decision rules:**
|
|
3531
|
-
|
|
3532
|
-
- **approved**: No critical findings, at most 1 major finding with minor findings OK.
|
|
3729
|
+
${REVIEW_DECISION_RULES}
|
|
3533
3730
|
|
|
3534
3731
|
**Do NOT modify any code.** This is a read-only review.
|
|
3535
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}\`.`}`;
|
|
3536
3733
|
}
|
|
3537
|
-
var init_review_prompt = () => {
|
|
3734
|
+
var init_review_prompt = __esm(() => {
|
|
3735
|
+
init_review_knowledge();
|
|
3736
|
+
});
|
|
3538
3737
|
|
|
3539
3738
|
// src/run-log.ts
|
|
3540
3739
|
import { createWriteStream, mkdirSync } from "node:fs";
|
|
@@ -4238,6 +4437,7 @@ class ReviewWorker {
|
|
|
4238
4437
|
} catch {
|
|
4239
4438
|
diff = "(unable to retrieve diff)";
|
|
4240
4439
|
}
|
|
4440
|
+
const diffSummary = formatDiffSummary(diff);
|
|
4241
4441
|
const previewUrl = `http://localhost:${port}`;
|
|
4242
4442
|
const enriched = {
|
|
4243
4443
|
card,
|
|
@@ -4247,7 +4447,7 @@ class ReviewWorker {
|
|
|
4247
4447
|
mode: "review"
|
|
4248
4448
|
};
|
|
4249
4449
|
const systemPrompt = buildReviewSystemPrompt();
|
|
4250
|
-
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl,
|
|
4450
|
+
const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch);
|
|
4251
4451
|
try {
|
|
4252
4452
|
await this.client.recordPromptHistory({
|
|
4253
4453
|
cardId: card.id,
|
|
@@ -4424,6 +4624,7 @@ class ReviewWorker {
|
|
|
4424
4624
|
}
|
|
4425
4625
|
spawnClaude(prompt, systemPrompt, tracker, shortId) {
|
|
4426
4626
|
return new Promise((resolve3, reject) => {
|
|
4627
|
+
const leanSources = this.config.claude.leanSettingSources;
|
|
4427
4628
|
const args = [
|
|
4428
4629
|
"--output-format",
|
|
4429
4630
|
"stream-json",
|
|
@@ -4431,9 +4632,10 @@ class ReviewWorker {
|
|
|
4431
4632
|
"--model",
|
|
4432
4633
|
this.config.claude.reviewModel,
|
|
4433
4634
|
"--max-turns",
|
|
4434
|
-
String(this.config.claude.
|
|
4635
|
+
String(this.config.claude.reviewMaxTurns),
|
|
4435
4636
|
"--allowedTools",
|
|
4436
4637
|
"Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
|
|
4638
|
+
...leanSources ? ["--setting-sources", leanSources] : [],
|
|
4437
4639
|
...systemPrompt ? ["--append-system-prompt", systemPrompt] : [],
|
|
4438
4640
|
...this.config.claude.additionalArgs,
|
|
4439
4641
|
"--",
|
|
@@ -4579,6 +4781,7 @@ var TAG19 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEO
|
|
|
4579
4781
|
var init_review_worker = __esm(() => {
|
|
4580
4782
|
init_board_helpers();
|
|
4581
4783
|
init_completion();
|
|
4784
|
+
init_git_diff_stat();
|
|
4582
4785
|
init_log();
|
|
4583
4786
|
init_process_group();
|
|
4584
4787
|
init_progress_tracker();
|
|
@@ -4725,130 +4928,12 @@ var init_unblock = __esm(() => {
|
|
|
4725
4928
|
init_log();
|
|
4726
4929
|
});
|
|
4727
4930
|
|
|
4728
|
-
//
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
return
|
|
4733
|
-
}
|
|
4734
|
-
function authorLabel(c) {
|
|
4735
|
-
if (c.author_type === "agent")
|
|
4736
|
-
return "AI agent";
|
|
4737
|
-
const raw = c.author?.full_name || "teammate";
|
|
4738
|
-
return sanitizeHeaderField(raw);
|
|
4739
|
-
}
|
|
4740
|
-
function criticalIds(comments) {
|
|
4741
|
-
const keep = new Set;
|
|
4742
|
-
for (const c of comments) {
|
|
4743
|
-
if (c.comment_type === "decision")
|
|
4744
|
-
keep.add(c.id);
|
|
4745
|
-
if (c.supersedes_id) {
|
|
4746
|
-
keep.add(c.id);
|
|
4747
|
-
keep.add(c.supersedes_id);
|
|
4748
|
-
}
|
|
4749
|
-
if (c.confirms_id) {
|
|
4750
|
-
keep.add(c.id);
|
|
4751
|
-
keep.add(c.confirms_id);
|
|
4752
|
-
}
|
|
4753
|
-
}
|
|
4754
|
-
return keep;
|
|
4755
|
-
}
|
|
4756
|
-
function serializeCommentThread(comments, options = {}) {
|
|
4757
|
-
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
4758
|
-
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
4759
|
-
if (visible.length === 0)
|
|
4760
|
-
return "";
|
|
4761
|
-
const indexById = new Map;
|
|
4762
|
-
visible.forEach((c, i) => {
|
|
4763
|
-
indexById.set(c.id, i + 1);
|
|
4764
|
-
});
|
|
4765
|
-
let rendered = visible;
|
|
4766
|
-
let elidedCount = 0;
|
|
4767
|
-
if (maxComments && visible.length > maxComments) {
|
|
4768
|
-
const keep = criticalIds(visible);
|
|
4769
|
-
const recentThreshold = visible.length - maxComments;
|
|
4770
|
-
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
4771
|
-
elidedCount = visible.length - rendered.length;
|
|
4772
|
-
}
|
|
4773
|
-
const ref = (id) => {
|
|
4774
|
-
const n = indexById.get(id);
|
|
4775
|
-
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
4776
|
-
};
|
|
4777
|
-
const lines = [];
|
|
4778
|
-
if (elidedCount > 0) {
|
|
4779
|
-
lines.push({
|
|
4780
|
-
at: visible[0]?.created_at ?? "",
|
|
4781
|
-
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
4782
|
-
});
|
|
4783
|
-
}
|
|
4784
|
-
for (const c of rendered) {
|
|
4785
|
-
const tags = [];
|
|
4786
|
-
if (c.edited_at)
|
|
4787
|
-
tags.push("edited");
|
|
4788
|
-
if (c.supersedes_id)
|
|
4789
|
-
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
4790
|
-
if (c.confirms_id)
|
|
4791
|
-
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
4792
|
-
if (c.resolved_at)
|
|
4793
|
-
tags.push("resolved");
|
|
4794
|
-
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
4795
|
-
const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
|
|
4796
|
-
const fencedBody = c.body.trim().replaceAll("<", "<").replaceAll(">", ">");
|
|
4797
|
-
lines.push({
|
|
4798
|
-
at: c.created_at,
|
|
4799
|
-
text: `${header}
|
|
4800
|
-
<comment-body>
|
|
4801
|
-
${fencedBody}
|
|
4802
|
-
</comment-body>`
|
|
4803
|
-
});
|
|
4804
|
-
}
|
|
4805
|
-
for (const a of activity) {
|
|
4806
|
-
const actor = a.actor ? `${a.actor} ` : "";
|
|
4807
|
-
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
4808
|
-
}
|
|
4809
|
-
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
4810
|
-
const body = lines.map((l) => l.text).join(`
|
|
4811
|
-
|
|
4812
|
-
`);
|
|
4813
|
-
const instruction = includeInstructions ? `
|
|
4814
|
-
|
|
4815
|
-
${CONFLICT_INSTRUCTION}` : "";
|
|
4816
|
-
return `## ${heading} (oldest → newest)
|
|
4817
|
-
|
|
4818
|
-
${body}${instruction}`;
|
|
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 };
|
|
4819
4936
|
}
|
|
4820
|
-
var CONFLICT_INSTRUCTION;
|
|
4821
|
-
var init_commentSerializer = __esm(() => {
|
|
4822
|
-
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.";
|
|
4823
|
-
});
|
|
4824
|
-
|
|
4825
|
-
// ../harmony-shared/dist/constants.js
|
|
4826
|
-
var TIMINGS;
|
|
4827
|
-
var init_constants = __esm(() => {
|
|
4828
|
-
TIMINGS = {
|
|
4829
|
-
SEARCH_DEBOUNCE: 300,
|
|
4830
|
-
AUTOSAVE_DEBOUNCE: 1000,
|
|
4831
|
-
TOAST_DURATION: 3000,
|
|
4832
|
-
QUERY_STALE_TIME: 1000 * 60 * 5,
|
|
4833
|
-
QUERY_GC_TIME: 1000 * 60 * 60 * 24
|
|
4834
|
-
};
|
|
4835
|
-
});
|
|
4836
|
-
// ../harmony-shared/dist/logger.js
|
|
4837
|
-
var init_logger = () => {};
|
|
4838
|
-
// ../harmony-shared/dist/projectTemplates.js
|
|
4839
|
-
var init_projectTemplates = () => {};
|
|
4840
|
-
// ../harmony-shared/dist/types.js
|
|
4841
|
-
var init_types2 = () => {};
|
|
4842
|
-
|
|
4843
|
-
// ../harmony-shared/dist/index.js
|
|
4844
|
-
var init_dist = __esm(() => {
|
|
4845
|
-
init_cardLinks();
|
|
4846
|
-
init_commentSerializer();
|
|
4847
|
-
init_constants();
|
|
4848
|
-
init_logger();
|
|
4849
|
-
init_projectTemplates();
|
|
4850
|
-
init_types2();
|
|
4851
|
-
});
|
|
4852
4937
|
|
|
4853
4938
|
// src/prompt.ts
|
|
4854
4939
|
async function buildPrompt(enriched, branchName, worktreePath, client, workspaceId, projectId) {
|
|
@@ -5161,7 +5246,9 @@ class Worker {
|
|
|
5161
5246
|
this.timedOut = true;
|
|
5162
5247
|
this.cancel();
|
|
5163
5248
|
}, this.config.maxTimeout);
|
|
5164
|
-
await this.spawnClaude(prompt, card, subtasks
|
|
5249
|
+
await this.spawnClaude(prompt, card, subtasks, {
|
|
5250
|
+
model: this.selectImplementModel(card)
|
|
5251
|
+
});
|
|
5165
5252
|
if (this.aborted)
|
|
5166
5253
|
return;
|
|
5167
5254
|
if (this.timeoutTimer) {
|
|
@@ -5287,6 +5374,14 @@ class Worker {
|
|
|
5287
5374
|
this.onDone(this);
|
|
5288
5375
|
}
|
|
5289
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
|
+
}
|
|
5290
5385
|
async recordOutcome(cardId, outcome) {
|
|
5291
5386
|
try {
|
|
5292
5387
|
const cost = this.lastSessionStats?.cost;
|
|
@@ -6395,9 +6490,11 @@ function configRows(config, projectName, gitProvider, httpPort) {
|
|
|
6395
6490
|
const reviewEnabled = config.agent.review.enabled;
|
|
6396
6491
|
const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
|
|
6397
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}`;
|
|
6398
6495
|
rows.push({
|
|
6399
6496
|
label: "Model",
|
|
6400
|
-
value: `${
|
|
6497
|
+
value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
|
|
6401
6498
|
});
|
|
6402
6499
|
const tail = [];
|
|
6403
6500
|
if (gitProvider)
|
|
@@ -6668,11 +6765,23 @@ var exports_worktree_gc = {};
|
|
|
6668
6765
|
__export(exports_worktree_gc, {
|
|
6669
6766
|
runWorktreeGc: () => runWorktreeGc,
|
|
6670
6767
|
pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
|
|
6768
|
+
isTransientGitNetworkError: () => isTransientGitNetworkError,
|
|
6671
6769
|
WorktreeGc: () => WorktreeGc
|
|
6672
6770
|
});
|
|
6673
6771
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
6674
6772
|
import { readdirSync, statSync as statSync2 } from "node:fs";
|
|
6675
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
|
+
}
|
|
6676
6785
|
function runWorktreeGc(basePath, store, opts = {}) {
|
|
6677
6786
|
const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1000;
|
|
6678
6787
|
const now = (opts.now ?? Date.now)();
|
|
@@ -6760,13 +6869,16 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6760
6869
|
try {
|
|
6761
6870
|
execFileSync9("git", ["fetch", "--prune", "origin"], {
|
|
6762
6871
|
cwd: repoRoot,
|
|
6763
|
-
stdio: "pipe"
|
|
6872
|
+
stdio: "pipe",
|
|
6873
|
+
...GIT_NETWORK_EXEC
|
|
6764
6874
|
});
|
|
6765
6875
|
} catch (err) {
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
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 });
|
|
6770
6882
|
}
|
|
6771
6883
|
const refPattern = `refs/remotes/origin/${opts.prefix}*`;
|
|
6772
6884
|
let listing = "";
|
|
@@ -6783,7 +6895,9 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6783
6895
|
});
|
|
6784
6896
|
return result;
|
|
6785
6897
|
}
|
|
6786
|
-
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;
|
|
6787
6901
|
for (const line of listing.split(`
|
|
6788
6902
|
`)) {
|
|
6789
6903
|
const trimmed = line.trim();
|
|
@@ -6799,17 +6913,24 @@ function pruneFailedRemoteBranches(opts) {
|
|
|
6799
6913
|
result.skipped.push(ref);
|
|
6800
6914
|
continue;
|
|
6801
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
|
+
}
|
|
6802
6920
|
try {
|
|
6803
6921
|
execFileSync9("git", ["push", "origin", `:refs/heads/${ref}`], {
|
|
6804
6922
|
cwd: repoRoot,
|
|
6805
|
-
stdio: "pipe"
|
|
6923
|
+
stdio: "pipe",
|
|
6924
|
+
...GIT_NETWORK_EXEC
|
|
6806
6925
|
});
|
|
6807
6926
|
result.removed.push(ref);
|
|
6808
6927
|
} catch (err) {
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
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 });
|
|
6813
6934
|
}
|
|
6814
6935
|
}
|
|
6815
6936
|
if (result.removed.length > 0) {
|
|
@@ -6867,10 +6988,32 @@ function getRepoRoot2() {
|
|
|
6867
6988
|
return null;
|
|
6868
6989
|
}
|
|
6869
6990
|
}
|
|
6870
|
-
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;
|
|
6871
6992
|
var init_worktree_gc = __esm(() => {
|
|
6872
6993
|
init_log();
|
|
6873
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");
|
|
6874
7017
|
});
|
|
6875
7018
|
|
|
6876
7019
|
// src/index.ts
|