@gethmy/agent 1.10.1 → 1.10.3

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