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