@gethmy/agent 1.10.2 → 1.10.4

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 +349 -166
  3. package/dist/index.js +350 -167
  4. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -397,8 +397,17 @@ var init_types = __esm(() => {
397
397
  },
398
398
  claude: {
399
399
  model: "opus",
400
+ escalateModel: "claude-fable-5",
401
+ escalateAfterAttempts: 2,
402
+ tiers: {
403
+ simple: "claude-haiku-4-5",
404
+ advanced: "claude-sonnet-4-6",
405
+ research: "claude-opus-4-8"
406
+ },
400
407
  reviewModel: "sonnet",
401
- maxTurns: 200,
408
+ maxTurns: 80,
409
+ reviewMaxTurns: 60,
410
+ leanSettingSources: "local,user",
402
411
  additionalArgs: []
403
412
  },
404
413
  worktree: {
@@ -2007,6 +2016,54 @@ function parseNumstat(raw, maxFiles = MAX_CHANGED_FILES2) {
2007
2016
  }
2008
2017
  return { files, insertions, deletions };
2009
2018
  }
2019
+ function summarizeUnifiedDiff(diff) {
2020
+ const files = [];
2021
+ let current = null;
2022
+ let totalAdded = 0;
2023
+ let totalRemoved = 0;
2024
+ for (const line of diff.split(`
2025
+ `)) {
2026
+ if (line.startsWith("diff --git")) {
2027
+ const m = line.match(/ b\/(.+)$/);
2028
+ current = {
2029
+ path: m ? m[1] : line.slice("diff --git ".length),
2030
+ added: 0,
2031
+ removed: 0
2032
+ };
2033
+ files.push(current);
2034
+ continue;
2035
+ }
2036
+ if (!current)
2037
+ continue;
2038
+ if (line.startsWith("+++") || line.startsWith("---"))
2039
+ continue;
2040
+ if (line.startsWith("+")) {
2041
+ current.added++;
2042
+ totalAdded++;
2043
+ } else if (line.startsWith("-")) {
2044
+ current.removed++;
2045
+ totalRemoved++;
2046
+ }
2047
+ }
2048
+ return { files, totalAdded, totalRemoved };
2049
+ }
2050
+ function formatDiffSummary(diff, maxFiles = 100) {
2051
+ const trimmed = diff.trim();
2052
+ if (!trimmed || diff === "(unable to retrieve diff)") {
2053
+ return trimmed ? diff : "(no diff available)";
2054
+ }
2055
+ const { files, totalAdded, totalRemoved } = summarizeUnifiedDiff(diff);
2056
+ if (files.length === 0)
2057
+ return "(no file changes detected in diff)";
2058
+ const shown = files.slice(0, maxFiles);
2059
+ const lines = shown.map((f) => ` ${f.path} | +${f.added} -${f.removed}`);
2060
+ if (files.length > maxFiles) {
2061
+ lines.push(` ... and ${files.length - maxFiles} more file(s)`);
2062
+ }
2063
+ lines.push(` ${files.length} file(s) changed, ${totalAdded} insertion(s)(+), ${totalRemoved} deletion(s)(-)`);
2064
+ return lines.join(`
2065
+ `);
2066
+ }
2010
2067
  function captureDiffStat(worktreePath, baseBranch, maxFiles = MAX_CHANGED_FILES2) {
2011
2068
  try {
2012
2069
  const raw = execFileSync5("git", ["diff", "--numstat", `${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
@@ -2123,7 +2180,17 @@ async function runDeepReview(worktreePath, config, workerId) {
2123
2180
  "```"
2124
2181
  ].join(`
2125
2182
  `);
2126
- const output = execFileSync6("claude", ["--print", "--model", "sonnet", "--max-turns", "10", "--", reviewPrompt], {
2183
+ const leanSources = config.claude.leanSettingSources;
2184
+ const output = execFileSync6("claude", [
2185
+ "--print",
2186
+ "--model",
2187
+ "sonnet",
2188
+ "--max-turns",
2189
+ "10",
2190
+ ...leanSources ? ["--setting-sources", leanSources] : [],
2191
+ "--",
2192
+ reviewPrompt
2193
+ ], {
2127
2194
  cwd: worktreePath,
2128
2195
  encoding: "utf-8",
2129
2196
  timeout: config.verification.timeout,
@@ -2154,6 +2221,7 @@ function attemptAutoFix(worktreePath, config, errors) {
2154
2221
  "```"
2155
2222
  ].join(`
2156
2223
  `);
2224
+ const leanSources = config.claude.leanSettingSources;
2157
2225
  const args = [
2158
2226
  "--print",
2159
2227
  "--model",
@@ -2162,6 +2230,7 @@ function attemptAutoFix(worktreePath, config, errors) {
2162
2230
  "50",
2163
2231
  "--allowedTools",
2164
2232
  "Bash,Read,Write,Edit,Glob,Grep",
2233
+ ...leanSources ? ["--setting-sources", leanSources] : [],
2165
2234
  "--",
2166
2235
  fixPrompt
2167
2236
  ];
@@ -3368,7 +3437,133 @@ var init_review_completion = __esm(() => {
3368
3437
  init_worktree();
3369
3438
  });
3370
3439
 
3371
- // src/review-knowledge.ts
3440
+ // ../harmony-shared/dist/cardLinks.js
3441
+ var init_cardLinks = () => {};
3442
+ // ../harmony-shared/dist/classification.js
3443
+ function escalateTier(tier) {
3444
+ const i = MODEL_TIERS.indexOf(tier);
3445
+ return MODEL_TIERS[Math.min(i + 1, MODEL_TIERS.length - 1)];
3446
+ }
3447
+ function isModelTier(v) {
3448
+ return typeof v === "string" && MODEL_TIERS.includes(v);
3449
+ }
3450
+ var MODEL_TIERS;
3451
+ var init_classification = __esm(() => {
3452
+ MODEL_TIERS = ["simple", "advanced", "research"];
3453
+ });
3454
+
3455
+ // ../harmony-shared/dist/commentSerializer.js
3456
+ function sanitizeHeaderField(value) {
3457
+ return value.replace(/[\]\r\n|<>]/g, " ").trim() || "—";
3458
+ }
3459
+ function authorLabel(c) {
3460
+ if (c.author_type === "agent")
3461
+ return "AI agent";
3462
+ const raw = c.author?.full_name || "teammate";
3463
+ return sanitizeHeaderField(raw);
3464
+ }
3465
+ function criticalIds(comments) {
3466
+ const keep = new Set;
3467
+ for (const c of comments) {
3468
+ if (c.comment_type === "decision")
3469
+ keep.add(c.id);
3470
+ if (c.supersedes_id) {
3471
+ keep.add(c.id);
3472
+ keep.add(c.supersedes_id);
3473
+ }
3474
+ if (c.confirms_id) {
3475
+ keep.add(c.id);
3476
+ keep.add(c.confirms_id);
3477
+ }
3478
+ }
3479
+ return keep;
3480
+ }
3481
+ function serializeCommentThread(comments, options = {}) {
3482
+ const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
3483
+ const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
3484
+ if (visible.length === 0)
3485
+ return "";
3486
+ const indexById = new Map;
3487
+ visible.forEach((c, i) => {
3488
+ indexById.set(c.id, i + 1);
3489
+ });
3490
+ let rendered = visible;
3491
+ let elidedCount = 0;
3492
+ if (maxComments && visible.length > maxComments) {
3493
+ const keep = criticalIds(visible);
3494
+ const recentThreshold = visible.length - maxComments;
3495
+ rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
3496
+ elidedCount = visible.length - rendered.length;
3497
+ }
3498
+ const ref = (id) => {
3499
+ const n = indexById.get(id);
3500
+ return n ? `#${n}` : `#${id.slice(0, 8)}`;
3501
+ };
3502
+ const lines = [];
3503
+ if (elidedCount > 0) {
3504
+ lines.push({
3505
+ at: visible[0]?.created_at ?? "",
3506
+ text: `(${elidedCount} earlier comment(s) omitted for brevity)`
3507
+ });
3508
+ }
3509
+ for (const c of rendered) {
3510
+ const tags = [];
3511
+ if (c.edited_at)
3512
+ tags.push("edited");
3513
+ if (c.supersedes_id)
3514
+ tags.push(`supersedes ${ref(c.supersedes_id)}`);
3515
+ if (c.confirms_id)
3516
+ tags.push(`confirms ${ref(c.confirms_id)}`);
3517
+ if (c.resolved_at)
3518
+ tags.push("resolved");
3519
+ const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
3520
+ const header = `[${sanitizeHeaderField(ref(c.id))} | ${sanitizeHeaderField(c.author_type)} | ${authorLabel(c)} | ${sanitizeHeaderField(c.comment_type)} | ${sanitizeHeaderField(c.created_at)}${tagStr}]`;
3521
+ const fencedBody = c.body.trim().replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3522
+ lines.push({
3523
+ at: c.created_at,
3524
+ text: `${header}
3525
+ <comment-body>
3526
+ ${fencedBody}
3527
+ </comment-body>`
3528
+ });
3529
+ }
3530
+ for (const a of activity) {
3531
+ const actor = a.actor ? `${a.actor} ` : "";
3532
+ lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
3533
+ }
3534
+ lines.sort((a, b) => a.at.localeCompare(b.at));
3535
+ const body = lines.map((l) => l.text).join(`
3536
+
3537
+ `);
3538
+ const instruction = includeInstructions ? `
3539
+
3540
+ ${CONFLICT_INSTRUCTION}` : "";
3541
+ return `## ${heading} (oldest → newest)
3542
+
3543
+ ${body}${instruction}`;
3544
+ }
3545
+ var CONFLICT_INSTRUCTION;
3546
+ var init_commentSerializer = __esm(() => {
3547
+ 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.";
3548
+ });
3549
+
3550
+ // ../harmony-shared/dist/constants.js
3551
+ var TIMINGS;
3552
+ var init_constants = __esm(() => {
3553
+ TIMINGS = {
3554
+ SEARCH_DEBOUNCE: 300,
3555
+ AUTOSAVE_DEBOUNCE: 1000,
3556
+ TOAST_DURATION: 3000,
3557
+ QUERY_STALE_TIME: 1000 * 60 * 5,
3558
+ QUERY_GC_TIME: 1000 * 60 * 60 * 24
3559
+ };
3560
+ });
3561
+ // ../harmony-shared/dist/logger.js
3562
+ var init_logger = () => {};
3563
+ // ../harmony-shared/dist/projectTemplates.js
3564
+ var init_projectTemplates = () => {};
3565
+
3566
+ // ../harmony-shared/dist/reviewMethodology.js
3372
3567
  var REVIEW_SYSTEM_PROMPT = `You are a senior code reviewer. Follow this two-pass methodology strictly.
3373
3568
  Report findings; do NOT fix them. This is a read-only review.
3374
3569
 
@@ -3444,7 +3639,42 @@ For each page affected by the changes:
3444
3639
  - Use snapshot for navigation — client-side routes may not appear in link lists.
3445
3640
  - Check for stale state: navigate away and back — does data refresh correctly?
3446
3641
  - Test browser back/forward — does the app handle history correctly?
3447
- - Watch for hydration errors or layout shifts after dynamic content loads.`;
3642
+ - Watch for hydration errors or layout shifts after dynamic content loads.`, REVIEW_VERDICT_SCHEMA = `{
3643
+ "verdict": "approved" | "rejected",
3644
+ "summary": "Brief overall assessment",
3645
+ "scopeCheck": {
3646
+ "status": "clean" | "drift" | "missing",
3647
+ "notes": "Optional explanation of scope issues"
3648
+ },
3649
+ "findings": [
3650
+ {
3651
+ "severity": "critical" | "major" | "minor",
3652
+ "category": "sql-safety | race-condition | llm-trust | enum-completeness | visual | functional | ux | console | scope | other",
3653
+ "title": "Short title",
3654
+ "description": "Detailed description of the issue",
3655
+ "location": "file:line (if applicable)"
3656
+ }
3657
+ ]
3658
+ }`, REVIEW_DECISION_RULES = `- **rejected**: Any \`critical\` finding, unaddressed requirements, or 2+ \`major\` findings.
3659
+ - **approved**: No critical findings, at most 1 major finding with minor findings OK.`;
3660
+ // ../harmony-shared/dist/types.js
3661
+ var init_types2 = () => {};
3662
+
3663
+ // ../harmony-shared/dist/index.js
3664
+ var init_dist = __esm(() => {
3665
+ init_cardLinks();
3666
+ init_classification();
3667
+ init_commentSerializer();
3668
+ init_constants();
3669
+ init_logger();
3670
+ init_projectTemplates();
3671
+ init_types2();
3672
+ });
3673
+
3674
+ // src/review-knowledge.ts
3675
+ var init_review_knowledge = __esm(() => {
3676
+ init_dist();
3677
+ });
3448
3678
 
3449
3679
  // src/review-prompt.ts
3450
3680
  function buildReviewSystemPrompt() {
@@ -3455,15 +3685,13 @@ ${REVIEW_SYSTEM_PROMPT}
3455
3685
 
3456
3686
  ${QA_VISUAL_CHECKLIST}`;
3457
3687
  }
3458
- function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diff, baseBranch) {
3688
+ function buildReviewUserPrompt(enriched, branchName, worktreePath, previewUrl, diffSummary, baseBranch) {
3459
3689
  const { card, labels, subtasks } = enriched;
3460
3690
  const labelStr = labels.length > 0 ? labels.map((l) => l.name).join(", ") : "none";
3461
3691
  const subtaskStr = subtasks.length > 0 ? subtasks.map((s) => `- [${s.completed ? "x" : " "}] ${s.title}`).join(`
3462
3692
  `) : "No subtasks defined.";
3463
3693
  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;
3694
+ const diffRange = branchName ? `origin/${baseBranch}..HEAD` : "HEAD";
3467
3695
  const branchLine = branchName ? `**Branch**: ${branchName}` : `**Mode**: Local review (no branch — reviewing working tree changes)`;
3468
3696
  return `## Card: #${card.short_id} - ${card.title}
3469
3697
  **Labels**: ${labelStr}
@@ -3475,10 +3703,15 @@ ${description}
3475
3703
  ## Subtasks (Acceptance Criteria)
3476
3704
  ${subtaskStr}
3477
3705
 
3478
- ## Diff ${branchName ? `(origin/${baseBranch}..HEAD)` : "(local changes)"}
3479
- \`\`\`diff
3480
- ${truncatedDiff}
3706
+ ## Changed Files (git diff --stat ${diffRange})
3481
3707
  \`\`\`
3708
+ ${diffSummary}
3709
+ \`\`\`
3710
+
3711
+ The full diff is intentionally NOT inlined here. Inspect the changes yourself —
3712
+ you have Read, Grep, Glob, and read-only Bash:
3713
+ - Run \`git diff ${diffRange}\` (or \`git diff ${diffRange} -- <file>\`) for the full hunks.
3714
+ - Read / Grep the changed files above and trace new values through their consumers.
3482
3715
 
3483
3716
  ## Review Steps
3484
3717
 
@@ -3509,33 +3742,18 @@ Use the \`/browse\` skill to navigate to ${previewUrl} and apply the visual QA c
3509
3742
  After completing all steps, output EXACTLY one JSON block (and nothing else after it):
3510
3743
 
3511
3744
  \`\`\`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
- }
3745
+ ${REVIEW_VERDICT_SCHEMA}
3529
3746
  \`\`\`
3530
3747
 
3531
3748
  **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.
3749
+ ${REVIEW_DECISION_RULES}
3534
3750
 
3535
3751
  **Do NOT modify any code.** This is a read-only review.
3536
3752
  ${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
3753
  }
3538
- var init_review_prompt = () => {};
3754
+ var init_review_prompt = __esm(() => {
3755
+ init_review_knowledge();
3756
+ });
3539
3757
 
3540
3758
  // src/run-log.ts
3541
3759
  import { createWriteStream, mkdirSync } from "node:fs";
@@ -4239,6 +4457,7 @@ class ReviewWorker {
4239
4457
  } catch {
4240
4458
  diff = "(unable to retrieve diff)";
4241
4459
  }
4460
+ const diffSummary = formatDiffSummary(diff);
4242
4461
  const previewUrl = `http://localhost:${port}`;
4243
4462
  const enriched = {
4244
4463
  card,
@@ -4248,7 +4467,7 @@ class ReviewWorker {
4248
4467
  mode: "review"
4249
4468
  };
4250
4469
  const systemPrompt = buildReviewSystemPrompt();
4251
- const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diff, this.config.worktree.baseBranch);
4470
+ const userPrompt = buildReviewUserPrompt(enriched, this.branchName, cwd, previewUrl, diffSummary, this.config.worktree.baseBranch);
4252
4471
  try {
4253
4472
  await this.client.recordPromptHistory({
4254
4473
  cardId: card.id,
@@ -4425,6 +4644,7 @@ class ReviewWorker {
4425
4644
  }
4426
4645
  spawnClaude(prompt, systemPrompt, tracker, shortId) {
4427
4646
  return new Promise((resolve3, reject) => {
4647
+ const leanSources = this.config.claude.leanSettingSources;
4428
4648
  const args = [
4429
4649
  "--output-format",
4430
4650
  "stream-json",
@@ -4432,9 +4652,10 @@ class ReviewWorker {
4432
4652
  "--model",
4433
4653
  this.config.claude.reviewModel,
4434
4654
  "--max-turns",
4435
- String(this.config.claude.maxTurns),
4655
+ String(this.config.claude.reviewMaxTurns),
4436
4656
  "--allowedTools",
4437
4657
  "Bash(readonly),Read,Glob,Grep,Agent,mcp__harmony__*",
4658
+ ...leanSources ? ["--setting-sources", leanSources] : [],
4438
4659
  ...systemPrompt ? ["--append-system-prompt", systemPrompt] : [],
4439
4660
  ...this.config.claude.additionalArgs,
4440
4661
  "--",
@@ -4580,6 +4801,7 @@ var TAG19 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEO
4580
4801
  var init_review_worker = __esm(() => {
4581
4802
  init_board_helpers();
4582
4803
  init_completion();
4804
+ init_git_diff_stat();
4583
4805
  init_log();
4584
4806
  init_process_group();
4585
4807
  init_progress_tracker();
@@ -4726,129 +4948,31 @@ var init_unblock = __esm(() => {
4726
4948
  init_log();
4727
4949
  });
4728
4950
 
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
- }
4951
+ // src/model-tier.ts
4952
+ function chooseImplementModel(claude, card, attempts) {
4953
+ if (card.model_override) {
4954
+ return { model: card.model_override, escalated: false, source: "override" };
4754
4955
  }
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;
4956
+ if (isModelTier(card.model_tier)) {
4957
+ const retry = attempts >= claude.escalateAfterAttempts;
4958
+ const tier = retry ? escalateTier(card.model_tier) : card.model_tier;
4959
+ const mapped = claude.tiers?.[tier];
4960
+ return {
4961
+ model: mapped && mapped.length > 0 ? mapped : claude.model,
4962
+ escalated: retry,
4963
+ source: "tier"
4964
+ };
4773
4965
  }
4774
- const ref = (id) => {
4775
- const n = indexById.get(id);
4776
- return n ? `#${n}` : `#${id.slice(0, 8)}`;
4966
+ const highPriority = card.priority === "high" || card.priority === "urgent";
4967
+ const escalated = highPriority || attempts >= claude.escalateAfterAttempts;
4968
+ return {
4969
+ model: escalated ? claude.escalateModel : claude.model,
4970
+ escalated,
4971
+ source: "policy"
4777
4972
  };
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}`;
4820
4973
  }
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();
4974
+ var init_model_tier = __esm(() => {
4975
+ init_dist();
4852
4976
  });
4853
4977
 
4854
4978
  // src/prompt.ts
@@ -5162,7 +5286,9 @@ class Worker {
5162
5286
  this.timedOut = true;
5163
5287
  this.cancel();
5164
5288
  }, this.config.maxTimeout);
5165
- await this.spawnClaude(prompt, card, subtasks);
5289
+ await this.spawnClaude(prompt, card, subtasks, {
5290
+ model: this.selectImplementModel(card)
5291
+ });
5166
5292
  if (this.aborted)
5167
5293
  return;
5168
5294
  if (this.timeoutTimer) {
@@ -5288,6 +5414,14 @@ class Worker {
5288
5414
  this.onDone(this);
5289
5415
  }
5290
5416
  }
5417
+ selectImplementModel(card) {
5418
+ const attempts = this.stateStore.getCard(card.id)?.attempts ?? 1;
5419
+ const { model, escalated, source } = chooseImplementModel(this.config.claude, card, attempts);
5420
+ if (source !== "policy" || escalated) {
5421
+ log.info(this.tag, `Implement model "${model}" (source=${source}, escalated=${escalated}, attempts=${attempts}, priority=${card.priority ?? "none"}, tier=${card.model_tier ?? "none"})`);
5422
+ }
5423
+ return model;
5424
+ }
5291
5425
  async recordOutcome(cardId, outcome) {
5292
5426
  try {
5293
5427
  const cost = this.lastSessionStats?.cost;
@@ -5620,6 +5754,7 @@ var init_worker = __esm(() => {
5620
5754
  init_completion();
5621
5755
  init_error_classifier();
5622
5756
  init_log();
5757
+ init_model_tier();
5623
5758
  init_plan_phase();
5624
5759
  init_process_group();
5625
5760
  init_progress_tracker();
@@ -6396,9 +6531,11 @@ function configRows(config, projectName, gitProvider, httpPort) {
6396
6531
  const reviewEnabled = config.agent.review.enabled;
6397
6532
  const poolDesc = reviewEnabled ? `Pool ${config.agent.poolSize} impl + ${config.agent.review.poolSize} review` : `Pool ${config.agent.poolSize} impl`;
6398
6533
  const flowDesc = reviewEnabled ? `Pickup ${config.agent.pickupColumns[0]} → ${config.agent.completion.moveToColumn} → ${config.agent.review.moveToColumn}` : `Pickup ${config.agent.pickupColumns.join(", ")}`;
6534
+ const { model, escalateModel, reviewModel } = config.agent.claude;
6535
+ const modelDesc = model === escalateModel ? model : `${model} → ${escalateModel} on retry/high · review ${reviewModel}`;
6399
6536
  rows.push({
6400
6537
  label: "Model",
6401
- value: `${config.agent.claude.model} · ${poolDesc} · ${flowDesc}`
6538
+ value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
6402
6539
  });
6403
6540
  const tail = [];
6404
6541
  if (gitProvider)
@@ -6669,11 +6806,23 @@ var exports_worktree_gc = {};
6669
6806
  __export(exports_worktree_gc, {
6670
6807
  runWorktreeGc: () => runWorktreeGc,
6671
6808
  pruneFailedRemoteBranches: () => pruneFailedRemoteBranches,
6809
+ isTransientGitNetworkError: () => isTransientGitNetworkError,
6672
6810
  WorktreeGc: () => WorktreeGc
6673
6811
  });
6674
6812
  import { execFileSync as execFileSync9 } from "node:child_process";
6675
6813
  import { readdirSync, statSync as statSync2 } from "node:fs";
6676
6814
  import { resolve as resolve3 } from "node:path";
6815
+ function isTransientGitNetworkError(message) {
6816
+ return TRANSIENT_GIT_NETWORK_ERROR.test(message);
6817
+ }
6818
+ function gitErrorDetail(err) {
6819
+ if (err && typeof err === "object" && "stderr" in err) {
6820
+ const stderr = String(err.stderr ?? "").trim();
6821
+ if (stderr)
6822
+ return stderr;
6823
+ }
6824
+ return err instanceof Error ? err.message : String(err);
6825
+ }
6677
6826
  function runWorktreeGc(basePath, store, opts = {}) {
6678
6827
  const minAgeMs = opts.minAgeMs ?? 60 * 60 * 1000;
6679
6828
  const now = (opts.now ?? Date.now)();
@@ -6761,13 +6910,16 @@ function pruneFailedRemoteBranches(opts) {
6761
6910
  try {
6762
6911
  execFileSync9("git", ["fetch", "--prune", "origin"], {
6763
6912
  cwd: repoRoot,
6764
- stdio: "pipe"
6913
+ stdio: "pipe",
6914
+ ...GIT_NETWORK_EXEC
6765
6915
  });
6766
6916
  } catch (err) {
6767
- result.errors.push({
6768
- ref: "fetch",
6769
- error: err instanceof Error ? err.message : String(err)
6770
- });
6917
+ const detail = gitErrorDetail(err);
6918
+ if (isTransientGitNetworkError(detail)) {
6919
+ log.debug(TAG30, `Remote branch GC skipped remote unreachable: ${detail}`);
6920
+ return result;
6921
+ }
6922
+ result.errors.push({ ref: "fetch", error: detail });
6771
6923
  }
6772
6924
  const refPattern = `refs/remotes/origin/${opts.prefix}*`;
6773
6925
  let listing = "";
@@ -6784,7 +6936,9 @@ function pruneFailedRemoteBranches(opts) {
6784
6936
  });
6785
6937
  return result;
6786
6938
  }
6787
- const cutoffSecs = (opts.now ?? Date.now)() / 1000 - opts.retentionDays * 24 * 60 * 60;
6939
+ const clock = opts.now ?? Date.now;
6940
+ const cutoffSecs = clock() / 1000 - opts.retentionDays * 24 * 60 * 60;
6941
+ const sweepDeadline = clock() + GIT_PRUNE_SWEEP_BUDGET_MS;
6788
6942
  for (const line of listing.split(`
6789
6943
  `)) {
6790
6944
  const trimmed = line.trim();
@@ -6800,17 +6954,24 @@ function pruneFailedRemoteBranches(opts) {
6800
6954
  result.skipped.push(ref);
6801
6955
  continue;
6802
6956
  }
6957
+ if (clock() > sweepDeadline) {
6958
+ log.debug(TAG30, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
6959
+ break;
6960
+ }
6803
6961
  try {
6804
6962
  execFileSync9("git", ["push", "origin", `:refs/heads/${ref}`], {
6805
6963
  cwd: repoRoot,
6806
- stdio: "pipe"
6964
+ stdio: "pipe",
6965
+ ...GIT_NETWORK_EXEC
6807
6966
  });
6808
6967
  result.removed.push(ref);
6809
6968
  } catch (err) {
6810
- result.errors.push({
6811
- ref,
6812
- error: err instanceof Error ? err.message : String(err)
6813
- });
6969
+ const detail = gitErrorDetail(err);
6970
+ if (isTransientGitNetworkError(detail)) {
6971
+ log.debug(TAG30, `Remote branch GC interrupted remote unreachable: ${detail}`);
6972
+ break;
6973
+ }
6974
+ result.errors.push({ ref, error: detail });
6814
6975
  }
6815
6976
  }
6816
6977
  if (result.removed.length > 0) {
@@ -6868,10 +7029,32 @@ function getRepoRoot2() {
6868
7029
  return null;
6869
7030
  }
6870
7031
  }
6871
- var TAG30 = "worktree-gc";
7032
+ 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
7033
  var init_worktree_gc = __esm(() => {
6873
7034
  init_log();
6874
7035
  init_worktree();
7036
+ GIT_NETWORK_EXEC = {
7037
+ timeout: GIT_NETWORK_TIMEOUT_MS,
7038
+ killSignal: "SIGKILL",
7039
+ env: {
7040
+ ...process.env,
7041
+ GIT_SSH_COMMAND: `${process.env.GIT_SSH_COMMAND ?? "ssh"} -o ConnectTimeout=${GIT_SSH_CONNECT_TIMEOUT_SECS} -o BatchMode=yes`,
7042
+ GIT_TERMINAL_PROMPT: "0"
7043
+ }
7044
+ };
7045
+ TRANSIENT_GIT_NETWORK_ERROR = new RegExp([
7046
+ "timed out",
7047
+ "connection refused",
7048
+ "connection reset",
7049
+ "could not resolve host",
7050
+ "temporary failure in name resolution",
7051
+ "ssh: connect to host",
7052
+ "kex_exchange_identification",
7053
+ "network is unreachable",
7054
+ "etimedout",
7055
+ "enotfound",
7056
+ "enetunreach"
7057
+ ].join("|"), "i");
6875
7058
  });
6876
7059
 
6877
7060
  // src/index.ts