@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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +313 -170
  3. package/dist/index.js +313 -170
  4. 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: 200,
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 output = execFileSync6("claude", ["--print", "--model", "sonnet", "--max-turns", "10", "--", reviewPrompt], {
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
- // src/review-knowledge.ts
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("<", "&lt;").replaceAll(">", "&gt;");
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, diff, baseBranch) {
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 truncatedDiff = diff.length > 80000 ? `${diff.slice(0, 80000)}
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
- ## Diff ${branchName ? `(origin/${baseBranch}..HEAD)` : "(local changes)"}
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
- - **rejected**: Any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
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, diff, this.config.worktree.baseBranch);
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.maxTurns),
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
- // ../harmony-shared/dist/cardLinks.js
4730
- var init_cardLinks = () => {};
4731
- // ../harmony-shared/dist/commentSerializer.js
4732
- function sanitizeHeaderField(value) {
4733
- return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
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("<", "&lt;").replaceAll(">", "&gt;");
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: `${config.agent.claude.model} · ${poolDesc} · ${flowDesc}`
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
- result.errors.push({
6768
- ref: "fetch",
6769
- error: err instanceof Error ? err.message : String(err)
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 cutoffSecs = (opts.now ?? Date.now)() / 1000 - opts.retentionDays * 24 * 60 * 60;
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
- result.errors.push({
6811
- ref,
6812
- error: err instanceof Error ? err.message : String(err)
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: 200,
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 output = execFileSync6("claude", ["--print", "--model", "sonnet", "--max-turns", "10", "--", reviewPrompt], {
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
- // src/review-knowledge.ts
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("<", "&lt;").replaceAll(">", "&gt;");
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, diff, baseBranch) {
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 truncatedDiff = diff.length > 80000 ? `${diff.slice(0, 80000)}
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
- ## Diff ${branchName ? `(origin/${baseBranch}..HEAD)` : "(local changes)"}
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
- - **rejected**: Any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
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, diff, this.config.worktree.baseBranch);
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.maxTurns),
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
- // ../harmony-shared/dist/cardLinks.js
4729
- var init_cardLinks = () => {};
4730
- // ../harmony-shared/dist/commentSerializer.js
4731
- function sanitizeHeaderField(value) {
4732
- return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
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("<", "&lt;").replaceAll(">", "&gt;");
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: `${config.agent.claude.model} · ${poolDesc} · ${flowDesc}`
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
- result.errors.push({
6767
- ref: "fetch",
6768
- error: err instanceof Error ? err.message : String(err)
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 cutoffSecs = (opts.now ?? Date.now)() / 1000 - opts.retentionDays * 24 * 60 * 60;
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
- result.errors.push({
6810
- ref,
6811
- error: err instanceof Error ? err.message : String(err)
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/agent",
3
- "version": "1.10.2",
3
+ "version": "1.10.3",
4
4
  "description": "Push-based agent daemon for Harmony — watches board assignments and spawns Claude CLI workers",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",