@gethmy/agent 1.10.8 → 1.11.0

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 (3) hide show
  1. package/dist/cli.js +1115 -332
  2. package/dist/index.js +1115 -332
  3. package/package.json +4 -3
package/dist/index.js CHANGED
@@ -384,11 +384,12 @@ var DEFAULT_AGENT_CONFIG, IN_PROGRESS_COLUMN = "In Progress", NEED_REVIEW_LABEL
384
384
  var init_types = __esm(() => {
385
385
  init_plan_phase();
386
386
  DEFAULT_AGENT_CONFIG = {
387
- poolSize: 3,
387
+ poolSize: 6,
388
388
  maxTimeout: 1800000,
389
389
  pickupColumns: ["To Do"],
390
390
  priorityLabels: { urgent: 100, critical: 90, bug: 50 },
391
391
  columnBoost: true,
392
+ runner: "cli",
392
393
  completion: {
393
394
  createPR: false,
394
395
  moveToColumn: "Review",
@@ -423,13 +424,14 @@ var init_types = __esm(() => {
423
424
  autoFix: true,
424
425
  maxFixAttempts: 1,
425
426
  deepReview: false,
427
+ revertGuard: true,
426
428
  devServerBasePort: 4200,
427
429
  timeout: 120000,
428
430
  failColumn: "To Do"
429
431
  },
430
432
  review: {
431
433
  enabled: true,
432
- poolSize: 2,
434
+ poolSize: 3,
433
435
  pickupColumns: ["Review"],
434
436
  moveToColumn: "Done",
435
437
  failColumn: "To Do",
@@ -561,6 +563,9 @@ function loadDaemonConfig() {
561
563
  ...agentOverrides.planning ?? {}
562
564
  }
563
565
  };
566
+ if (agent.runner !== "cli" && agent.runner !== "sdk") {
567
+ agent.runner = "cli";
568
+ }
564
569
  return {
565
570
  apiKey,
566
571
  apiUrl,
@@ -1208,6 +1213,23 @@ var init_pm = __esm(() => {
1208
1213
  import { execFileSync as execFileSync3, execSync as execSync2 } from "node:child_process";
1209
1214
  import { existsSync as existsSync2, rmSync } from "node:fs";
1210
1215
  import { resolve } from "node:path";
1216
+ function fetchBaseBranch(repoRoot, baseBranch, attempts = 3, fetchImpl = (root, branch) => execFileSync3("git", ["fetch", "origin", branch], {
1217
+ cwd: root,
1218
+ stdio: "pipe"
1219
+ })) {
1220
+ let lastErr;
1221
+ for (let attempt = 1;attempt <= attempts; attempt++) {
1222
+ try {
1223
+ fetchImpl(repoRoot, baseBranch);
1224
+ return;
1225
+ } catch (err) {
1226
+ lastErr = err;
1227
+ log.warn(TAG5, `fetch origin ${baseBranch} failed (attempt ${attempt}/${attempts})`);
1228
+ }
1229
+ }
1230
+ const detail = lastErr instanceof Error ? lastErr.message : String(lastErr);
1231
+ throw new WorktreeBaseError(`Could not fetch origin/${baseBranch} after ${attempts} attempts — ` + `refusing to build on a stale base. ${detail}`);
1232
+ }
1211
1233
  function createWorktree(basePath, baseBranch, branchName) {
1212
1234
  const repoRoot = execFileSync3("git", ["rev-parse", "--show-toplevel"], {
1213
1235
  encoding: "utf-8"
@@ -1223,14 +1245,7 @@ function createWorktree(basePath, baseBranch, branchName) {
1223
1245
  stdio: "pipe"
1224
1246
  });
1225
1247
  } catch {}
1226
- try {
1227
- execFileSync3("git", ["fetch", "origin", baseBranch], {
1228
- cwd: repoRoot,
1229
- stdio: "pipe"
1230
- });
1231
- } catch {
1232
- log.warn(TAG5, "Failed to fetch latest — continuing with local state");
1233
- }
1248
+ fetchBaseBranch(repoRoot, baseBranch);
1234
1249
  log.info(TAG5, `Creating worktree: ${worktreeDir} (branch: ${branchName})`);
1235
1250
  try {
1236
1251
  execFileSync3("git", [
@@ -1318,10 +1333,16 @@ function makeBranchName(shortId, title, prefix = "agent-attempts/") {
1318
1333
  const slug = title.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
1319
1334
  return `${prefix}${shortId}-${slug || "task"}`;
1320
1335
  }
1321
- var TAG5 = "worktree";
1336
+ var TAG5 = "worktree", WorktreeBaseError;
1322
1337
  var init_worktree = __esm(() => {
1323
1338
  init_log();
1324
1339
  init_pm();
1340
+ WorktreeBaseError = class WorktreeBaseError extends Error {
1341
+ constructor(message) {
1342
+ super(message);
1343
+ this.name = "WorktreeBaseError";
1344
+ }
1345
+ };
1325
1346
  });
1326
1347
 
1327
1348
  // src/review-worktree.ts
@@ -2199,41 +2220,91 @@ var init_project_type = __esm(() => {
2199
2220
  _cache = new Map;
2200
2221
  });
2201
2222
 
2223
+ // src/revert-guard.ts
2224
+ import { execFileSync as execFileSync7 } from "node:child_process";
2225
+ function isTestFile(path) {
2226
+ return TEST_FILE.test(path);
2227
+ }
2228
+ function filterTestFiles(paths) {
2229
+ return paths.filter(isTestFile);
2230
+ }
2231
+ function refetchBase(worktreePath, baseBranch) {
2232
+ try {
2233
+ execFileSync7("git", ["fetch", "origin", baseBranch], {
2234
+ cwd: worktreePath,
2235
+ stdio: "pipe"
2236
+ });
2237
+ } catch {
2238
+ log.warn(TAG12, "Failed to re-fetch base for revert guard — using last fetch");
2239
+ }
2240
+ }
2241
+ function listDeletedFilesAgainstBase(worktreePath, baseBranch) {
2242
+ try {
2243
+ const out = execFileSync7("git", ["diff", "--diff-filter=D", "--name-only", `origin/${baseBranch}...HEAD`], { cwd: worktreePath, encoding: "utf-8" });
2244
+ return out.split(`
2245
+ `).map((l) => l.trim()).filter((l) => l.length > 0);
2246
+ } catch (err) {
2247
+ log.warn(TAG12, `Failed to list deleted files: ${err instanceof Error ? err.message : err}`);
2248
+ return [];
2249
+ }
2250
+ }
2251
+ function findDeletedTestFiles(worktreePath, baseBranch) {
2252
+ refetchBase(worktreePath, baseBranch);
2253
+ return filterTestFiles(listDeletedFilesAgainstBase(worktreePath, baseBranch));
2254
+ }
2255
+ var TAG12 = "revert-guard", TEST_FILE;
2256
+ var init_revert_guard = __esm(() => {
2257
+ init_log();
2258
+ TEST_FILE = /(?:^|\/)__tests__\/|\.(?:test|spec)\.[cm]?[jt]sx?$/;
2259
+ });
2260
+
2202
2261
  // src/verification.ts
2203
- import { execFileSync as execFileSync7, spawn } from "node:child_process";
2262
+ import { execFileSync as execFileSync8, spawn } from "node:child_process";
2204
2263
  async function runVerification(worktreePath, config, workerId) {
2205
2264
  const result = {
2206
2265
  passed: true,
2207
2266
  buildErrors: [],
2208
2267
  lintWarnings: [],
2209
- reviewFindings: []
2268
+ reviewFindings: [],
2269
+ revertWarnings: []
2210
2270
  };
2271
+ if (config.verification.revertGuard) {
2272
+ log.info(TAG13, `[worker:${workerId}] Checking for reverted merged work...`);
2273
+ const deletedTests = findDeletedTestFiles(worktreePath, config.worktree.baseBranch);
2274
+ if (deletedTests.length > 0) {
2275
+ result.revertWarnings = deletedTests.map((f) => `Branch deletes test file '${f}' relative to current ${config.worktree.baseBranch} — ` + "likely an accidental revert of already-merged work. Restore the test or rebase on current main.");
2276
+ log.warn(TAG13, `[worker:${workerId}] Revert guard tripped: ${deletedTests.length} deleted test file(s)`);
2277
+ result.passed = false;
2278
+ } else {
2279
+ log.info(TAG13, `[worker:${workerId}] Revert guard passed`);
2280
+ }
2281
+ }
2211
2282
  if (config.verification.build) {
2212
- log.info(TAG12, `[worker:${workerId}] Running build...`);
2283
+ log.info(TAG13, `[worker:${workerId}] Running build...`);
2213
2284
  result.buildErrors = runBuild(worktreePath, config.verification.timeout);
2214
2285
  if (result.buildErrors.length > 0) {
2215
- log.warn(TAG12, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2286
+ log.warn(TAG13, `[worker:${workerId}] Build failed with ${result.buildErrors.length} error(s)`);
2216
2287
  result.passed = false;
2217
2288
  } else {
2218
- log.info(TAG12, `[worker:${workerId}] Build passed`);
2289
+ log.info(TAG13, `[worker:${workerId}] Build passed`);
2219
2290
  }
2220
2291
  }
2221
2292
  if (config.verification.lint) {
2222
- log.info(TAG12, `[worker:${workerId}] Running lint...`);
2293
+ log.info(TAG13, `[worker:${workerId}] Running lint...`);
2223
2294
  result.lintWarnings = runLint(worktreePath, config.verification.timeout);
2224
2295
  if (result.lintWarnings.length > 0) {
2225
- log.warn(TAG12, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2296
+ log.warn(TAG13, `[worker:${workerId}] Lint found ${result.lintWarnings.length} issue(s)`);
2226
2297
  } else {
2227
- log.info(TAG12, `[worker:${workerId}] Lint passed`);
2298
+ log.info(TAG13, `[worker:${workerId}] Lint passed`);
2228
2299
  }
2229
2300
  }
2230
2301
  if (config.verification.deepReview) {
2231
- log.info(TAG12, `[worker:${workerId}] Running deep review...`);
2302
+ log.info(TAG13, `[worker:${workerId}] Running deep review...`);
2232
2303
  result.reviewFindings = await runDeepReview(worktreePath, config, workerId);
2233
2304
  if (result.reviewFindings.length > 0) {
2234
- log.warn(TAG12, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2305
+ log.warn(TAG13, `[worker:${workerId}] Deep review found ${result.reviewFindings.length} finding(s)`);
2235
2306
  } else {
2236
- log.info(TAG12, `[worker:${workerId}] Deep review passed`);
2307
+ log.info(TAG13, `[worker:${workerId}] Deep review passed`);
2237
2308
  }
2238
2309
  }
2239
2310
  return result;
@@ -2241,11 +2312,11 @@ async function runVerification(worktreePath, config, workerId) {
2241
2312
  function runBuild(worktreePath, timeout) {
2242
2313
  const command = buildCommand(worktreePath);
2243
2314
  if (!command) {
2244
- log.warn(TAG12, `No known build toolchain for ${worktreePath} — skipping build`);
2315
+ log.warn(TAG13, `No known build toolchain for ${worktreePath} — skipping build`);
2245
2316
  return [];
2246
2317
  }
2247
2318
  try {
2248
- execFileSync7(command.cmd, command.args, {
2319
+ execFileSync8(command.cmd, command.args, {
2249
2320
  cwd: worktreePath,
2250
2321
  timeout,
2251
2322
  stdio: "pipe"
@@ -2258,11 +2329,11 @@ function runBuild(worktreePath, timeout) {
2258
2329
  function runLint(worktreePath, timeout) {
2259
2330
  const command = lintCommand(worktreePath);
2260
2331
  if (!command) {
2261
- log.info(TAG12, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
2332
+ log.info(TAG13, `No lint step for detected toolchain in ${worktreePath} — skipping lint`);
2262
2333
  return [];
2263
2334
  }
2264
2335
  try {
2265
- execFileSync7(command.cmd, command.args, {
2336
+ execFileSync8(command.cmd, command.args, {
2266
2337
  cwd: worktreePath,
2267
2338
  timeout,
2268
2339
  stdio: "pipe"
@@ -2274,7 +2345,7 @@ function runLint(worktreePath, timeout) {
2274
2345
  }
2275
2346
  async function runDeepReview(worktreePath, config, workerId) {
2276
2347
  if (!supportsDevServer(worktreePath)) {
2277
- log.info(TAG12, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
2348
+ log.info(TAG13, `[worker:${workerId}] Detected non-web toolchain — skipping deep review`);
2278
2349
  return [];
2279
2350
  }
2280
2351
  const port = config.verification.devServerBasePort + workerId;
@@ -2289,12 +2360,12 @@ async function runDeepReview(worktreePath, config, workerId) {
2289
2360
  await waitForDevServer(devServer, 30000);
2290
2361
  await probeDevServer(port);
2291
2362
  } catch (err) {
2292
- log.error(TAG12, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
2363
+ log.error(TAG13, `Dev server did not become ready: ${err instanceof Error ? err.message : err}`);
2293
2364
  return [];
2294
2365
  }
2295
2366
  let diff = "";
2296
2367
  try {
2297
- diff = execFileSync7("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
2368
+ diff = execFileSync8("git", ["diff", `origin/${config.worktree.baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8", timeout: 30000 });
2298
2369
  } catch {
2299
2370
  diff = "(unable to retrieve diff)";
2300
2371
  }
@@ -2311,7 +2382,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2311
2382
  ].join(`
2312
2383
  `);
2313
2384
  const leanSources = config.claude.leanSettingSources;
2314
- const output = execFileSync7("claude", [
2385
+ const output = execFileSync8("claude", [
2315
2386
  "--print",
2316
2387
  "--model",
2317
2388
  "sonnet",
@@ -2328,7 +2399,7 @@ async function runDeepReview(worktreePath, config, workerId) {
2328
2399
  });
2329
2400
  return parseReviewFindings(output);
2330
2401
  } catch (err) {
2331
- log.error(TAG12, `Deep review failed: ${err instanceof Error ? err.message : err}`);
2402
+ log.error(TAG13, `Deep review failed: ${err instanceof Error ? err.message : err}`);
2332
2403
  return [];
2333
2404
  } finally {
2334
2405
  if (devServer && !devServer.killed) {
@@ -2364,8 +2435,8 @@ function attemptAutoFix(worktreePath, config, errors) {
2364
2435
  "--",
2365
2436
  fixPrompt
2366
2437
  ];
2367
- log.info(TAG12, "Spawning Claude for auto-fix...");
2368
- execFileSync7("claude", args, {
2438
+ log.info(TAG13, "Spawning Claude for auto-fix...");
2439
+ execFileSync8("claude", args, {
2369
2440
  cwd: worktreePath,
2370
2441
  timeout: config.verification.timeout,
2371
2442
  stdio: "pipe"
@@ -2378,6 +2449,9 @@ async function reportFindings(client, cardId, result, recovery) {
2378
2449
  const url = recovery.branchUrl ? ` (${recovery.branchUrl})` : "";
2379
2450
  items.push(`Recovery: \`${cmd}\`${url}`);
2380
2451
  }
2452
+ for (const warn of result.revertWarnings) {
2453
+ items.push(`Revert: ${warn}`);
2454
+ }
2381
2455
  for (const err of result.buildErrors) {
2382
2456
  items.push(`Build: ${err}`);
2383
2457
  }
@@ -2395,7 +2469,7 @@ async function reportFindings(client, cardId, result, recovery) {
2395
2469
  try {
2396
2470
  await client.createSubtask(cardId, title);
2397
2471
  } catch (err) {
2398
- log.error(TAG12, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
2472
+ log.error(TAG13, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
2399
2473
  }
2400
2474
  }));
2401
2475
  if (overflow > 0) {
@@ -2403,7 +2477,7 @@ async function reportFindings(client, cardId, result, recovery) {
2403
2477
  await client.createSubtask(cardId, `...and ${overflow} more issues`);
2404
2478
  } catch {}
2405
2479
  }
2406
- log.info(TAG12, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
2480
+ log.info(TAG13, `Reported ${Math.min(items.length, maxSubtasks)} finding(s) as subtasks on card ${cardId}`);
2407
2481
  }
2408
2482
  function parseErrorOutput(err) {
2409
2483
  const stderr = err?.stderr?.toString() ?? "";
@@ -2487,11 +2561,12 @@ async function probeDevServer(port, timeoutMs = 5000) {
2487
2561
  clearTimeout(timer);
2488
2562
  }
2489
2563
  }
2490
- var TAG12 = "verification", DevServerReadinessError;
2564
+ var TAG13 = "verification", DevServerReadinessError;
2491
2565
  var init_verification = __esm(() => {
2492
2566
  init_log();
2493
2567
  init_pm();
2494
2568
  init_project_type();
2569
+ init_revert_guard();
2495
2570
  DevServerReadinessError = class DevServerReadinessError extends Error {
2496
2571
  constructor(message) {
2497
2572
  super(message);
@@ -2501,7 +2576,7 @@ var init_verification = __esm(() => {
2501
2576
  });
2502
2577
 
2503
2578
  // src/completion.ts
2504
- import { execFileSync as execFileSync8 } from "node:child_process";
2579
+ import { execFileSync as execFileSync9 } from "node:child_process";
2505
2580
  function formatTokenCount(tokens) {
2506
2581
  if (tokens >= 1e6)
2507
2582
  return `${(tokens / 1e6).toFixed(1)}M`;
@@ -2509,6 +2584,13 @@ function formatTokenCount(tokens) {
2509
2584
  return `${(tokens / 1000).toFixed(1)}k`;
2510
2585
  return String(tokens);
2511
2586
  }
2587
+ function describeNoCommitFailure(numTurns, maxTurns) {
2588
+ const maxTurnsExhausted = maxTurns > 0 && numTurns >= maxTurns;
2589
+ return {
2590
+ maxTurnsExhausted,
2591
+ failureSummary: maxTurnsExhausted ? `Agent exhausted its ${maxTurns}-turn budget without committing any changes` : "Agent finished without making any changes to commit"
2592
+ };
2593
+ }
2512
2594
  function buildTokenPayload(stats) {
2513
2595
  if (!stats?.cost)
2514
2596
  return {};
@@ -2527,26 +2609,30 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2527
2609
  passed: true,
2528
2610
  buildErrors: [],
2529
2611
  lintWarnings: [],
2530
- reviewFindings: []
2612
+ reviewFindings: [],
2613
+ revertWarnings: []
2531
2614
  };
2532
2615
  const hasCommits = checkHasCommits(worktreePath, config.worktree.baseBranch);
2533
2616
  if (!hasCommits) {
2534
- log.warn(TAG13, `No commits on branch ${branchName} skipping completion`);
2617
+ const { maxTurnsExhausted, failureSummary } = describeNoCommitFailure(sessionStats?.cost?.numTurns ?? 0, config.claude.maxTurns);
2618
+ log.warn(TAG14, `No commits on branch ${branchName} — ${failureSummary}; counting as a failed attempt`);
2619
+ await moveCardToColumn(client, card, config.pickupColumns[0] ?? "To Do");
2535
2620
  await client.endAgentSession(card.id, {
2536
- status: "completed",
2537
- progressPercent: 100,
2621
+ status: "failed",
2622
+ failureReason: maxTurnsExhausted ? "timeout" : "other",
2623
+ failureSummary,
2538
2624
  ...buildTokenPayload(sessionStats)
2539
2625
  });
2540
2626
  cleanupWorktree(worktreePath, branchName);
2541
- return true;
2627
+ return false;
2542
2628
  }
2543
- log.info(TAG13, `Pushing branch ${branchName} (pre-verify)...`);
2629
+ log.info(TAG14, `Pushing branch ${branchName} (pre-verify)...`);
2544
2630
  let lastPushedSha = null;
2545
2631
  try {
2546
2632
  pushBranch(branchName, worktreePath);
2547
2633
  lastPushedSha = readHeadSha(worktreePath);
2548
2634
  } catch (err) {
2549
- log.error(TAG13, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2635
+ log.error(TAG14, `pre-verify push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2550
2636
  }
2551
2637
  const recoveryUrl = lastPushedSha ? getBranchWebUrl(branchName, worktreePath) : null;
2552
2638
  if (config.verification.enabled) {
@@ -2561,7 +2647,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2561
2647
  let autoFixAttempts = 0;
2562
2648
  if (!result.passed && config.verification.autoFix) {
2563
2649
  for (let attempt = 0;attempt < config.verification.maxFixAttempts; attempt++) {
2564
- log.info(TAG13, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
2650
+ log.info(TAG14, `Auto-fix attempt ${attempt + 1}/${config.verification.maxFixAttempts}`);
2565
2651
  await client.updateAgentProgress(card.id, {
2566
2652
  agentIdentifier: agentIdentifier(workerId),
2567
2653
  agentName: AGENT_NAME,
@@ -2574,14 +2660,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2574
2660
  result = await runVerification(worktreePath, config, workerId);
2575
2661
  autoFixAttempts = attempt + 1;
2576
2662
  if (result.passed) {
2577
- log.info(TAG13, `Auto-fix succeeded on attempt ${attempt + 1}`);
2663
+ log.info(TAG14, `Auto-fix succeeded on attempt ${attempt + 1}`);
2578
2664
  const sha = readHeadSha(worktreePath);
2579
2665
  if (sha && sha !== lastPushedSha) {
2580
2666
  try {
2581
2667
  pushBranch(branchName, worktreePath);
2582
2668
  lastPushedSha = sha;
2583
2669
  } catch (err) {
2584
- log.warn(TAG13, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2670
+ log.warn(TAG14, `post-fix push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2585
2671
  }
2586
2672
  }
2587
2673
  break;
@@ -2590,14 +2676,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2590
2676
  }
2591
2677
  verificationResult = result;
2592
2678
  if (!result.passed) {
2593
- log.warn(TAG13, `Verification failed for #${card.short_id} — reporting findings`);
2679
+ log.warn(TAG14, `Verification failed for #${card.short_id} — reporting findings`);
2594
2680
  const failSha = readHeadSha(worktreePath);
2595
2681
  if (failSha && failSha !== lastPushedSha) {
2596
2682
  try {
2597
2683
  pushBranch(branchName, worktreePath);
2598
2684
  lastPushedSha = failSha;
2599
2685
  } catch (err) {
2600
- log.warn(TAG13, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2686
+ log.warn(TAG14, `post-fail push failed for ${branchName}: ${err instanceof Error ? err.message : err}`);
2601
2687
  }
2602
2688
  }
2603
2689
  const failureSummary = buildVerificationFailureSummary(result, autoFixAttempts);
@@ -2608,7 +2694,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2608
2694
  recoveryBranch: branchName
2609
2695
  });
2610
2696
  } catch (err) {
2611
- log.debug(TAG13, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
2697
+ log.debug(TAG14, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
2612
2698
  }
2613
2699
  await reportFindings(client, card.id, result, lastPushedSha ? { branchName, branchUrl: recoveryUrl } : null);
2614
2700
  await moveCardToColumn(client, card, config.verification.failColumn);
@@ -2622,7 +2708,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2622
2708
  cleanupWorktree(worktreePath, branchName);
2623
2709
  return false;
2624
2710
  }
2625
- log.info(TAG13, `Verification passed for #${card.short_id}`);
2711
+ log.info(TAG14, `Verification passed for #${card.short_id}`);
2626
2712
  }
2627
2713
  let prUrl = null;
2628
2714
  if (config.completion.createPR) {
@@ -2635,7 +2721,7 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2635
2721
  try {
2636
2722
  await onMovedToCompletion(card);
2637
2723
  } catch (err) {
2638
- log.warn(TAG13, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2724
+ log.warn(TAG14, `successor promotion failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
2639
2725
  }
2640
2726
  }
2641
2727
  }
@@ -2669,11 +2755,14 @@ async function runCompletion(client, card, branchName, worktreePath, config, wor
2669
2755
  });
2670
2756
  }
2671
2757
  cleanupWorktree(worktreePath, branchName);
2672
- log.info(TAG13, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
2758
+ log.info(TAG14, `Completion done for #${card.short_id}${prUrl ? ` — PR: ${prUrl}` : ""}`);
2673
2759
  return true;
2674
2760
  }
2675
2761
  function buildVerificationFailureSummary(result, autoFixAttempts) {
2676
2762
  const counts = [];
2763
+ if (result.revertWarnings.length > 0) {
2764
+ counts.push(`${result.revertWarnings.length} reverted test(s)`);
2765
+ }
2677
2766
  if (result.buildErrors.length > 0) {
2678
2767
  counts.push(`${result.buildErrors.length} build error(s)`);
2679
2768
  }
@@ -2689,7 +2778,7 @@ function buildVerificationFailureSummary(result, autoFixAttempts) {
2689
2778
  }
2690
2779
  function readHeadSha(worktreePath) {
2691
2780
  try {
2692
- return execFileSync8("git", ["rev-parse", "HEAD"], {
2781
+ return execFileSync9("git", ["rev-parse", "HEAD"], {
2693
2782
  cwd: worktreePath,
2694
2783
  encoding: "utf-8"
2695
2784
  }).trim();
@@ -2699,7 +2788,7 @@ function readHeadSha(worktreePath) {
2699
2788
  }
2700
2789
  function checkHasCommits(worktreePath, baseBranch) {
2701
2790
  try {
2702
- const count = execFileSync8("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
2791
+ const count = execFileSync9("git", ["rev-list", "--count", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
2703
2792
  return parseInt(count, 10) > 0;
2704
2793
  } catch {
2705
2794
  return false;
@@ -2708,7 +2797,7 @@ function checkHasCommits(worktreePath, baseBranch) {
2708
2797
  async function postSummary(client, card, branchName, worktreePath, prUrl, baseBranch, sessionStats) {
2709
2798
  let commitLog = "";
2710
2799
  try {
2711
- commitLog = execFileSync8("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
2800
+ commitLog = execFileSync9("git", ["log", "--oneline", `origin/${baseBranch}..HEAD`], { cwd: worktreePath, encoding: "utf-8" }).trim();
2712
2801
  } catch {}
2713
2802
  const SUMMARY_MARKER = `---
2714
2803
  **Agent completed**`;
@@ -2753,12 +2842,12 @@ ${commitLog}
2753
2842
  description: baseDesc + parts.join(`
2754
2843
  `)
2755
2844
  });
2756
- log.info(TAG13, `Posted completion summary to #${card.short_id}`);
2845
+ log.info(TAG14, `Posted completion summary to #${card.short_id}`);
2757
2846
  } catch (err) {
2758
- log.error(TAG13, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
2847
+ log.error(TAG14, `Failed to post summary: ${err instanceof Error ? err.message : err}`);
2759
2848
  }
2760
2849
  }
2761
- var TAG13 = "completion";
2850
+ var TAG14 = "completion";
2762
2851
  var init_completion = __esm(() => {
2763
2852
  init_board_helpers();
2764
2853
  init_episode_writer();
@@ -2798,7 +2887,21 @@ function signalGroup(proc, signal) {
2798
2887
  } catch (err) {
2799
2888
  const code = err.code;
2800
2889
  if (code !== "ESRCH") {
2801
- log.warn(TAG14, `signal ${signal} to pgid ${proc.pid} failed: ${err instanceof Error ? err.message : err}`);
2890
+ log.warn(TAG15, `signal ${signal} to pgid ${proc.pid} failed: ${err instanceof Error ? err.message : err}`);
2891
+ }
2892
+ }
2893
+ }
2894
+ function reapGroup(pgid) {
2895
+ if (!pgid || pgid <= 1 || pgid === process.pid)
2896
+ return;
2897
+ if (process.platform === "win32")
2898
+ return;
2899
+ try {
2900
+ process.kill(-pgid, "SIGKILL");
2901
+ } catch (err) {
2902
+ const code = err.code;
2903
+ if (code !== "ESRCH") {
2904
+ log.warn(TAG15, `reapGroup(${pgid}) failed: ${err instanceof Error ? err.message : err}`);
2802
2905
  }
2803
2906
  }
2804
2907
  }
@@ -2823,7 +2926,7 @@ async function terminateGroup(proc, opts) {
2823
2926
  return;
2824
2927
  signalGroup(proc, "SIGKILL");
2825
2928
  }
2826
- var TAG14 = "pgroup";
2929
+ var TAG15 = "pgroup";
2827
2930
  var init_process_group = __esm(() => {
2828
2931
  init_log();
2829
2932
  });
@@ -2853,8 +2956,8 @@ class ProgressTracker {
2853
2956
  filesEdited = new Set;
2854
2957
  filesRead = new Set;
2855
2958
  lastCost = null;
2856
- logBuffer = [];
2857
- sessionId = null;
2959
+ runEventSink = null;
2960
+ lastEmittedProgress = -1;
2858
2961
  lastAssistantText = "";
2859
2962
  assistantTextBlocks = [];
2860
2963
  constructor(client, cardId, workerId, subtasks, initialPhase = "exploring") {
@@ -2867,32 +2970,15 @@ class ProgressTracker {
2867
2970
  this.phase = initialPhase;
2868
2971
  this.progress = PHASES[initialPhase].min;
2869
2972
  }
2870
- setSessionId(id) {
2871
- this.sessionId = id;
2973
+ setRunEventSink(sink) {
2974
+ this.runEventSink = sink;
2872
2975
  }
2873
2976
  attach(parser) {
2874
2977
  parser.on("tool_start", (name, input) => {
2875
2978
  this.onToolStart(name, input);
2876
- const desc = this.describeToolAction(name, input);
2877
- if (desc) {
2878
- this.pushLogEntry({
2879
- phase: this.phase,
2880
- eventType: "tool_start",
2881
- toolName: name,
2882
- description: desc,
2883
- metadata: this.extractToolMetadata(name, input)
2884
- });
2885
- }
2886
2979
  });
2887
2980
  parser.on("tool_end", (name, _id, content) => {
2888
2981
  this.onToolEnd(name, content);
2889
- this.pushLogEntry({
2890
- phase: this.phase,
2891
- eventType: "tool_end",
2892
- toolName: name,
2893
- description: `Completed: ${name}`,
2894
- metadata: {}
2895
- });
2896
2982
  });
2897
2983
  parser.on("text", (content) => {
2898
2984
  this.onText(content);
@@ -2902,6 +2988,37 @@ class ProgressTracker {
2902
2988
  });
2903
2989
  this.startHeartbeat();
2904
2990
  }
2991
+ ingest(draft) {
2992
+ switch (draft.kind) {
2993
+ case "run_started":
2994
+ this.startHeartbeat();
2995
+ break;
2996
+ case "tool_started":
2997
+ this.onToolStart(draft.payload.toolName, draft.payload.input);
2998
+ break;
2999
+ case "tool_ended":
3000
+ this.onToolEnd(draft.payload.toolName, draft.payload.output);
3001
+ break;
3002
+ case "assistant_text":
3003
+ this.onText(draft.payload.text);
3004
+ break;
3005
+ case "cost_updated": {
3006
+ const p = draft.payload;
3007
+ this.lastCost = {
3008
+ totalCostUsd: p.totalCostUsd,
3009
+ totalInputTokens: p.inputTokens,
3010
+ totalOutputTokens: p.outputTokens,
3011
+ totalCacheCreationInputTokens: p.cacheCreationInputTokens,
3012
+ totalCacheReadInputTokens: p.cacheReadInputTokens,
3013
+ durationMs: p.durationMs ?? 0,
3014
+ durationApiMs: 0,
3015
+ numTurns: p.numTurns,
3016
+ modelName: p.modelName
3017
+ };
3018
+ break;
3019
+ }
3020
+ }
3021
+ }
2905
3022
  stop() {
2906
3023
  this.stopped = true;
2907
3024
  if (this.pendingUpdate) {
@@ -2926,7 +3043,7 @@ class ProgressTracker {
2926
3043
  }
2927
3044
  onToolStart(name, input) {
2928
3045
  this.toolCallCount++;
2929
- log.debug(TAG15, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
3046
+ log.debug(TAG16, `Tool: ${name} (count: ${this.toolCallCount}, phase: ${this.phase})`);
2930
3047
  const filePath = this.extractString(input, "file_path");
2931
3048
  if (filePath) {
2932
3049
  if (EDIT_TOOLS.has(name)) {
@@ -2997,17 +3114,12 @@ class ProgressTracker {
2997
3114
  transitionTo(newPhase) {
2998
3115
  if (PHASE_ORDER[newPhase] <= PHASE_ORDER[this.phase])
2999
3116
  return;
3000
- log.info(TAG15, `Phase: ${this.phase} → ${newPhase}`);
3117
+ log.info(TAG16, `Phase: ${this.phase} → ${newPhase}`);
3118
+ const previousPhase = this.phase;
3119
+ this.runEventSink?.recordPhaseChanged(newPhase, previousPhase);
3001
3120
  this.phase = newPhase;
3002
3121
  this.progress = Math.max(this.progress, PHASES[newPhase].min);
3003
3122
  this.lastAction = "";
3004
- this.pushLogEntry({
3005
- phase: newPhase,
3006
- eventType: "phase_change",
3007
- toolName: null,
3008
- description: `Entering ${newPhase} phase`,
3009
- metadata: {}
3010
- });
3011
3123
  this.scheduleUpdate(PHASES[newPhase].label);
3012
3124
  }
3013
3125
  incrementProgress() {
@@ -3096,7 +3208,7 @@ class ProgressTracker {
3096
3208
  }
3097
3209
  sendUpdate(currentTask) {
3098
3210
  this.lastUpdateAt = Date.now();
3099
- log.debug(TAG15, `Progress: ${this.progress}% — ${currentTask}`);
3211
+ log.debug(TAG16, `Progress: ${this.progress}% — ${currentTask}`);
3100
3212
  this.client.updateAgentProgress(this.cardId, {
3101
3213
  agentIdentifier: agentIdentifier(this.workerId),
3102
3214
  agentName: AGENT_NAME,
@@ -3113,9 +3225,17 @@ class ProgressTracker {
3113
3225
  modelName: this.lastCost?.modelName,
3114
3226
  numTurns: this.lastCost?.numTurns ?? 0
3115
3227
  }).catch((err) => {
3116
- log.warn(TAG15, `Failed to send progress update: ${err}`);
3228
+ log.warn(TAG16, `Failed to send progress update: ${err}`);
3117
3229
  });
3118
- this.flushActivityLog();
3230
+ if (this.runEventSink && this.progress !== this.lastEmittedProgress) {
3231
+ this.lastEmittedProgress = this.progress;
3232
+ this.runEventSink.recordProgress({
3233
+ progressPercent: this.progress,
3234
+ currentTask: truncate(currentTask, MAX_TASK_LENGTH),
3235
+ phase: this.phase,
3236
+ filesChanged: this.filesEdited.size
3237
+ });
3238
+ }
3119
3239
  }
3120
3240
  startHeartbeat() {
3121
3241
  if (this.heartbeatTimer) {
@@ -3129,55 +3249,6 @@ class ProgressTracker {
3129
3249
  }
3130
3250
  }, HEARTBEAT_MS);
3131
3251
  }
3132
- flushFinal() {
3133
- this.flushActivityLog();
3134
- }
3135
- pushLogEntry(entry) {
3136
- this.logBuffer.push({
3137
- ...entry,
3138
- createdAt: new Date().toISOString()
3139
- });
3140
- if (this.logBuffer.length > MAX_LOG_BUFFER) {
3141
- this.logBuffer.shift();
3142
- }
3143
- }
3144
- flushActivityLog() {
3145
- if (!this.sessionId || this.logBuffer.length === 0)
3146
- return;
3147
- const raw = [...this.logBuffer];
3148
- this.logBuffer = [];
3149
- this.client.flushActivityLog(this.cardId, {
3150
- sessionId: this.sessionId,
3151
- entries: raw.map((e) => ({
3152
- ...e,
3153
- phase: e.phase ?? undefined,
3154
- toolName: e.toolName ?? undefined
3155
- }))
3156
- }).catch((err) => {
3157
- log.warn(TAG15, `Failed to flush activity log: ${err}`);
3158
- this.logBuffer.unshift(...raw);
3159
- if (this.logBuffer.length > MAX_LOG_BUFFER) {
3160
- this.logBuffer.length = MAX_LOG_BUFFER;
3161
- }
3162
- });
3163
- }
3164
- extractToolMetadata(_name, input) {
3165
- const meta = {};
3166
- const fp = this.extractString(input, "file_path");
3167
- if (fp)
3168
- meta.file_path = fp;
3169
- const cmd = this.extractString(input, "command");
3170
- if (cmd)
3171
- meta.command = cmd.split(`
3172
- `)[0].slice(0, 200);
3173
- const pattern = this.extractString(input, "pattern");
3174
- if (pattern)
3175
- meta.pattern = pattern;
3176
- const desc = this.extractString(input, "description");
3177
- if (desc)
3178
- meta.description = desc;
3179
- return meta;
3180
- }
3181
3252
  extractString(input, key) {
3182
3253
  if (typeof input === "object" && input !== null && key in input) {
3183
3254
  return String(input[key]);
@@ -3185,7 +3256,7 @@ class ProgressTracker {
3185
3256
  return null;
3186
3257
  }
3187
3258
  }
3188
- var TAG15 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_LOG_BUFFER = 500, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
3259
+ var TAG16 = "progress-tracker", THROTTLE_MS = 5000, HEARTBEAT_MS = 60000, MAX_TASK_LENGTH = 120, MAX_TEXT_BLOCKS = 40, SENTENCE_SPLIT, ACTION_PREFIX, GIT_COMMIT_RE, BUILD_CMD_RE, PHASES, PHASE_ORDER, EDIT_TOOLS, FILE_TOOL_VERBS;
3189
3260
  var init_progress_tracker = __esm(() => {
3190
3261
  init_log();
3191
3262
  init_types();
@@ -3221,6 +3292,41 @@ var init_progress_tracker = __esm(() => {
3221
3292
 
3222
3293
  // src/review-completion.ts
3223
3294
  import { readFileSync as readFileSync2, statSync } from "node:fs";
3295
+ function clampSubtaskTitle(title) {
3296
+ return title.length > MAX_SUBTASK_TITLE ? `${title.slice(0, MAX_SUBTASK_TITLE - 3)}...` : title;
3297
+ }
3298
+ function renderFindingBlock(f) {
3299
+ const locationLine = f.location ? `
3300
+ Location: ${f.location}` : "";
3301
+ return `**[${f.severity}] ${f.title}**
3302
+ ${f.description}${locationLine}`;
3303
+ }
3304
+ function buildFindingComments(findings) {
3305
+ const header = `**Review findings — ${findings.length} blocking issue(s) to resolve.**`;
3306
+ const sep = `
3307
+
3308
+ `;
3309
+ const bodies = [];
3310
+ let current = header;
3311
+ for (const f of findings) {
3312
+ let block = renderFindingBlock(f);
3313
+ const maxBlock = COMMENT_BODY_BUDGET - header.length - sep.length;
3314
+ if (block.length > maxBlock) {
3315
+ const suffix = `
3316
+ …[truncated]`;
3317
+ block = `${block.slice(0, Math.max(0, maxBlock - suffix.length))}${suffix}`;
3318
+ }
3319
+ if (current.length + sep.length + block.length > COMMENT_BODY_BUDGET) {
3320
+ bodies.push(current);
3321
+ current = `${header}${sep}${block}`;
3322
+ } else {
3323
+ current = `${current}${sep}${block}`;
3324
+ }
3325
+ }
3326
+ if (current.length > header.length)
3327
+ bodies.push(current);
3328
+ return bodies;
3329
+ }
3224
3330
  function tailRunLog(path, bytes = RUN_LOG_TAIL_BYTES) {
3225
3331
  try {
3226
3332
  const size = statSync(path).size;
@@ -3260,7 +3366,7 @@ function parseReviewOutput(stdout) {
3260
3366
  try {
3261
3367
  const parsed = JSON.parse(raw);
3262
3368
  if (parsed && typeof parsed === "object" && "verdict" in parsed) {
3263
- log.debug(TAG16, "Parsed review output from fenced JSON block");
3369
+ log.debug(TAG17, "Parsed review output from fenced JSON block");
3264
3370
  return extractResult(parsed);
3265
3371
  }
3266
3372
  } catch {}
@@ -3286,21 +3392,21 @@ function parseReviewOutput(stdout) {
3286
3392
  try {
3287
3393
  const parsed = JSON.parse(candidates[i]);
3288
3394
  if (parsed && typeof parsed === "object" && "verdict" in parsed) {
3289
- log.debug(TAG16, "Parsed review output from raw JSON object");
3395
+ log.debug(TAG17, "Parsed review output from raw JSON object");
3290
3396
  return extractResult(parsed);
3291
3397
  }
3292
3398
  } catch {}
3293
3399
  }
3294
3400
  const verdictMatch = stdout.match(/"verdict"\s*:\s*"(approved|rejected)"/i);
3295
3401
  if (verdictMatch) {
3296
- log.warn(TAG16, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
3402
+ log.warn(TAG17, `Parsed verdict via regex fallback — findings lost (${verdictMatch[1]})`);
3297
3403
  return {
3298
3404
  verdict: verdictMatch[1].toLowerCase(),
3299
3405
  summary: "Parsed via regex fallback — original JSON was malformed. Check run log.",
3300
3406
  findings: []
3301
3407
  };
3302
3408
  }
3303
- log.warn(TAG16, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
3409
+ log.warn(TAG17, "Failed to parse review JSON output — returning error verdict (card stays in Review)");
3304
3410
  return {
3305
3411
  verdict: "error",
3306
3412
  summary: stdout.slice(0, 500),
@@ -3333,7 +3439,7 @@ async function postReviewComment(client, card, commentType, body) {
3333
3439
  try {
3334
3440
  await client.addComment(card.id, body, { commentType });
3335
3441
  } catch (err) {
3336
- log.error(TAG16, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3442
+ log.error(TAG17, `Failed to post review comment to #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3337
3443
  }
3338
3444
  }
3339
3445
  async function runReviewCompletion(client, card, result, config, worktreePath, branchName, sessionStats, runLogPath, workspaceId, agentSessionId, stateStore) {
@@ -3347,11 +3453,11 @@ async function runReviewCompletion(client, card, result, config, worktreePath, b
3347
3453
  const currentCycle = getReviewCycle(freshDesc) + 1;
3348
3454
  const maxCycles = config.review.maxReviewCycles;
3349
3455
  if (result.verdict === "error") {
3350
- log.warn(TAG16, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
3456
+ log.warn(TAG17, `#${card.short_id} review output unparseable — labelling "${NEED_REVIEW_LABEL}" for manual inspection`);
3351
3457
  try {
3352
3458
  await addLabelByName(client, card, NEED_REVIEW_LABEL, NEED_REVIEW_LABEL_COLOR);
3353
3459
  } catch (err) {
3354
- log.warn(TAG16, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
3460
+ log.warn(TAG17, `Failed to add "${NEED_REVIEW_LABEL}" label: ${err instanceof Error ? err.message : err}`);
3355
3461
  }
3356
3462
  if (config.review.postFindings) {
3357
3463
  const rawTail = runLogPath ? tailRunLog(runLogPath) : null;
@@ -3394,7 +3500,7 @@ ${runLogTail}
3394
3500
  renameRemoteBranch(branchName, newRef, worktreePath);
3395
3501
  approvedBranch = newRef;
3396
3502
  } catch (err) {
3397
- log.warn(TAG16, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
3503
+ log.warn(TAG17, `Branch rename failed (continuing on ${branchName}): ${err instanceof Error ? err.message : err}`);
3398
3504
  }
3399
3505
  }
3400
3506
  if (config.review.createPR && approvedBranch) {
@@ -3403,6 +3509,21 @@ ${runLogTail}
3403
3509
  }
3404
3510
  }
3405
3511
  await addLabelByName(client, card, config.review.approvedLabel, config.review.approvedLabelColor);
3512
+ if (prUrl) {
3513
+ try {
3514
+ const { card: latest } = await client.getCard(card.id);
3515
+ const desc = latest.description || "";
3516
+ if (!extractPrUrl(desc)) {
3517
+ const separator = desc ? `
3518
+ ` : "";
3519
+ await client.updateCard(card.id, {
3520
+ description: `${desc}${separator}PR: ${prUrl}`
3521
+ });
3522
+ }
3523
+ } catch (err) {
3524
+ log.warn(TAG17, `Failed to persist PR URL to #${card.short_id} description: ${err instanceof Error ? err.message : err}`);
3525
+ }
3526
+ }
3406
3527
  if (config.review.postFindings) {
3407
3528
  const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
3408
3529
  const body = [
@@ -3421,14 +3542,14 @@ ${runLogTail}
3421
3542
  progressPercent: 100,
3422
3543
  ...buildTokenPayload(sessionStats)
3423
3544
  });
3424
- log.info(TAG16, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
3545
+ log.info(TAG17, `#${card.short_id} approved${prUrl ? ` — PR: ${prUrl}` : ""} — labeled "${config.review.approvedLabel}"`);
3425
3546
  } else {
3426
3547
  const criticalFindings = result.findings.filter((f) => f.severity === "critical").slice(0, MAX_FINDINGS);
3427
3548
  const majorFindings = result.findings.filter((f) => f.severity === "major").slice(0, MAX_FINDINGS);
3428
3549
  const linkedFindings = [...criticalFindings, ...majorFindings];
3429
3550
  const minorFindings = result.findings.filter((f) => f.severity === "minor").slice(0, MAX_FINDINGS);
3430
3551
  if (currentCycle >= maxCycles) {
3431
- log.warn(TAG16, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
3552
+ log.warn(TAG17, `#${card.short_id} reached max review cycles (${maxCycles}), moving to Done with note`);
3432
3553
  await moveCardToColumn(client, card, config.review.moveToColumn);
3433
3554
  const body = [
3434
3555
  "**Review — needs human review.**",
@@ -3466,28 +3587,21 @@ ${runLogTail}
3466
3587
  if (config.review.postFindings) {
3467
3588
  await Promise.all(linkedFindings.map(async (finding) => {
3468
3589
  try {
3469
- const locationLine = finding.location ? `
3470
- **Location:** ${finding.location}` : "";
3471
- const newCard = await client.createCard(card.project_id, {
3472
- title: `[Review: ${finding.severity}] ${finding.title}`,
3473
- description: `Found during review of #${card.short_id} (${finding.severity}):
3474
-
3475
- ${finding.description}${locationLine}`
3476
- });
3477
- const newCardId = newCard?.card?.id;
3478
- if (newCardId) {
3479
- await client.addLinkToCard(card.id, newCardId, "relates_to");
3480
- }
3590
+ await client.createSubtask(card.id, clampSubtaskTitle(`[${finding.severity}] ${finding.title}`));
3481
3591
  } catch (err) {
3482
- log.error(TAG16, `Failed to create finding card: ${err instanceof Error ? err.message : err}`);
3592
+ log.error(TAG17, `Failed to create finding subtask: ${err instanceof Error ? err.message : err}`);
3483
3593
  }
3484
3594
  }));
3595
+ if (linkedFindings.length > 0) {
3596
+ for (const body2 of buildFindingComments(linkedFindings)) {
3597
+ await postReviewComment(client, card, "finding", body2);
3598
+ }
3599
+ }
3485
3600
  await Promise.all(minorFindings.map(async (finding) => {
3486
3601
  try {
3487
- const title = finding.title.length > 120 ? `${finding.title.slice(0, 117)}...` : finding.title;
3488
- await client.createSubtask(card.id, title);
3602
+ await client.createSubtask(card.id, clampSubtaskTitle(finding.title));
3489
3603
  } catch (err) {
3490
- log.error(TAG16, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3604
+ log.error(TAG17, `Failed to create subtask: ${err instanceof Error ? err.message : err}`);
3491
3605
  }
3492
3606
  }));
3493
3607
  const baseDesc = stripReviewSummary(freshDesc);
@@ -3495,7 +3609,7 @@ ${finding.description}${locationLine}`
3495
3609
  try {
3496
3610
  await client.updateCard(card.id, { description: updatedDesc });
3497
3611
  } catch (err) {
3498
- log.error(TAG16, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
3612
+ log.error(TAG17, `Failed to update review cycle marker: ${err instanceof Error ? err.message : err}`);
3499
3613
  }
3500
3614
  const scopeLine = result.scopeCheck ? `Scope: ${result.scopeCheck.status}${result.scopeCheck.notes ? ` — ${result.scopeCheck.notes}` : ""}` : "";
3501
3615
  const body = [
@@ -3511,9 +3625,9 @@ ${finding.description}${locationLine}`
3511
3625
  if (config.planning.enabled && card.plan_id) {
3512
3626
  try {
3513
3627
  await client.updateCard(card.id, { needsPlanRefresh: true });
3514
- log.info(TAG16, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
3628
+ log.info(TAG17, `#${card.short_id} flagged needs_plan_refresh after rejected review`);
3515
3629
  } catch (err) {
3516
- log.warn(TAG16, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3630
+ log.warn(TAG17, `Failed to flag needs_plan_refresh for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
3517
3631
  }
3518
3632
  }
3519
3633
  await moveCardToColumn(client, card, config.review.failColumn);
@@ -3527,10 +3641,10 @@ ${finding.description}${locationLine}`
3527
3641
  recoveryBranch
3528
3642
  });
3529
3643
  } catch (err) {
3530
- log.debug(TAG16, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
3644
+ log.debug(TAG17, `recordFailureSummary failed: ${err instanceof Error ? err.message : err}`);
3531
3645
  }
3532
3646
  if (recoveryBranch) {
3533
- log.info(TAG16, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
3647
+ log.info(TAG17, `#${card.short_id} recovery branch ${recoveryBranch}${recoveryUrl ? ` (${recoveryUrl})` : ""}`);
3534
3648
  }
3535
3649
  await client.endAgentSession(card.id, {
3536
3650
  status: "failed",
@@ -3539,7 +3653,7 @@ ${finding.description}${locationLine}`
3539
3653
  recoveryBranch,
3540
3654
  ...buildTokenPayload(sessionStats)
3541
3655
  });
3542
- log.info(TAG16, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
3656
+ log.info(TAG17, `#${card.short_id} rejected (cycle ${currentCycle}/${maxCycles}) — moved to "${config.review.failColumn}"`);
3543
3657
  }
3544
3658
  if (workspaceId && (result.verdict === "approved" || result.verdict === "rejected")) {
3545
3659
  const originalEpisodeId = await findLatestImplementEpisode(client, workspaceId, card.project_id, card.short_id);
@@ -3561,7 +3675,7 @@ ${finding.description}${locationLine}`
3561
3675
  cleanupWorktree(worktreePath, branchName);
3562
3676
  }
3563
3677
  }
3564
- var TAG16 = "review-completion", MAX_FINDINGS = 10, REVIEW_MARKER = `---
3678
+ var TAG17 = "review-completion", MAX_FINDINGS = 10, MAX_SUBTASK_TITLE = 120, COMMENT_BODY_BUDGET = 9500, REVIEW_MARKER = `---
3565
3679
  **Review:`, RUN_LOG_TAIL_BYTES = 2048;
3566
3680
  var init_review_completion = __esm(() => {
3567
3681
  init_board_helpers();
@@ -3572,7 +3686,6 @@ var init_review_completion = __esm(() => {
3572
3686
  init_types();
3573
3687
  init_worktree();
3574
3688
  });
3575
-
3576
3689
  // ../harmony-shared/dist/cardLinks.js
3577
3690
  var init_cardLinks = () => {};
3578
3691
  // ../harmony-shared/dist/classification.js
@@ -3974,7 +4087,7 @@ class StateStore {
3974
4087
  const raw = readFileSync3(this.path, "utf-8");
3975
4088
  const parsed = JSON.parse(raw);
3976
4089
  if (parsed?.version !== SCHEMA_VERSION) {
3977
- log.warn(TAG17, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
4090
+ log.warn(TAG18, `state file has version ${parsed?.version}, expected ${SCHEMA_VERSION} — starting fresh`);
3978
4091
  return emptyState();
3979
4092
  }
3980
4093
  return {
@@ -3987,7 +4100,7 @@ class StateStore {
3987
4100
  daily: parsed.daily ?? []
3988
4101
  };
3989
4102
  } catch (err) {
3990
- log.error(TAG17, `failed to read state file: ${err instanceof Error ? err.message : err}`);
4103
+ log.error(TAG18, `failed to read state file: ${err instanceof Error ? err.message : err}`);
3991
4104
  return emptyState();
3992
4105
  }
3993
4106
  }
@@ -4143,7 +4256,7 @@ class StateStore {
4143
4256
  return this.state.daily.find((d) => d.date === key)?.costCents ?? 0;
4144
4257
  }
4145
4258
  }
4146
- var TAG17 = "state-store", SCHEMA_VERSION = 1;
4259
+ var TAG18 = "state-store", SCHEMA_VERSION = 1;
4147
4260
  var init_state_store = __esm(() => {
4148
4261
  init_log();
4149
4262
  });
@@ -4170,7 +4283,7 @@ function normalizeToolResultContent(raw) {
4170
4283
  return String(raw);
4171
4284
  }
4172
4285
  }
4173
- var TAG18 = "stream-parser", StreamParser;
4286
+ var TAG19 = "stream-parser", StreamParser;
4174
4287
  var init_stream_parser = __esm(() => {
4175
4288
  init_log();
4176
4289
  StreamParser = class StreamParser extends EventEmitter {
@@ -4179,6 +4292,10 @@ var init_stream_parser = __esm(() => {
4179
4292
  toolNames = new Map;
4180
4293
  hasEmittedText = false;
4181
4294
  observedModel;
4295
+ capturedSessionId;
4296
+ get sessionId() {
4297
+ return this.capturedSessionId;
4298
+ }
4182
4299
  attach(stream) {
4183
4300
  if (this.attached) {
4184
4301
  throw new Error("StreamParser already attached to a stream");
@@ -4214,14 +4331,14 @@ var init_stream_parser = __esm(() => {
4214
4331
  try {
4215
4332
  msg = JSON.parse(line);
4216
4333
  } catch {
4217
- log.debug(TAG18, `Non-JSON line: ${line.slice(0, 100)}`);
4334
+ log.debug(TAG19, `Non-JSON line: ${line.slice(0, 100)}`);
4218
4335
  return;
4219
4336
  }
4220
4337
  try {
4221
4338
  this.handleMessage(msg);
4222
4339
  } catch (err) {
4223
4340
  const errMsg = err instanceof Error ? err.message : String(err);
4224
- log.warn(TAG18, `Error handling stream event: ${errMsg}`);
4341
+ log.warn(TAG19, `Error handling stream event: ${errMsg}`);
4225
4342
  this.emit("parse_error", errMsg);
4226
4343
  }
4227
4344
  }
@@ -4233,6 +4350,9 @@ var init_stream_parser = __esm(() => {
4233
4350
  this.observedModel = msg.message.model;
4234
4351
  }
4235
4352
  }
4353
+ if (!this.capturedSessionId && typeof msg.session_id === "string") {
4354
+ this.capturedSessionId = msg.session_id;
4355
+ }
4236
4356
  switch (msg.type) {
4237
4357
  case "assistant": {
4238
4358
  const blocks = msg.message?.content;
@@ -4243,10 +4363,11 @@ var init_stream_parser = __esm(() => {
4243
4363
  this.emit("text", block.text);
4244
4364
  this.hasEmittedText = true;
4245
4365
  } else if (block.type === "tool_use" && typeof block.name === "string") {
4246
- if (typeof block.id === "string") {
4247
- this.toolNames.set(block.id, block.name);
4366
+ const toolUseId = typeof block.id === "string" ? block.id : undefined;
4367
+ if (toolUseId) {
4368
+ this.toolNames.set(toolUseId, block.name);
4248
4369
  }
4249
- this.emit("tool_start", block.name, block.input);
4370
+ this.emit("tool_start", block.name, block.input, toolUseId);
4250
4371
  }
4251
4372
  }
4252
4373
  break;
@@ -4303,7 +4424,7 @@ async function withRetry(step, cardShortId, op, attempts, backoffMs) {
4303
4424
  const msg2 = err instanceof Error ? err.message : String(err);
4304
4425
  if (i < attempts - 1) {
4305
4426
  const wait = backoffMs * 2 ** i;
4306
- log.warn(TAG19, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
4427
+ log.warn(TAG20, `${step} failed for #${cardShortId} (attempt ${i + 1}/${attempts}): ${msg2} — retrying in ${wait}ms`);
4307
4428
  await new Promise((r) => setTimeout(r, wait));
4308
4429
  }
4309
4430
  }
@@ -4325,10 +4446,10 @@ async function runTransition(client, card, plan, opts = {}) {
4325
4446
  if (opts.strictColumn) {
4326
4447
  throw new TransitionError("move", 1, msg);
4327
4448
  }
4328
- log.warn(TAG19, `#${shortId}: ${msg} — skipping move`);
4449
+ log.warn(TAG20, `#${shortId}: ${msg} — skipping move`);
4329
4450
  } else if (card.column_id !== target.id) {
4330
4451
  await withRetry("move", shortId, () => client.moveCard(card.id, target.id), attempts, backoffMs);
4331
- log.info(TAG19, `#${shortId} → "${target.name}"`);
4452
+ log.info(TAG20, `#${shortId} → "${target.name}"`);
4332
4453
  card.column_id = target.id;
4333
4454
  }
4334
4455
  }
@@ -4341,7 +4462,7 @@ async function runTransition(client, card, plan, opts = {}) {
4341
4462
  continue;
4342
4463
  await withRetry("addLabel", shortId, () => client.addLabelToCard(card.id, labelId), attempts, backoffMs);
4343
4464
  existing.add(labelId);
4344
- log.info(TAG19, `#${shortId} +label "${name}"`);
4465
+ log.info(TAG20, `#${shortId} +label "${name}"`);
4345
4466
  }
4346
4467
  card.labelIds = Array.from(existing);
4347
4468
  }
@@ -4353,17 +4474,17 @@ async function runTransition(client, card, plan, opts = {}) {
4353
4474
  continue;
4354
4475
  await withRetry("removeLabel", shortId, () => client.removeLabelFromCard(card.id, match.id), attempts, backoffMs);
4355
4476
  existing.delete(match.id);
4356
- log.info(TAG19, `#${shortId} -label "${name}"`);
4477
+ log.info(TAG20, `#${shortId} -label "${name}"`);
4357
4478
  }
4358
4479
  card.labelIds = Array.from(existing);
4359
4480
  }
4360
4481
  if (plan.updateCard) {
4361
4482
  await withRetry("updateCard", shortId, () => client.updateCard(card.id, plan.updateCard), attempts, backoffMs);
4362
- log.info(TAG19, `#${shortId} updated`);
4483
+ log.info(TAG20, `#${shortId} updated`);
4363
4484
  }
4364
4485
  if (plan.endSession) {
4365
4486
  await withRetry("endSession", shortId, () => client.endAgentSession(card.id, plan.endSession), attempts, backoffMs);
4366
- log.info(TAG19, `#${shortId} session ended (${plan.endSession.status})`);
4487
+ log.info(TAG20, `#${shortId} session ended (${plan.endSession.status})`);
4367
4488
  }
4368
4489
  if (opts.store && opts.runId) {
4369
4490
  try {
@@ -4376,11 +4497,11 @@ async function ensureLabel(client, projectId, name, color, attempts, backoffMs)
4376
4497
  const result = await withRetry("addLabel", 0, () => client.createLabel(projectId, { name, color: color ?? "#8b5cf6" }), attempts, backoffMs);
4377
4498
  return result?.label?.id ?? null;
4378
4499
  } catch (err) {
4379
- log.warn(TAG19, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
4500
+ log.warn(TAG20, `ensureLabel "${name}" failed: ${err instanceof Error ? err.message : err}`);
4380
4501
  return null;
4381
4502
  }
4382
4503
  }
4383
- var TAG19 = "transition", TransitionError;
4504
+ var TAG20 = "transition", TransitionError;
4384
4505
  var init_transitions = __esm(() => {
4385
4506
  init_log();
4386
4507
  TransitionError = class TransitionError extends Error {
@@ -4398,7 +4519,7 @@ var init_transitions = __esm(() => {
4398
4519
  });
4399
4520
 
4400
4521
  // src/review-worker.ts
4401
- import { execFileSync as execFileSync9 } from "node:child_process";
4522
+ import { execFileSync as execFileSync10 } from "node:child_process";
4402
4523
 
4403
4524
  class ReviewWorker {
4404
4525
  config;
@@ -4464,7 +4585,7 @@ class ReviewWorker {
4464
4585
  }
4465
4586
  }
4466
4587
  get tag() {
4467
- return `${TAG20}:${this.id}`;
4588
+ return `${TAG21}:${this.id}`;
4468
4589
  }
4469
4590
  get isIdle() {
4470
4591
  return this.state === "idle";
@@ -4521,7 +4642,7 @@ class ReviewWorker {
4521
4642
  let localDiff = null;
4522
4643
  if (localMode) {
4523
4644
  log.info(this.tag, `No branch found for #${card.short_id}, attempting local review`);
4524
- this.worktreePath = execFileSync9("git", ["rev-parse", "--show-toplevel"], {
4645
+ this.worktreePath = execFileSync10("git", ["rev-parse", "--show-toplevel"], {
4525
4646
  encoding: "utf-8",
4526
4647
  timeout: 5000
4527
4648
  }).trim();
@@ -4588,7 +4709,7 @@ class ReviewWorker {
4588
4709
  if (localMode) {
4589
4710
  diff = localDiff ?? "";
4590
4711
  } else {
4591
- diff = execFileSync9("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
4712
+ diff = execFileSync10("git", ["diff", `origin/${this.config.worktree.baseBranch}..HEAD`], { cwd, encoding: "utf-8", timeout: 30000 });
4592
4713
  }
4593
4714
  } catch {
4594
4715
  diff = "(unable to retrieve diff)";
@@ -4868,7 +4989,7 @@ class ReviewWorker {
4868
4989
  }
4869
4990
  resolveLocalChanges(repoRoot, shortId) {
4870
4991
  try {
4871
- const localChanges = execFileSync9("git", ["diff", "HEAD"], {
4992
+ const localChanges = execFileSync10("git", ["diff", "HEAD"], {
4872
4993
  cwd: repoRoot,
4873
4994
  encoding: "utf-8",
4874
4995
  timeout: 5000
@@ -4880,7 +5001,7 @@ class ReviewWorker {
4880
5001
  log.warn(this.tag, "Failed to check uncommitted changes");
4881
5002
  }
4882
5003
  try {
4883
- const matchingCommits = execFileSync9("git", ["log", "--format=%H", "-20", `--grep=#${shortId}`], { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }).trim();
5004
+ const matchingCommits = execFileSync10("git", ["log", "--format=%H", "-20", `--grep=#${shortId}`], { cwd: repoRoot, encoding: "utf-8", timeout: 1e4 }).trim();
4884
5005
  if (matchingCommits) {
4885
5006
  const hashes = matchingCommits.split(`
4886
5007
  `).filter((h) => /^[0-9a-f]{4,40}$/i.test(h));
@@ -4890,7 +5011,7 @@ class ReviewWorker {
4890
5011
  const diffs = [];
4891
5012
  for (const hash of hashes) {
4892
5013
  try {
4893
- const commitDiff = execFileSync9("git", ["diff", `${hash}~1..${hash}`], { cwd: repoRoot, encoding: "utf-8", timeout: 30000 });
5014
+ const commitDiff = execFileSync10("git", ["diff", `${hash}~1..${hash}`], { cwd: repoRoot, encoding: "utf-8", timeout: 30000 });
4894
5015
  if (commitDiff)
4895
5016
  diffs.push(commitDiff);
4896
5017
  } catch {
@@ -4933,7 +5054,7 @@ class ReviewWorker {
4933
5054
  this.lastSessionStats = null;
4934
5055
  }
4935
5056
  }
4936
- var TAG20 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
5057
+ var TAG21 = "review-worker", CANCEL_SIGINT_TIMEOUT = 30000, CANCEL_SIGTERM_TIMEOUT = 1e4;
4937
5058
  var init_review_worker = __esm(() => {
4938
5059
  init_board_helpers();
4939
5060
  init_completion();
@@ -4982,7 +5103,7 @@ class SleepGuard {
4982
5103
  if (!this.child.killed)
4983
5104
  this.child.kill("SIGTERM");
4984
5105
  this.child = null;
4985
- log.info(TAG21, "sleep assertion released");
5106
+ log.info(TAG22, "sleep assertion released");
4986
5107
  }
4987
5108
  }
4988
5109
  start() {
@@ -4997,7 +5118,7 @@ class SleepGuard {
4997
5118
  spawned = true;
4998
5119
  });
4999
5120
  child.on("error", (err) => {
5000
- log.warn(TAG21, `caffeinate unavailable: ${err.message}`);
5121
+ log.warn(TAG22, `caffeinate unavailable: ${err.message}`);
5001
5122
  if (this.child === child)
5002
5123
  this.child = null;
5003
5124
  });
@@ -5010,13 +5131,13 @@ class SleepGuard {
5010
5131
  });
5011
5132
  child.unref();
5012
5133
  this.child = child;
5013
- log.info(TAG21, "sleep assertion acquired (caffeinate -i)");
5134
+ log.info(TAG22, "sleep assertion acquired (caffeinate -i)");
5014
5135
  } catch (err) {
5015
- log.warn(TAG21, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
5136
+ log.warn(TAG22, `failed to spawn caffeinate: ${err instanceof Error ? err.message : err}`);
5016
5137
  }
5017
5138
  }
5018
5139
  }
5019
- var TAG21 = "sleep-guard";
5140
+ var TAG22 = "sleep-guard";
5020
5141
  var init_sleep_guard = __esm(() => {
5021
5142
  init_log();
5022
5143
  });
@@ -5027,7 +5148,7 @@ async function fetchBlocksLinks(client, cardId) {
5027
5148
  const { links } = await client.getCardLinks(cardId);
5028
5149
  return links.filter((l) => l.link_type === "blocks");
5029
5150
  } catch (err) {
5030
- log.warn(TAG22, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5151
+ log.warn(TAG23, `link fetch failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
5031
5152
  return null;
5032
5153
  }
5033
5154
  }
@@ -5059,31 +5180,175 @@ async function promoteUnblockedSuccessors(completedCard, deps) {
5059
5180
  const successors = links.filter((l) => l.direction === "outgoing" && !l.target_card.done);
5060
5181
  if (successors.length === 0)
5061
5182
  return;
5062
- log.info(TAG22, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
5183
+ log.info(TAG23, `#${completedCard.short_id} completed — checking ${successors.length} chained successor(s)`);
5063
5184
  for (const link of successors) {
5064
5185
  const successorId = link.target_card.id;
5065
5186
  try {
5066
5187
  const { card } = await deps.client.getCard(successorId);
5067
5188
  if (card.assigned_agent_id === deps.agentId) {} else if (card.assigned_agent_id === null && !card.assignee_id) {
5068
- log.info(TAG22, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
5189
+ log.info(TAG23, `successor #${card.short_id} unassigned — auto-assigning to continue chain`);
5069
5190
  await deps.client.updateCard(successorId, {
5070
5191
  assignedAgentId: deps.agentId
5071
5192
  });
5072
5193
  } else {
5073
- log.debug(TAG22, `successor #${card.short_id} assigned to different entity — skipping`);
5194
+ log.debug(TAG23, `successor #${card.short_id} assigned to different entity — skipping`);
5074
5195
  continue;
5075
5196
  }
5076
5197
  await deps.enqueue(successorId);
5077
5198
  } catch (err) {
5078
- log.warn(TAG22, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
5199
+ log.warn(TAG23, `promotion failed for successor ${successorId}: ${err instanceof Error ? err.message : err}`);
5079
5200
  }
5080
5201
  }
5081
5202
  }
5082
- var TAG22 = "unblock";
5203
+ var TAG23 = "unblock";
5083
5204
  var init_unblock = __esm(() => {
5084
5205
  init_log();
5085
5206
  });
5086
5207
 
5208
+ // src/cli-agent-runner.ts
5209
+ class CliAgentRunner {
5210
+ client;
5211
+ cardId;
5212
+ sessionId;
5213
+ buffer = [];
5214
+ flushTimer = null;
5215
+ flushing = false;
5216
+ stopped = false;
5217
+ constructor(client, cardId, sessionId) {
5218
+ this.client = client;
5219
+ this.cardId = cardId;
5220
+ this.sessionId = sessionId;
5221
+ }
5222
+ attach(parser) {
5223
+ if (this.stopped)
5224
+ return;
5225
+ parser.on("tool_start", (name, input, toolUseId) => {
5226
+ this.enqueue({
5227
+ kind: "tool_started",
5228
+ source: "agent",
5229
+ payload: { toolName: name, toolUseId, input }
5230
+ });
5231
+ });
5232
+ parser.on("tool_end", (name, toolUseId, content) => {
5233
+ this.enqueue({
5234
+ kind: "tool_ended",
5235
+ source: "agent",
5236
+ payload: {
5237
+ toolName: name,
5238
+ toolUseId,
5239
+ output: content === undefined ? undefined : content.slice(0, MAX_OUTPUT_LEN)
5240
+ }
5241
+ });
5242
+ });
5243
+ parser.on("text", (content) => {
5244
+ const text = content.trim();
5245
+ if (!text)
5246
+ return;
5247
+ this.enqueue({
5248
+ kind: "assistant_text",
5249
+ source: "agent",
5250
+ payload: { text: text.slice(0, MAX_TEXT_LEN) }
5251
+ });
5252
+ });
5253
+ parser.on("cost_update", (cost) => {
5254
+ this.enqueue({
5255
+ kind: "cost_updated",
5256
+ source: "agent",
5257
+ payload: mapCost(cost)
5258
+ });
5259
+ });
5260
+ this.startTimer();
5261
+ }
5262
+ recordRunStarted(payload) {
5263
+ this.enqueue({ kind: "run_started", source: "system", payload });
5264
+ this.startTimer();
5265
+ }
5266
+ recordPhaseChanged(phase, previousPhase) {
5267
+ this.enqueue({
5268
+ kind: "phase_changed",
5269
+ source: "system",
5270
+ payload: { phase, previousPhase }
5271
+ });
5272
+ }
5273
+ recordProgress(payload) {
5274
+ this.enqueue({ kind: "progress", source: "system", payload });
5275
+ }
5276
+ recordError(payload) {
5277
+ this.enqueue({ kind: "error", source: "system", payload });
5278
+ }
5279
+ recordFinished(payload) {
5280
+ this.enqueue({ kind: "run_finished", source: "system", payload });
5281
+ }
5282
+ record(body) {
5283
+ this.enqueue(body);
5284
+ this.startTimer();
5285
+ }
5286
+ enqueue(body) {
5287
+ if (this.stopped)
5288
+ return;
5289
+ this.buffer.push({ ...body, createdAt: new Date().toISOString() });
5290
+ if (this.buffer.length >= MAX_BUFFER)
5291
+ this.flush();
5292
+ }
5293
+ startTimer() {
5294
+ if (this.flushTimer || this.stopped)
5295
+ return;
5296
+ this.flushTimer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
5297
+ this.flushTimer.unref?.();
5298
+ }
5299
+ async flush() {
5300
+ if (this.flushing || this.buffer.length === 0)
5301
+ return;
5302
+ this.flushing = true;
5303
+ const batch = this.buffer.splice(0, MAX_BUFFER);
5304
+ try {
5305
+ await this.client.appendAgentRunEvents(this.cardId, {
5306
+ sessionId: this.sessionId,
5307
+ events: batch
5308
+ });
5309
+ } catch (err) {
5310
+ log.warn(TAG24, `Failed to flush run events: ${err}`);
5311
+ this.buffer.unshift(...batch);
5312
+ if (this.buffer.length > MAX_BUFFER) {
5313
+ this.buffer.length = MAX_BUFFER;
5314
+ }
5315
+ } finally {
5316
+ this.flushing = false;
5317
+ }
5318
+ }
5319
+ async flushFinal() {
5320
+ for (let i = 0;this.buffer.length > 0 && i < 5; i++) {
5321
+ const before = this.buffer.length;
5322
+ await this.flush();
5323
+ if (this.buffer.length >= before)
5324
+ break;
5325
+ }
5326
+ }
5327
+ stop() {
5328
+ this.stopped = true;
5329
+ if (this.flushTimer) {
5330
+ clearInterval(this.flushTimer);
5331
+ this.flushTimer = null;
5332
+ }
5333
+ }
5334
+ }
5335
+ function mapCost(cost) {
5336
+ return {
5337
+ totalCostUsd: cost.totalCostUsd,
5338
+ inputTokens: cost.totalInputTokens,
5339
+ outputTokens: cost.totalOutputTokens,
5340
+ cacheCreationInputTokens: cost.totalCacheCreationInputTokens,
5341
+ cacheReadInputTokens: cost.totalCacheReadInputTokens,
5342
+ numTurns: cost.numTurns,
5343
+ modelName: cost.modelName,
5344
+ durationMs: cost.durationMs
5345
+ };
5346
+ }
5347
+ var TAG24 = "cli-agent-runner", FLUSH_INTERVAL_MS = 2000, MAX_BUFFER = 1000, MAX_TEXT_LEN = 8000, MAX_OUTPUT_LEN = 4000;
5348
+ var init_cli_agent_runner = __esm(() => {
5349
+ init_log();
5350
+ });
5351
+
5087
5352
  // src/model-tier.ts
5088
5353
  function clampWithdrawn(model) {
5089
5354
  return WITHDRAWN_MODEL.test(model) ? MAX_IMPLEMENT_MODEL : model;
@@ -5134,11 +5399,11 @@ async function buildPrompt(enriched, branchName, worktreePath, client, workspace
5134
5399
  Do NOT push to main. All your work stays on \`${branchName}\`.
5135
5400
  When finished, call harmony_end_agent_session with status="completed".`
5136
5401
  });
5137
- log.info(TAG23, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
5402
+ log.info(TAG25, `Generated prompt for #${card.short_id} — ${result.contextSummary.memoryCount} memories, ${result.tokenEstimate} tokens`);
5138
5403
  return result.prompt + pastEpisodesSection;
5139
5404
  } catch (err) {
5140
5405
  const msg = err instanceof Error ? err.message : String(err);
5141
- log.warn(TAG23, `Failed to generate prompt via API, using fallback: ${msg}`);
5406
+ log.warn(TAG25, `Failed to generate prompt via API, using fallback: ${msg}`);
5142
5407
  const commentsSection = await renderCommentsSection(client, card.id);
5143
5408
  return buildFallbackPrompt(enriched, branchName, worktreePath) + commentsSection + pastEpisodesSection;
5144
5409
  }
@@ -5156,7 +5421,7 @@ async function renderCommentsSection(client, cardId) {
5156
5421
 
5157
5422
  ${section}` : "";
5158
5423
  } catch (err) {
5159
- log.warn(TAG23, "comment-thread fetch failed", {
5424
+ log.warn(TAG25, "comment-thread fetch failed", {
5160
5425
  event: "comment_fetch_failed",
5161
5426
  error: err instanceof Error ? err.message : String(err)
5162
5427
  });
@@ -5206,7 +5471,7 @@ ${description}`.trim();
5206
5471
  ## Similar past tasks
5207
5472
  ${bullets}`;
5208
5473
  } catch (err) {
5209
- log.warn(TAG23, "past-episodes recall failed", {
5474
+ log.warn(TAG25, "past-episodes recall failed", {
5210
5475
  event: "episode_recall_failed",
5211
5476
  error: err instanceof Error ? err.message : String(err)
5212
5477
  });
@@ -5247,13 +5512,346 @@ ${subtaskStr}
5247
5512
  You are working in a git worktree at \`${worktreePath}\` on branch \`${branchName}\`.
5248
5513
  Do NOT push to main. All your work stays on \`${branchName}\`.`;
5249
5514
  }
5250
- var TAG23 = "prompt";
5515
+ var TAG25 = "prompt";
5251
5516
  var init_prompt = __esm(() => {
5252
5517
  init_dist();
5253
5518
  init_log();
5254
5519
  });
5255
5520
 
5521
+ // src/sdk-agent-runner.ts
5522
+ import {
5523
+ query
5524
+ } from "@anthropic-ai/claude-agent-sdk";
5525
+ function mapSdkErrorKind(e) {
5526
+ switch (e) {
5527
+ case "authentication_failed":
5528
+ case "oauth_org_not_allowed":
5529
+ return "auth";
5530
+ case "billing_error":
5531
+ return "out_of_credits";
5532
+ case "rate_limit":
5533
+ case "overloaded":
5534
+ return "rate_limit";
5535
+ default:
5536
+ return null;
5537
+ }
5538
+ }
5539
+
5540
+ class SdkAgentRunner {
5541
+ cfg;
5542
+ abort = null;
5543
+ capturedSessionId;
5544
+ child = null;
5545
+ leaderPid;
5546
+ capturedStderr = "";
5547
+ toolNames = new Map;
5548
+ observedModel;
5549
+ effectiveModel;
5550
+ constructor(cfg = {}) {
5551
+ this.cfg = cfg;
5552
+ }
5553
+ get sessionId() {
5554
+ return this.capturedSessionId;
5555
+ }
5556
+ get capturedStderrText() {
5557
+ return this.capturedStderr;
5558
+ }
5559
+ start(input) {
5560
+ return this.run(input);
5561
+ }
5562
+ resume(input) {
5563
+ return this.run(input, input.resumeSessionId);
5564
+ }
5565
+ async send(_message) {
5566
+ throw new Error("SdkAgentRunner.send() (streaming-input steering) is not wired; the worker steers via stop→resume");
5567
+ }
5568
+ async stop(_reason) {
5569
+ this.abort?.abort();
5570
+ if (this.child) {
5571
+ await terminateGroup(this.child, {
5572
+ sigintTimeoutMs: STOP_SIGINT_MS,
5573
+ sigtermTimeoutMs: STOP_SIGTERM_MS
5574
+ });
5575
+ }
5576
+ reapGroup(this.leaderPid);
5577
+ }
5578
+ async* run(input, resumeSessionId) {
5579
+ this.abort = new AbortController;
5580
+ this.capturedStderr = "";
5581
+ this.child = null;
5582
+ this.leaderPid = undefined;
5583
+ this.toolNames.clear();
5584
+ this.observedModel = undefined;
5585
+ this.effectiveModel = input.model ?? this.cfg.model;
5586
+ yield {
5587
+ kind: "run_started",
5588
+ source: "system",
5589
+ payload: { runner: "sdk", model: input.model }
5590
+ };
5591
+ const allowed = this.cfg.allowedTools ?? SDK_ALLOWED_TOOLS;
5592
+ const builtinTools = allowed.filter((t) => !t.startsWith("mcp__") && !t.includes("*"));
5593
+ const options = {
5594
+ cwd: input.cwd,
5595
+ model: input.model ?? this.cfg.model,
5596
+ allowedTools: allowed,
5597
+ tools: builtinTools,
5598
+ permissionMode: "dontAsk",
5599
+ maxTurns: this.cfg.maxTurns,
5600
+ abortController: this.abort,
5601
+ ...resumeSessionId ? { resume: resumeSessionId } : {},
5602
+ ...this.cfg.maxBudgetUsd ? { maxBudgetUsd: this.cfg.maxBudgetUsd } : {},
5603
+ ...this.cfg.settingSources ? { settingSources: this.cfg.settingSources } : {},
5604
+ ...this.cfg.mcpServers ? { mcpServers: this.cfg.mcpServers } : {},
5605
+ ...this.cfg.strictMcpConfig ? { strictMcpConfig: true } : {},
5606
+ stderr: (data) => {
5607
+ this.capturedStderr += data;
5608
+ },
5609
+ spawnClaudeCodeProcess: (spawnOpts) => this.spawn(spawnOpts)
5610
+ };
5611
+ try {
5612
+ const q = query({ prompt: input.prompt, options });
5613
+ let failureReason = null;
5614
+ for await (const msg of q) {
5615
+ for (const ev of this.mapMessage(msg)) {
5616
+ if (ev.kind === "error") {
5617
+ failureReason = ev.payload.errorKind ?? "crash";
5618
+ }
5619
+ yield ev;
5620
+ }
5621
+ }
5622
+ yield {
5623
+ kind: "run_finished",
5624
+ source: "system",
5625
+ payload: failureReason ? { status: "failed", failureReason } : { status: "completed" }
5626
+ };
5627
+ } catch (err) {
5628
+ const message = err instanceof Error ? err.message : String(err);
5629
+ const cls = classifyRunError(`${message}
5630
+ ${this.capturedStderr}`);
5631
+ yield {
5632
+ kind: "error",
5633
+ source: "system",
5634
+ payload: {
5635
+ message,
5636
+ errorKind: cls.kind,
5637
+ retryable: cls.kind !== "auth" && cls.kind !== null
5638
+ }
5639
+ };
5640
+ yield {
5641
+ kind: "run_finished",
5642
+ source: "system",
5643
+ payload: { status: "failed", failureReason: cls.kind ?? "crash" }
5644
+ };
5645
+ } finally {
5646
+ reapGroup(this.leaderPid);
5647
+ }
5648
+ }
5649
+ spawn(spawnOpts) {
5650
+ const child = spawnInGroup(spawnOpts.command, spawnOpts.args, {
5651
+ cwd: spawnOpts.cwd,
5652
+ env: spawnOpts.env,
5653
+ stdio: ["pipe", "pipe", "pipe"]
5654
+ });
5655
+ this.child = child;
5656
+ this.leaderPid = child.pid;
5657
+ child.stderr?.on("data", (d) => {
5658
+ this.capturedStderr += d.toString();
5659
+ });
5660
+ this.cfg.onSpawn?.(child);
5661
+ return {
5662
+ stdin: child.stdin,
5663
+ stdout: child.stdout,
5664
+ get killed() {
5665
+ return child.killed;
5666
+ },
5667
+ get exitCode() {
5668
+ return child.exitCode;
5669
+ },
5670
+ kill: (signal) => {
5671
+ if (!child.pid)
5672
+ return false;
5673
+ try {
5674
+ process.kill(-child.pid, signal);
5675
+ return true;
5676
+ } catch {
5677
+ return child.kill(signal);
5678
+ }
5679
+ },
5680
+ on: (event, listener) => child.on(event, listener),
5681
+ once: (event, listener) => child.once(event, listener),
5682
+ off: (event, listener) => child.off(event, listener)
5683
+ };
5684
+ }
5685
+ *mapMessage(msg) {
5686
+ const sid = msg.session_id;
5687
+ if (sid && !this.capturedSessionId)
5688
+ this.capturedSessionId = sid;
5689
+ const model = msg.message?.model ?? msg.model;
5690
+ if (typeof model === "string" && !this.observedModel) {
5691
+ this.observedModel = model;
5692
+ }
5693
+ switch (msg.type) {
5694
+ case "assistant": {
5695
+ const am = msg;
5696
+ if (am.error) {
5697
+ yield {
5698
+ kind: "error",
5699
+ source: "system",
5700
+ payload: {
5701
+ message: `assistant error: ${am.error}`,
5702
+ errorKind: mapSdkErrorKind(am.error)
5703
+ }
5704
+ };
5705
+ }
5706
+ const blocks = am.message?.content;
5707
+ if (Array.isArray(blocks)) {
5708
+ for (const b of blocks) {
5709
+ if (b.type === "text" && typeof b.text === "string") {
5710
+ const text = b.text.trim();
5711
+ if (text) {
5712
+ yield {
5713
+ kind: "assistant_text",
5714
+ source: "agent",
5715
+ payload: { text: text.slice(0, MAX_TEXT_LEN2) }
5716
+ };
5717
+ }
5718
+ } else if (b.type === "tool_use" && typeof b.name === "string") {
5719
+ if (typeof b.id === "string")
5720
+ this.toolNames.set(b.id, b.name);
5721
+ yield {
5722
+ kind: "tool_started",
5723
+ source: "agent",
5724
+ payload: { toolName: b.name, toolUseId: b.id, input: b.input }
5725
+ };
5726
+ }
5727
+ }
5728
+ }
5729
+ break;
5730
+ }
5731
+ case "user": {
5732
+ const um = msg;
5733
+ const blocks = um.message?.content;
5734
+ if (Array.isArray(blocks)) {
5735
+ for (const b of blocks) {
5736
+ if (b.type === "tool_result" && typeof b.tool_use_id === "string") {
5737
+ const toolName = this.toolNames.get(b.tool_use_id) ?? "";
5738
+ this.toolNames.delete(b.tool_use_id);
5739
+ yield {
5740
+ kind: "tool_ended",
5741
+ source: "agent",
5742
+ payload: {
5743
+ toolName,
5744
+ toolUseId: b.tool_use_id,
5745
+ output: normalize(b.content)?.slice(0, MAX_OUTPUT_LEN2),
5746
+ isError: b.is_error
5747
+ }
5748
+ };
5749
+ }
5750
+ }
5751
+ }
5752
+ break;
5753
+ }
5754
+ case "result": {
5755
+ const r = msg;
5756
+ if (typeof r.total_cost_usd === "number") {
5757
+ yield {
5758
+ kind: "cost_updated",
5759
+ source: "agent",
5760
+ payload: {
5761
+ totalCostUsd: r.total_cost_usd,
5762
+ inputTokens: r.usage?.input_tokens ?? 0,
5763
+ outputTokens: r.usage?.output_tokens ?? 0,
5764
+ cacheCreationInputTokens: r.usage?.cache_creation_input_tokens ?? 0,
5765
+ cacheReadInputTokens: r.usage?.cache_read_input_tokens ?? 0,
5766
+ numTurns: r.num_turns ?? 0,
5767
+ durationMs: r.duration_ms,
5768
+ modelName: this.observedModel ?? this.effectiveModel
5769
+ }
5770
+ };
5771
+ }
5772
+ if (r.subtype && r.subtype !== "success") {
5773
+ const joined = (r.errors ?? []).join(`
5774
+ `);
5775
+ const cls = classifyRunError(`${r.subtype}
5776
+ ${joined}
5777
+ ${this.capturedStderr}`);
5778
+ yield {
5779
+ kind: "error",
5780
+ source: "system",
5781
+ payload: {
5782
+ message: `result ${r.subtype}: ${joined || "(no detail)"}`,
5783
+ errorKind: cls.kind,
5784
+ retryable: cls.kind !== "auth" && cls.kind !== null
5785
+ }
5786
+ };
5787
+ }
5788
+ break;
5789
+ }
5790
+ }
5791
+ }
5792
+ }
5793
+ function normalize(raw) {
5794
+ if (raw == null)
5795
+ return;
5796
+ if (typeof raw === "string")
5797
+ return raw;
5798
+ if (Array.isArray(raw)) {
5799
+ const parts = [];
5800
+ for (const b of raw) {
5801
+ if (b && typeof b === "object" && "text" in b && typeof b.text === "string") {
5802
+ parts.push(b.text);
5803
+ }
5804
+ }
5805
+ return parts.length ? parts.join("") : JSON.stringify(raw);
5806
+ }
5807
+ try {
5808
+ return JSON.stringify(raw);
5809
+ } catch {
5810
+ return String(raw);
5811
+ }
5812
+ }
5813
+ var SDK_ALLOWED_TOOLS, MAX_TEXT_LEN2 = 8000, MAX_OUTPUT_LEN2 = 4000, STOP_SIGINT_MS = 2000, STOP_SIGTERM_MS = 2000;
5814
+ var init_sdk_agent_runner = __esm(() => {
5815
+ init_error_classifier();
5816
+ init_process_group();
5817
+ SDK_ALLOWED_TOOLS = [
5818
+ "Bash",
5819
+ "Read",
5820
+ "Write",
5821
+ "Edit",
5822
+ "Glob",
5823
+ "Grep",
5824
+ "Agent",
5825
+ "mcp__harmony__*"
5826
+ ];
5827
+ });
5828
+
5256
5829
  // src/worker.ts
5830
+ function sdkDraftLogLine(ev) {
5831
+ switch (ev.kind) {
5832
+ case "assistant_text":
5833
+ return ev.payload.text.slice(0, 200);
5834
+ case "tool_started":
5835
+ return ev.payload.toolName;
5836
+ case "tool_ended":
5837
+ return `${ev.payload.toolUseId ?? ""}${ev.payload.isError ? " (error)" : ""}`;
5838
+ case "cost_updated":
5839
+ return `$${ev.payload.totalCostUsd.toFixed(4)} turns=${ev.payload.numTurns}`;
5840
+ case "error":
5841
+ return ev.payload.message.slice(0, 200);
5842
+ case "run_finished":
5843
+ return ev.payload.status;
5844
+ default:
5845
+ return "";
5846
+ }
5847
+ }
5848
+ function buildSteeringPrompt(messages) {
5849
+ if (messages.length === 1)
5850
+ return messages[0];
5851
+ return messages.map((m, i) => `${i + 1}. ${m}`).join(`
5852
+ `);
5853
+ }
5854
+
5257
5855
  class Worker {
5258
5856
  config;
5259
5857
  client;
@@ -5274,12 +5872,16 @@ class Worker {
5274
5872
  timeoutTimer = null;
5275
5873
  heartbeatTimer = null;
5276
5874
  progressTracker = null;
5875
+ cliRunner = null;
5876
+ sdkRunner = null;
5277
5877
  lastSessionStats;
5278
5878
  aborted = false;
5279
5879
  timedOut = false;
5280
5880
  verificationFailed = false;
5281
5881
  sessionId = null;
5282
5882
  runId = null;
5883
+ cliSessionId = null;
5884
+ lastDrainedSeq = 0;
5283
5885
  runCostCents = 0;
5284
5886
  runTurns = 0;
5285
5887
  constructor(id, config, client, agentId, onDone, workspaceId, projectId, stateStore, onCardCompleted, onApiError) {
@@ -5326,7 +5928,7 @@ class Worker {
5326
5928
  }
5327
5929
  }
5328
5930
  get tag() {
5329
- return `${TAG24}:${this.id}`;
5931
+ return `${TAG26}:${this.id}`;
5330
5932
  }
5331
5933
  get isIdle() {
5332
5934
  return this.state === "idle";
@@ -5348,6 +5950,8 @@ class Worker {
5348
5950
  this.verificationFailed = false;
5349
5951
  this.runCostCents = 0;
5350
5952
  this.runTurns = 0;
5953
+ this.cliSessionId = null;
5954
+ this.lastDrainedSeq = 0;
5351
5955
  this.cardId = card.id;
5352
5956
  this.startedAt = Date.now();
5353
5957
  this.runId = newRunId();
@@ -5385,9 +5989,16 @@ class Worker {
5385
5989
  });
5386
5990
  const sid = session && typeof session === "object" && "id" in session ? session.id : null;
5387
5991
  if (!sid) {
5388
- log.warn(TAG24, "startAgentSession returned no session id");
5992
+ log.warn(TAG26, "startAgentSession returned no session id");
5389
5993
  }
5390
5994
  this.sessionId = sid;
5995
+ if (this.sessionId) {
5996
+ this.cliRunner = new CliAgentRunner(this.client, card.id, this.sessionId);
5997
+ this.cliRunner.recordRunStarted({
5998
+ runner: this.config.runner,
5999
+ model: this.config.claude.model
6000
+ });
6001
+ }
5391
6002
  await this.recordPhase("preparing");
5392
6003
  const moved = await moveCardAndAddLabel(this.client, card, IN_PROGRESS_COLUMN, "agent");
5393
6004
  if (!moved) {
@@ -5434,6 +6045,9 @@ class Worker {
5434
6045
  await this.spawnClaude(prompt, card, subtasks, {
5435
6046
  model: this.selectImplementModel(card)
5436
6047
  });
6048
+ if (this.aborted)
6049
+ return;
6050
+ await this.drainSteeringMessages(card, subtasks);
5437
6051
  if (this.aborted)
5438
6052
  return;
5439
6053
  if (this.timeoutTimer) {
@@ -5460,7 +6074,17 @@ class Worker {
5460
6074
  log.error(this.tag, `Error on #${card.short_id}: ${msg}`);
5461
6075
  const rawStderr = err?.stderr;
5462
6076
  const errClass = classifyRunError(typeof rawStderr === "string" && rawStderr ? rawStderr : msg);
6077
+ const sdkKind = err?.errorKind;
6078
+ if (errClass.kind === null && sdkKind != null)
6079
+ errClass.kind = sdkKind;
5463
6080
  const apiError = errClass.kind !== null;
6081
+ const baseError = err instanceof WorktreeBaseError;
6082
+ const noBudgetBurn = apiError || baseError;
6083
+ this.cliRunner?.recordError({
6084
+ message: msg.slice(0, 500),
6085
+ errorKind: errClass.kind,
6086
+ retryable: noBudgetBurn
6087
+ });
5464
6088
  if (apiError) {
5465
6089
  try {
5466
6090
  this.onApiError?.(errClass);
@@ -5475,7 +6099,7 @@ class Worker {
5475
6099
  this.worktreePath = null;
5476
6100
  }
5477
6101
  const failureReason = apiError ? errClass.kind : "other";
5478
- const failureSummary = apiError ? describeApiError(errClass.kind) : `Run failed: ${msg.slice(0, 300)}`;
6102
+ const failureSummary = apiError ? describeApiError(errClass.kind) : baseError ? "Could not fetch current base branch — requeued without counting an attempt" : `Run failed: ${msg.slice(0, 300)}`;
5479
6103
  try {
5480
6104
  await runTransition(this.client, card, {
5481
6105
  move: { columnName: this.config.pickupColumns[0] ?? "To Do" },
@@ -5496,7 +6120,7 @@ class Worker {
5496
6120
  ...this.runLedger()
5497
6121
  });
5498
6122
  } catch {}
5499
- if (apiError) {
6123
+ if (noBudgetBurn) {
5500
6124
  await this.stateStore.decrementAttempt(card.id);
5501
6125
  } else {
5502
6126
  await this.recordOutcome(card.id, "failure");
@@ -5554,6 +6178,26 @@ class Worker {
5554
6178
  } catch {}
5555
6179
  await this.recordOutcome(card.id, "failure");
5556
6180
  }
6181
+ if (this.cliRunner) {
6182
+ if (succeeded) {
6183
+ this.cliRunner.recordFinished({ status: "completed" });
6184
+ } else if (this.timedOut) {
6185
+ this.cliRunner.recordFinished({
6186
+ status: "failed",
6187
+ failureReason: "timeout"
6188
+ });
6189
+ } else if (this.aborted) {
6190
+ this.cliRunner.recordFinished({
6191
+ status: "stopped",
6192
+ stopReason: "user_requested"
6193
+ });
6194
+ } else {
6195
+ this.cliRunner.recordFinished({ status: "failed" });
6196
+ }
6197
+ try {
6198
+ await this.cliRunner.flushFinal();
6199
+ } catch {}
6200
+ }
5557
6201
  this.cleanup();
5558
6202
  this.state = "idle";
5559
6203
  this.onDone(this);
@@ -5643,7 +6287,9 @@ class Worker {
5643
6287
  this.aborted = true;
5644
6288
  this.state = "cancelling";
5645
6289
  log.info(this.tag, `Cancelling work on ${this.cardId}`);
5646
- if (this.process && !this.process.killed) {
6290
+ if (this.sdkRunner) {
6291
+ await this.sdkRunner.stop(this.timedOut ? "timeout" : "user_requested");
6292
+ } else if (this.process && !this.process.killed) {
5647
6293
  await terminateGroup(this.process, {
5648
6294
  sigintTimeoutMs: CANCEL_SIGINT_TIMEOUT2,
5649
6295
  sigtermTimeoutMs: CANCEL_SIGTERM_TIMEOUT2
@@ -5678,7 +6324,9 @@ class Worker {
5678
6324
  const planTimeout = setTimeout(() => {
5679
6325
  planTimedOut = true;
5680
6326
  log.warn(this.tag, "Planning pass exceeded timeout — abandoning, implementing directly");
5681
- if (this.process && !this.process.killed) {
6327
+ if (this.sdkRunner) {
6328
+ this.sdkRunner.stop("timeout").catch(() => {});
6329
+ } else if (this.process && !this.process.killed) {
5682
6330
  terminateGroup(this.process, {
5683
6331
  sigintTimeoutMs: 1e4,
5684
6332
  sigtermTimeoutMs: 5000
@@ -5772,7 +6420,40 @@ class Worker {
5772
6420
  }
5773
6421
  return false;
5774
6422
  }
5775
- async spawnClaude(prompt, card, subtasks, opts = {}) {
6423
+ async drainSteeringMessages(card, subtasks) {
6424
+ if (!this.cliSessionId || !this.sessionId || !this.cardId)
6425
+ return;
6426
+ for (let i = 0;i < MAX_STEERING_ITERATIONS && !this.aborted; i++) {
6427
+ let messages;
6428
+ try {
6429
+ const res = await this.client.getPendingUserMessages(this.cardId, this.sessionId, this.lastDrainedSeq);
6430
+ messages = res.messages ?? [];
6431
+ } catch (err) {
6432
+ log.warn(this.tag, `Failed to fetch steering messages (non-fatal): ${err instanceof Error ? err.message : err}`);
6433
+ return;
6434
+ }
6435
+ if (messages.length === 0)
6436
+ return;
6437
+ this.lastDrainedSeq = Math.max(this.lastDrainedSeq, ...messages.map((m) => m.seq));
6438
+ log.info(this.tag, `Steering #${card.short_id}: resuming with ${messages.length} queued message(s)`);
6439
+ this.state = "running";
6440
+ await this.recordPhase("running");
6441
+ try {
6442
+ await this.spawnClaude(buildSteeringPrompt(messages.map((m) => m.text)), card, subtasks, {
6443
+ model: this.selectImplementModel(card),
6444
+ maxTurns: STEERING_MAX_TURNS,
6445
+ resumeSessionId: this.cliSessionId
6446
+ });
6447
+ } catch (err) {
6448
+ log.warn(this.tag, `Steering resume failed (non-fatal): ${err instanceof Error ? err.message : err}`);
6449
+ return;
6450
+ }
6451
+ }
6452
+ }
6453
+ spawnClaude(prompt, card, subtasks, opts = {}) {
6454
+ return this.config.runner === "sdk" ? this.spawnClaudeSdk(prompt, card, subtasks, opts) : this.spawnClaudeCli(prompt, card, subtasks, opts);
6455
+ }
6456
+ async spawnClaudeCli(prompt, card, subtasks, opts = {}) {
5776
6457
  const model = opts.model ?? this.config.claude.model;
5777
6458
  const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
5778
6459
  const allowedTools = opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS;
@@ -5789,6 +6470,7 @@ class Worker {
5789
6470
  String(maxTurns),
5790
6471
  "--allowedTools",
5791
6472
  allowedTools,
6473
+ ...opts.resumeSessionId ? ["--resume", opts.resumeSessionId] : [],
5792
6474
  ...this.config.claude.additionalArgs,
5793
6475
  "--",
5794
6476
  prompt
@@ -5808,10 +6490,11 @@ class Worker {
5808
6490
  });
5809
6491
  const parser = new StreamParser;
5810
6492
  this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
5811
- if (this.sessionId) {
5812
- this.progressTracker.setSessionId(this.sessionId);
5813
- }
5814
6493
  this.progressTracker.attach(parser);
6494
+ this.cliRunner?.attach(parser);
6495
+ if (this.cliRunner) {
6496
+ this.progressTracker.setRunEventSink(this.cliRunner);
6497
+ }
5815
6498
  if (this.process.stdout) {
5816
6499
  parser.attach(this.process.stdout);
5817
6500
  if (runLog) {
@@ -5835,14 +6518,16 @@ class Worker {
5835
6518
  reject(new Error(`Failed to spawn claude: ${err.message}`));
5836
6519
  });
5837
6520
  this.process.on("close", (code) => {
6521
+ const leaderPid = this.process?.pid;
5838
6522
  this.process = null;
6523
+ if (parser.sessionId)
6524
+ this.cliSessionId = parser.sessionId;
5839
6525
  this.lastSessionStats = this.progressTracker?.stats;
5840
6526
  const spawnCost = this.lastSessionStats?.cost;
5841
6527
  if (spawnCost) {
5842
6528
  this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
5843
6529
  this.runTurns += spawnCost.numTurns;
5844
6530
  }
5845
- this.progressTracker?.flushFinal();
5846
6531
  this.progressTracker?.stop();
5847
6532
  this.progressTracker = null;
5848
6533
  if (runLog) {
@@ -5852,6 +6537,7 @@ class Worker {
5852
6537
  `);
5853
6538
  runLog.stream.end();
5854
6539
  }
6540
+ reapGroup(leaderPid);
5855
6541
  if (this.aborted) {
5856
6542
  resolve3();
5857
6543
  } else if (code === 0) {
@@ -5864,11 +6550,101 @@ class Worker {
5864
6550
  });
5865
6551
  });
5866
6552
  }
6553
+ async spawnClaudeSdk(prompt, card, subtasks, opts = {}) {
6554
+ const model = opts.model ?? this.config.claude.model;
6555
+ const maxTurns = opts.maxTurns ?? this.config.claude.maxTurns;
6556
+ const allowedTools = (opts.allowedTools ?? IMPLEMENT_ALLOWED_TOOLS).split(",").map((t) => t.trim()).filter(Boolean);
6557
+ const initialPhase = opts.initialPhase ?? "exploring";
6558
+ const sdkCfg = this.config.sdk;
6559
+ log.info(this.tag, `Spawning Agent SDK runner (model=${model}, maxTurns=${maxTurns}${opts.resumeSessionId ? ", resume" : ""})`);
6560
+ const runLog = openRunLog(this.tag, this.runId, card.short_id);
6561
+ if (runLog) {
6562
+ log.info(this.tag, `Run log: ${runLog.path}`);
6563
+ runLog.stream.write(`# run=${this.runId} card=#${card.short_id} runner=sdk started=${new Date().toISOString()}
6564
+ ` + `# model=${model} maxTurns=${maxTurns} <prompt:${prompt.length} chars>
6565
+
6566
+ `);
6567
+ }
6568
+ this.progressTracker = new ProgressTracker(this.client, card.id, this.id, subtasks, initialPhase);
6569
+ if (this.cliRunner) {
6570
+ this.progressTracker.setRunEventSink(this.cliRunner);
6571
+ }
6572
+ const runner = new SdkAgentRunner({
6573
+ model,
6574
+ maxTurns,
6575
+ allowedTools,
6576
+ maxBudgetUsd: sdkCfg?.maxBudgetUsd,
6577
+ settingSources: sdkCfg?.settingSources,
6578
+ mcpServers: sdkCfg?.mcpServers,
6579
+ strictMcpConfig: sdkCfg?.strictMcpConfig,
6580
+ onSpawn: (child) => {
6581
+ this.process = child;
6582
+ }
6583
+ });
6584
+ this.sdkRunner = runner;
6585
+ const baseInput = {
6586
+ sessionId: this.sessionId ?? "",
6587
+ cardId: card.id,
6588
+ workspaceId: this.workspaceId,
6589
+ prompt,
6590
+ cwd: this.worktreePath,
6591
+ model
6592
+ };
6593
+ const stream = opts.resumeSessionId ? runner.resume({ ...baseInput, resumeSessionId: opts.resumeSessionId }) : runner.start(baseInput);
6594
+ let failure = null;
6595
+ let failureKind = null;
6596
+ try {
6597
+ for await (const ev of stream) {
6598
+ this.progressTracker?.ingest(ev);
6599
+ if (ev.source === "agent")
6600
+ this.cliRunner?.record(ev);
6601
+ if (ev.kind === "error") {
6602
+ failure = ev.payload.message;
6603
+ if (ev.payload.errorKind != null)
6604
+ failureKind = ev.payload.errorKind;
6605
+ }
6606
+ runLog?.stream.write(`[${ev.kind}] ${sdkDraftLogLine(ev)}
6607
+ `);
6608
+ }
6609
+ } finally {
6610
+ this.cliSessionId = runner.sessionId ?? this.cliSessionId;
6611
+ this.lastSessionStats = this.progressTracker?.stats;
6612
+ const spawnCost = this.lastSessionStats?.cost;
6613
+ if (spawnCost) {
6614
+ this.runCostCents += Math.round(spawnCost.totalCostUsd * 100);
6615
+ this.runTurns += spawnCost.numTurns;
6616
+ }
6617
+ if (runLog) {
6618
+ const stats = this.lastSessionStats;
6619
+ runLog.stream.write(`
6620
+ # runner=sdk aborted=${this.aborted} ` + `toolCalls=${stats?.toolCalls ?? 0} filesEdited=${stats?.filesEdited ?? 0} ` + `cost=$${stats?.cost?.totalCostUsd.toFixed(4) ?? "0"} ` + `ended=${new Date().toISOString()}
6621
+ `);
6622
+ runLog.stream.end();
6623
+ }
6624
+ this.progressTracker?.stop();
6625
+ this.progressTracker = null;
6626
+ this.process = null;
6627
+ this.sdkRunner = null;
6628
+ }
6629
+ if (this.aborted)
6630
+ return;
6631
+ if (failure) {
6632
+ const err = new Error(failure);
6633
+ err.stderr = runner.capturedStderrText;
6634
+ err.errorKind = failureKind;
6635
+ throw err;
6636
+ }
6637
+ }
5867
6638
  cleanup() {
5868
6639
  if (this.progressTracker) {
5869
6640
  this.progressTracker.stop();
5870
6641
  this.progressTracker = null;
5871
6642
  }
6643
+ if (this.cliRunner) {
6644
+ this.cliRunner.stop();
6645
+ this.cliRunner = null;
6646
+ }
6647
+ this.sdkRunner = null;
5872
6648
  this.stopHeartbeat();
5873
6649
  this.lastSessionStats = undefined;
5874
6650
  if (this.timeoutTimer) {
@@ -5893,9 +6669,10 @@ class Worker {
5893
6669
  this.runTurns = 0;
5894
6670
  }
5895
6671
  }
5896
- var TAG24 = "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;
6672
+ var TAG26 = "worker", CANCEL_SIGINT_TIMEOUT2 = 30000, CANCEL_SIGTERM_TIMEOUT2 = 1e4, STEERING_MAX_TURNS = 15, MAX_STEERING_ITERATIONS = 10, PLAN_ALLOWED_TOOLS = "Read,Grep,Glob,mcp__harmony__*", IMPLEMENT_ALLOWED_TOOLS = "Bash,Read,Write,Edit,Glob,Grep,Agent,mcp__harmony__*", PLAN_PHASE_TIMEOUT;
5897
6673
  var init_worker = __esm(() => {
5898
6674
  init_board_helpers();
6675
+ init_cli_agent_runner();
5899
6676
  init_completion();
5900
6677
  init_error_classifier();
5901
6678
  init_log();
@@ -5905,6 +6682,7 @@ var init_worker = __esm(() => {
5905
6682
  init_progress_tracker();
5906
6683
  init_prompt();
5907
6684
  init_run_log();
6685
+ init_sdk_agent_runner();
5908
6686
  init_state_store();
5909
6687
  init_stream_parser();
5910
6688
  init_transitions();
@@ -5963,39 +6741,39 @@ class Pool {
5963
6741
  }
5964
6742
  async enqueue(card, column, labels, subtasks, mode = "implement") {
5965
6743
  if (this.implQueue.has(card.id) || this.reviewQueue.has(card.id) || this.isCardActive(card.id)) {
5966
- log.debug(TAG25, `Card ${card.id} already queued or active, skipping`);
6744
+ log.debug(TAG27, `Card ${card.id} already queued or active, skipping`);
5967
6745
  return;
5968
6746
  }
5969
6747
  if (mode === "implement") {
5970
6748
  if (this.authPaused) {
5971
- log.debug(TAG25, `#${card.short_id} held — agent paused (auth error)`);
6749
+ log.debug(TAG27, `#${card.short_id} held — agent paused (auth error)`);
5972
6750
  await this.emitWaiting(card.id, "Agent paused — Anthropic auth error, check API credentials");
5973
6751
  return;
5974
6752
  }
5975
6753
  const cooldownMs = this.apiCooldownRemainingMs();
5976
6754
  if (cooldownMs > 0) {
5977
- log.debug(TAG25, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
6755
+ log.debug(TAG27, `#${card.short_id} held — API cooldown ${Math.round(cooldownMs / 1000)}s remaining`);
5978
6756
  await this.emitWaiting(card.id, `Paused — Anthropic API limit, retrying in ~${Math.round(cooldownMs / 1000)}s`);
5979
6757
  return;
5980
6758
  }
5981
6759
  const decision = this.budget.check(card.id);
5982
6760
  if (!decision.allow) {
5983
6761
  if (decision.reason === "daily_budget") {
5984
- log.warn(TAG25, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
6762
+ log.warn(TAG27, `#${card.short_id} skipped (daily_budget): ${decision.detail}`);
5985
6763
  await this.emitWaiting(card.id, `Daily budget reached — waiting for reset (${decision.detail})`);
5986
6764
  } else {
5987
- log.debug(TAG25, `#${card.short_id} gave up: ${decision.detail}`);
6765
+ log.debug(TAG27, `#${card.short_id} gave up: ${decision.detail}`);
5988
6766
  }
5989
6767
  return;
5990
6768
  }
5991
6769
  const blockers = await getUnresolvedBlockers(this.client, card, this.projectId);
5992
6770
  if (blockers === null) {
5993
- log.warn(TAG25, `#${card.short_id} blocker check failed — deferring to next tick`);
6771
+ log.warn(TAG27, `#${card.short_id} blocker check failed — deferring to next tick`);
5994
6772
  return;
5995
6773
  }
5996
6774
  if (blockers.length > 0) {
5997
6775
  const list = blockers.map((b) => `#${b.shortId}`).join(", ");
5998
- log.info(TAG25, `#${card.short_id} blocked by ${list} — waiting`);
6776
+ log.info(TAG27, `#${card.short_id} blocked by ${list} — waiting`);
5999
6777
  await this.emitWaiting(card.id, `Blocked by ${list} — waiting for chain`);
6000
6778
  return;
6001
6779
  }
@@ -6024,7 +6802,7 @@ class Pool {
6024
6802
  });
6025
6803
  this.lastWaitingEmit.set(cardId, currentTask);
6026
6804
  } catch (err) {
6027
- log.debug(TAG25, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6805
+ log.debug(TAG27, `waiting emit failed for ${cardId}: ${err instanceof Error ? err.message : err}`);
6028
6806
  }
6029
6807
  }
6030
6808
  noteApiError(err) {
@@ -6032,7 +6810,7 @@ class Pool {
6032
6810
  return;
6033
6811
  if (err.kind === "auth") {
6034
6812
  if (!this.authPaused) {
6035
- log.error(TAG25, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6813
+ log.error(TAG27, "Auth error from Claude CLI — pausing implement pickups until the daemon is restarted with valid credentials");
6036
6814
  }
6037
6815
  this.authPaused = true;
6038
6816
  return;
@@ -6041,7 +6819,7 @@ class Pool {
6041
6819
  const until = Date.now() + cooldownMs;
6042
6820
  if (until > this.apiCooldownUntil) {
6043
6821
  this.apiCooldownUntil = until;
6044
- log.warn(TAG25, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6822
+ log.warn(TAG27, `${describeApiError(err.kind)} — pausing implement pickups for ${Math.round(cooldownMs / 1000)}s`);
6045
6823
  }
6046
6824
  }
6047
6825
  apiCooldownRemainingMs() {
@@ -6054,13 +6832,13 @@ class Pool {
6054
6832
  const removed = queue.remove(cardId);
6055
6833
  if (removed) {
6056
6834
  this.cardDataCache.delete(cardId);
6057
- log.info(TAG25, `Removed #${removed.shortId} from ${removed.mode} queue`);
6835
+ log.info(TAG27, `Removed #${removed.shortId} from ${removed.mode} queue`);
6058
6836
  return;
6059
6837
  }
6060
6838
  }
6061
6839
  const worker = this.implWorkers.find((w) => w.cardId === cardId) ?? this.reviewWorkers.find((w) => w.cardId === cardId);
6062
6840
  if (worker) {
6063
- log.info(TAG25, `Cancelling worker ${worker.id} for card ${cardId}`);
6841
+ log.info(TAG27, `Cancelling worker ${worker.id} for card ${cardId}`);
6064
6842
  await worker.cancel();
6065
6843
  }
6066
6844
  }
@@ -6093,10 +6871,10 @@ class Pool {
6093
6871
  async handleAgentCommand(cardId, command) {
6094
6872
  const worker = this.implWorkers.find((w) => w.cardId === cardId && w.isActive) ?? this.reviewWorkers.find((w) => w.cardId === cardId && w.isActive);
6095
6873
  if (!worker) {
6096
- log.debug(TAG25, `No active worker for card ${cardId}, ignoring ${command}`);
6874
+ log.debug(TAG27, `No active worker for card ${cardId}, ignoring ${command}`);
6097
6875
  return;
6098
6876
  }
6099
- log.info(TAG25, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6877
+ log.info(TAG27, `Agent command: ${command} → worker ${worker.id} (card ${cardId})`);
6100
6878
  switch (command) {
6101
6879
  case "pause":
6102
6880
  await worker.pause();
@@ -6144,7 +6922,7 @@ class Pool {
6144
6922
  };
6145
6923
  }
6146
6924
  async shutdown() {
6147
- log.info(TAG25, "Shutting down pool...");
6925
+ log.info(TAG27, "Shutting down pool...");
6148
6926
  this.shuttingDown = true;
6149
6927
  const active = [
6150
6928
  ...this.implWorkers.filter((w) => w.isActive),
@@ -6152,7 +6930,7 @@ class Pool {
6152
6930
  ];
6153
6931
  await Promise.all(active.map((w) => w.cancel()));
6154
6932
  this.sleepGuard.stop();
6155
- log.info(TAG25, "Pool shutdown complete");
6933
+ log.info(TAG27, "Pool shutdown complete");
6156
6934
  }
6157
6935
  cardDataCache = new Map;
6158
6936
  tryDispatchFor(workers, queue, label) {
@@ -6160,7 +6938,7 @@ class Pool {
6160
6938
  return false;
6161
6939
  const idle = workers.find((w) => w.isIdle);
6162
6940
  if (!idle) {
6163
- log.debug(TAG25, `No idle ${label} workers (queue: ${queue.length})`);
6941
+ log.debug(TAG27, `No idle ${label} workers (queue: ${queue.length})`);
6164
6942
  return false;
6165
6943
  }
6166
6944
  const next = queue.dequeue();
@@ -6168,18 +6946,18 @@ class Pool {
6168
6946
  return false;
6169
6947
  const data = this.cardDataCache.get(next.cardId);
6170
6948
  if (!data) {
6171
- log.warn(TAG25, `No cached data for card ${next.cardId}, skipping`);
6949
+ log.warn(TAG27, `No cached data for card ${next.cardId}, skipping`);
6172
6950
  return false;
6173
6951
  }
6174
6952
  this.cardDataCache.delete(next.cardId);
6175
6953
  this.lastWaitingEmit.delete(next.cardId);
6176
- log.info(TAG25, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6954
+ log.info(TAG27, `Dispatching #${next.shortId} to ${label} worker ${idle.id}`);
6177
6955
  this.sleepGuard.acquire();
6178
6956
  idle.run(data.card, data.column, data.labels, data.subtasks);
6179
6957
  return true;
6180
6958
  }
6181
6959
  }
6182
- var TAG25 = "pool";
6960
+ var TAG27 = "pool";
6183
6961
  var init_pool = __esm(() => {
6184
6962
  init_error_classifier();
6185
6963
  init_log();
@@ -6221,7 +6999,7 @@ function load(path) {
6221
6999
  return parsed;
6222
7000
  return {};
6223
7001
  } catch (err) {
6224
- log.warn(TAG26, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
7002
+ log.warn(TAG28, `failed to read ${path}: ${err instanceof Error ? err.message : err}`);
6225
7003
  return {};
6226
7004
  }
6227
7005
  }
@@ -6239,7 +7017,7 @@ function recordDaemonPort(projectId, entry, path = defaultRegistryPath()) {
6239
7017
  registry[projectId] = { ...entry, updatedAt: Date.now() };
6240
7018
  save(path, registry);
6241
7019
  } catch (err) {
6242
- log.warn(TAG26, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7020
+ log.warn(TAG28, `failed to record port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6243
7021
  }
6244
7022
  }
6245
7023
  function lookupDaemonPort(projectId, path = defaultRegistryPath()) {
@@ -6255,10 +7033,10 @@ function clearDaemonPort(projectId, pid, path = defaultRegistryPath()) {
6255
7033
  delete registry[projectId];
6256
7034
  save(path, registry);
6257
7035
  } catch (err) {
6258
- log.warn(TAG26, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
7036
+ log.warn(TAG28, `failed to clear port for ${projectId}: ${err instanceof Error ? err.message : err}`);
6259
7037
  }
6260
7038
  }
6261
- var TAG26 = "port-registry";
7039
+ var TAG28 = "port-registry";
6262
7040
  var init_port_registry = __esm(() => {
6263
7041
  init_log();
6264
7042
  });
@@ -6279,7 +7057,7 @@ async function fetchCardSafely(client, cardId) {
6279
7057
  const { card } = await client.getCard(cardId);
6280
7058
  return card;
6281
7059
  } catch (err) {
6282
- log.warn(TAG27, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
7060
+ log.warn(TAG29, `cannot fetch card ${cardId}: ${err instanceof Error ? err.message : err}`);
6283
7061
  return null;
6284
7062
  }
6285
7063
  }
@@ -6289,7 +7067,7 @@ async function recoverOrphans(store, client, config) {
6289
7067
  return [];
6290
7068
  }
6291
7069
  const outcomes = [];
6292
- log.info(TAG27, `recovering ${active.length} orphan run(s) from prior daemon`);
7070
+ log.info(TAG29, `recovering ${active.length} orphan run(s) from prior daemon`);
6293
7071
  for (const run of active) {
6294
7072
  const outcome = {
6295
7073
  runId: run.runId,
@@ -6301,11 +7079,11 @@ async function recoverOrphans(store, client, config) {
6301
7079
  };
6302
7080
  outcomes.push(outcome);
6303
7081
  if (isProcessAlive(run.daemonPid, process.pid)) {
6304
- log.warn(TAG27, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
7082
+ log.warn(TAG29, `run ${run.runId} claims live daemon pid ${run.daemonPid} — skipping`);
6305
7083
  outcome.actions.push("skipped: daemon pid still alive");
6306
7084
  continue;
6307
7085
  }
6308
- log.info(TAG27, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
7086
+ log.info(TAG29, `recovering ${run.pipeline} run ${run.runId} for card #${run.cardShortId}`);
6309
7087
  await recoverRun(run, store, client, config, outcome);
6310
7088
  }
6311
7089
  return outcomes;
@@ -6323,7 +7101,7 @@ async function recoverRun(run, store, client, config, outcome) {
6323
7101
  } catch (err) {
6324
7102
  const msg = err instanceof Error ? err.message : String(err);
6325
7103
  outcome.errors.push(`endAgentSession: ${msg}`);
6326
- log.warn(TAG27, `endAgentSession failed for ${run.cardId}: ${msg}`);
7104
+ log.warn(TAG29, `endAgentSession failed for ${run.cardId}: ${msg}`);
6327
7105
  }
6328
7106
  const card = await fetchCardSafely(client, run.cardId);
6329
7107
  if (card) {
@@ -6366,9 +7144,9 @@ async function recoverRun(run, store, client, config, outcome) {
6366
7144
  const msg = err instanceof Error ? err.message : String(err);
6367
7145
  outcome.errors.push(`endRun: ${msg}`);
6368
7146
  }
6369
- log.info(TAG27, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
7147
+ log.info(TAG29, `recovered run ${run.runId} (card #${run.cardShortId}): ${outcome.actions.join(", ")}${outcome.errors.length ? ` | errors: ${outcome.errors.join("; ")}` : ""}`);
6370
7148
  }
6371
- var TAG27 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
7149
+ var TAG29 = "recovery", RECOVERED_LABEL = "agent-recovered", RECOVERED_LABEL_COLOR = "#f59e0b";
6372
7150
  var init_recovery = __esm(() => {
6373
7151
  init_board_helpers();
6374
7152
  init_log();
@@ -6416,7 +7194,7 @@ class Reconciler {
6416
7194
  clearInterval(this.timer);
6417
7195
  this.timer = null;
6418
7196
  }
6419
- log.info(TAG28, "Heartbeat stopped");
7197
+ log.info(TAG30, "Heartbeat stopped");
6420
7198
  }
6421
7199
  async recoverStaleRuns() {
6422
7200
  if (!this.stateStore || !this.agentConfig)
@@ -6433,7 +7211,7 @@ class Reconciler {
6433
7211
  if (!daemonDead && !(heartbeatStale && ourZombie))
6434
7212
  continue;
6435
7213
  const reason = daemonDead ? `foreign daemon ${run.daemonPid} is dead` : `our worker lost card ${run.cardId} with ${Math.round((now - run.lastHeartbeatAt) / 1000)}s stale heartbeat`;
6436
- log.warn(TAG28, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
7214
+ log.warn(TAG30, `zombie run ${run.runId} (#${run.cardShortId}): ${reason} — recovering`);
6437
7215
  await recoverRun(run, this.stateStore, this.client, this.agentConfig, {
6438
7216
  runId: run.runId,
6439
7217
  cardId: run.cardId,
@@ -6460,11 +7238,11 @@ class Reconciler {
6460
7238
  const stalledAt = Date.parse(card.updated_at ?? "");
6461
7239
  if (!Number.isFinite(stalledAt) || now - stalledAt < graceMs)
6462
7240
  continue;
6463
- log.warn(TAG28, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
7241
+ log.warn(TAG30, `#${card.short_id} stranded in "${inProgressCol.name}" (no live run) — requeueing to "${pickupCol.name}"`);
6464
7242
  try {
6465
7243
  await this.client.moveCard(card.id, pickupCol.id);
6466
7244
  } catch (err) {
6467
- log.error(TAG28, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7245
+ log.error(TAG30, `stranded requeue failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6468
7246
  }
6469
7247
  }
6470
7248
  }
@@ -6487,11 +7265,11 @@ class Reconciler {
6487
7265
  const parkedAt = Date.parse(card.updated_at ?? "");
6488
7266
  if (!Number.isFinite(parkedAt) || now - parkedAt < ttlMs)
6489
7267
  continue;
6490
- log.warn(TAG28, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
7268
+ log.warn(TAG30, `#${card.short_id} parked for approval > ${planning.approvalTtlHours}h — auto-releasing to "${pickupCol.name}"`);
6491
7269
  try {
6492
7270
  await this.client.moveCard(card.id, pickupCol.id);
6493
7271
  } catch (err) {
6494
- log.error(TAG28, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
7272
+ log.error(TAG30, `auto-release failed for #${card.short_id}: ${err instanceof Error ? err.message : err}`);
6495
7273
  }
6496
7274
  }
6497
7275
  }
@@ -6520,18 +7298,18 @@ class Reconciler {
6520
7298
  const subtasks = card.subtasks ?? [];
6521
7299
  const mode = reviewColumnIds.has(card.column_id) ? "review" : "implement";
6522
7300
  if (mode === "review" && this.approvedLabel && hasLabel(cardLabels, this.approvedLabel)) {
6523
- log.debug(TAG28, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
7301
+ log.debug(TAG30, `Skipping #${card.short_id} — already has "${this.approvedLabel}" label`);
6524
7302
  continue;
6525
7303
  }
6526
7304
  if (mode === "review" && hasLabel(cardLabels, NEED_REVIEW_LABEL)) {
6527
- log.debug(TAG28, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
7305
+ log.debug(TAG30, `Skipping #${card.short_id} — has "${NEED_REVIEW_LABEL}" label (needs human)`);
6528
7306
  continue;
6529
7307
  }
6530
7308
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
6531
- log.debug(TAG28, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
7309
+ log.debug(TAG30, `Skipping #${card.short_id} — no branch reference (not qualified for auto-review)`);
6532
7310
  continue;
6533
7311
  }
6534
- log.info(TAG28, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
7312
+ log.info(TAG30, `Missed assignment: #${card.short_id} "${card.title}" (${mode}) — enqueueing`);
6535
7313
  await this.pool.enqueue(card, column, cardLabels, subtasks, mode);
6536
7314
  }
6537
7315
  }
@@ -6541,18 +7319,18 @@ class Reconciler {
6541
7319
  await this.recoverStrandedInProgress(cards, columns, knownCardIds);
6542
7320
  for (const knownId of knownCardIds) {
6543
7321
  if (!allAgentCardIds.has(knownId)) {
6544
- log.info(TAG28, `Missed unassign: ${knownId} — removing`);
7322
+ log.info(TAG30, `Missed unassign: ${knownId} — removing`);
6545
7323
  await this.pool.removeCard(knownId);
6546
7324
  }
6547
7325
  }
6548
7326
  await this.releaseStalledApprovals(cards, columns, knownCardIds);
6549
- log.debug(TAG28, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
7327
+ log.debug(TAG30, `Reconciled: ${assignedCards.length} assigned, ${knownCardIds.size} known`);
6550
7328
  } catch (err) {
6551
- log.error(TAG28, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
7329
+ log.error(TAG30, `Heartbeat failed: ${err instanceof Error ? err.message : err}`);
6552
7330
  }
6553
7331
  }
6554
7332
  }
6555
- var TAG28 = "reconcile";
7333
+ var TAG30 = "reconcile";
6556
7334
  var init_reconcile = __esm(() => {
6557
7335
  init_board_helpers();
6558
7336
  init_log();
@@ -6590,7 +7368,7 @@ function prettyBanner(config, version) {
6590
7368
  checks.push({ kind: "ok", message });
6591
7369
  },
6592
7370
  warn(message) {
6593
- log.warn(TAG29, message);
7371
+ log.warn(TAG31, message);
6594
7372
  checks.push({ kind: "warn", message: message.split(`
6595
7373
  `, 1)[0] });
6596
7374
  },
@@ -6615,25 +7393,25 @@ function prettyBanner(config, version) {
6615
7393
  };
6616
7394
  }
6617
7395
  function jsonBanner(config, version) {
6618
- log.info(TAG29, `Harmony Agent Daemon v${version} starting...`);
6619
- log.info(TAG29, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
7396
+ log.info(TAG31, `Harmony Agent Daemon v${version} starting...`);
7397
+ log.info(TAG31, `Project: ${config.projectId} | Pool: ${config.agent.poolSize} | Model: ${config.agent.claude.model} | Runner: ${config.agent.runner} | Pickup: ${config.agent.pickupColumns.join(", ")}`);
6620
7398
  if (config.agent.review.enabled) {
6621
- log.info(TAG29, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
7399
+ log.info(TAG31, `Review: enabled | Columns: ${config.agent.review.pickupColumns.join(", ")} | → ${config.agent.review.moveToColumn} / ${config.agent.review.failColumn}`);
6622
7400
  }
6623
7401
  let failed = false;
6624
7402
  return {
6625
7403
  setProjectName(_name) {},
6626
7404
  setGitProvider(provider) {
6627
- log.info(TAG29, `Git provider: ${provider}`);
7405
+ log.info(TAG31, `Git provider: ${provider}`);
6628
7406
  },
6629
7407
  setHttpPort(port) {
6630
- log.info(TAG29, `HTTP server on port ${port}`);
7408
+ log.info(TAG31, `HTTP server on port ${port}`);
6631
7409
  },
6632
7410
  check(message) {
6633
- log.info(TAG29, message);
7411
+ log.info(TAG31, message);
6634
7412
  },
6635
7413
  warn(message) {
6636
- log.warn(TAG29, message);
7414
+ log.warn(TAG31, message);
6637
7415
  },
6638
7416
  fail() {
6639
7417
  failed = true;
@@ -6641,7 +7419,7 @@ function jsonBanner(config, version) {
6641
7419
  async ready(message) {
6642
7420
  if (failed)
6643
7421
  return;
6644
- log.info(TAG29, message);
7422
+ log.info(TAG31, message);
6645
7423
  }
6646
7424
  };
6647
7425
  }
@@ -6687,6 +7465,7 @@ function configRows(config, projectName, gitProvider, httpPort) {
6687
7465
  label: "Model",
6688
7466
  value: `${modelDesc} · ${poolDesc} · ${flowDesc}`
6689
7467
  });
7468
+ rows.push({ label: "Runner", value: runnerDesc(config.agent.runner) });
6690
7469
  const tail = [];
6691
7470
  if (gitProvider)
6692
7471
  tail.push(gitProvider);
@@ -6704,6 +7483,9 @@ function titleRule(title) {
6704
7483
  const suffix = "─".repeat(Math.max(3, RULE_WIDTH - prefix.length - title.length - surround.length));
6705
7484
  return dim(`${prefix}${title}${surround}${suffix}`);
6706
7485
  }
7486
+ function runnerDesc(runner) {
7487
+ return runner === "sdk" ? "sdk (Agent SDK)" : "cli (Claude CLI)";
7488
+ }
6707
7489
  function shortenId(id) {
6708
7490
  if (id.length <= 8)
6709
7491
  return id;
@@ -6718,7 +7500,7 @@ function cyan(s) {
6718
7500
  function yellow(s) {
6719
7501
  return `${ANSI.yellow}${s}${ANSI.reset}`;
6720
7502
  }
6721
- var TAG29 = "daemon", RULE_WIDTH = 70, ANSI;
7503
+ var TAG31 = "daemon", RULE_WIDTH = 70, ANSI;
6722
7504
  var init_startup_banner = __esm(() => {
6723
7505
  init_log();
6724
7506
  ANSI = {
@@ -6865,18 +7647,18 @@ class Watcher {
6865
7647
  }
6866
7648
  async start() {
6867
7649
  if (!isPretty()) {
6868
- log.info(TAG30, "Connecting to Supabase realtime (broadcast)...");
7650
+ log.info(TAG32, "Connecting to Supabase realtime (broadcast)...");
6869
7651
  }
6870
7652
  this.supabase = createClient(this.credentials.supabaseUrl, this.credentials.supabaseAnonKey);
6871
7653
  const presenceChannel = this.supabase.channel(`board-presence-${this.projectId}`);
6872
7654
  const channel = this.supabase.channel(`board-${this.projectId}`).on("broadcast", { event: "card_update" }, (msg) => {
6873
- log.debug(TAG30, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
7655
+ log.debug(TAG32, `Broadcast: card_update ${JSON.stringify(msg.payload)}`);
6874
7656
  this.onCardBroadcast({
6875
7657
  event: "card_update",
6876
7658
  payload: msg.payload ?? {}
6877
7659
  });
6878
7660
  }).on("broadcast", { event: "card_created" }, (msg) => {
6879
- log.debug(TAG30, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
7661
+ log.debug(TAG32, `Broadcast: card_created ${JSON.stringify(msg.payload)}`);
6880
7662
  this.onCardBroadcast({
6881
7663
  event: "card_created",
6882
7664
  payload: msg.payload ?? {}
@@ -6886,29 +7668,29 @@ class Watcher {
6886
7668
  const cardId = payload.card_id;
6887
7669
  const command = payload.command;
6888
7670
  if (cardId && command) {
6889
- log.info(TAG30, `Broadcast: agent_command ${command} for ${cardId}`);
7671
+ log.info(TAG32, `Broadcast: agent_command ${command} for ${cardId}`);
6890
7672
  this.onAgentCommand?.({ cardId, command });
6891
7673
  }
6892
7674
  }).subscribe((status) => {
6893
7675
  if (status === "SUBSCRIBED") {
6894
7676
  this.connected = true;
6895
7677
  if (!isPretty() || !this.suppressStartupLogs) {
6896
- log.info(TAG30, "Broadcast subscription active");
7678
+ log.info(TAG32, "Broadcast subscription active");
6897
7679
  }
6898
7680
  this.maybeResolveReady();
6899
7681
  } else if (status === "CHANNEL_ERROR") {
6900
7682
  this.connected = false;
6901
- log.error(TAG30, "Broadcast channel error — will rely on reconciliation");
7683
+ log.error(TAG32, "Broadcast channel error — will rely on reconciliation");
6902
7684
  } else if (status === "TIMED_OUT") {
6903
7685
  this.connected = false;
6904
- log.warn(TAG30, "Broadcast subscription timed out — retrying...");
7686
+ log.warn(TAG32, "Broadcast subscription timed out — retrying...");
6905
7687
  } else if (status === "CLOSED") {
6906
7688
  this.connected = false;
6907
7689
  }
6908
7690
  });
6909
7691
  this.channel = channel;
6910
7692
  presenceChannel.on("presence", { event: "sync" }, () => {
6911
- log.debug(TAG30, "Presence sync");
7693
+ log.debug(TAG32, "Presence sync");
6912
7694
  }).subscribe(async (status) => {
6913
7695
  if (status === "SUBSCRIBED") {
6914
7696
  await presenceChannel.track({
@@ -6921,7 +7703,7 @@ class Watcher {
6921
7703
  agentName: this.identity.agentName
6922
7704
  });
6923
7705
  if (!isPretty() || !this.suppressStartupLogs) {
6924
- log.info(TAG30, "Presence tracked on board-presence channel");
7706
+ log.info(TAG32, "Presence tracked on board-presence channel");
6925
7707
  }
6926
7708
  this.presenceTracked = true;
6927
7709
  this.maybeResolveReady();
@@ -6943,10 +7725,10 @@ class Watcher {
6943
7725
  this.supabase = null;
6944
7726
  }
6945
7727
  this.connected = false;
6946
- log.info(TAG30, "Broadcast subscription stopped");
7728
+ log.info(TAG32, "Broadcast subscription stopped");
6947
7729
  }
6948
7730
  }
6949
- var TAG30 = "watcher";
7731
+ var TAG32 = "watcher";
6950
7732
  var init_watcher = __esm(() => {
6951
7733
  init_log();
6952
7734
  });
@@ -6959,7 +7741,7 @@ __export(exports_worktree_gc, {
6959
7741
  isTransientGitNetworkError: () => isTransientGitNetworkError,
6960
7742
  WorktreeGc: () => WorktreeGc
6961
7743
  });
6962
- import { execFileSync as execFileSync10 } from "node:child_process";
7744
+ import { execFileSync as execFileSync11 } from "node:child_process";
6963
7745
  import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
6964
7746
  import { resolve as resolve3 } from "node:path";
6965
7747
  function isTransientGitNetworkError(message) {
@@ -7027,16 +7809,16 @@ function runWorktreeGc(basePath, store, opts = {}) {
7027
7809
  }
7028
7810
  }
7029
7811
  try {
7030
- execFileSync10("git", ["worktree", "prune", "--expire=now"], {
7812
+ execFileSync11("git", ["worktree", "prune", "--expire=now"], {
7031
7813
  cwd: repoRoot,
7032
7814
  stdio: "pipe"
7033
7815
  });
7034
7816
  } catch {}
7035
7817
  if (result.removed.length > 0) {
7036
- log.info(TAG31, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
7818
+ log.info(TAG33, `GC removed ${result.removed.length} orphan worktree(s): ${result.removed.map((p) => p.split("/").pop()).join(", ")}`);
7037
7819
  }
7038
7820
  if (result.errors.length > 0) {
7039
- log.warn(TAG31, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
7821
+ log.warn(TAG33, `GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.path}: ${e.error}`).join("; ")}`);
7040
7822
  }
7041
7823
  return result;
7042
7824
  }
@@ -7058,7 +7840,7 @@ function pruneFailedRemoteBranches(opts) {
7058
7840
  return result;
7059
7841
  }
7060
7842
  try {
7061
- execFileSync10("git", ["fetch", "--prune", "origin"], {
7843
+ execFileSync11("git", ["fetch", "--prune", "origin"], {
7062
7844
  cwd: repoRoot,
7063
7845
  stdio: "pipe",
7064
7846
  ...GIT_NETWORK_EXEC
@@ -7066,7 +7848,7 @@ function pruneFailedRemoteBranches(opts) {
7066
7848
  } catch (err) {
7067
7849
  const detail = gitErrorDetail(err);
7068
7850
  if (isTransientGitNetworkError(detail)) {
7069
- log.debug(TAG31, `Remote branch GC skipped — remote unreachable: ${detail}`);
7851
+ log.debug(TAG33, `Remote branch GC skipped — remote unreachable: ${detail}`);
7070
7852
  return result;
7071
7853
  }
7072
7854
  result.errors.push({ ref: "fetch", error: detail });
@@ -7074,7 +7856,7 @@ function pruneFailedRemoteBranches(opts) {
7074
7856
  const refPattern = `refs/remotes/origin/${opts.prefix}*`;
7075
7857
  let listing = "";
7076
7858
  try {
7077
- listing = execFileSync10("git", [
7859
+ listing = execFileSync11("git", [
7078
7860
  "for-each-ref",
7079
7861
  "--format=%(refname:strip=3) %(committerdate:unix)",
7080
7862
  refPattern
@@ -7105,11 +7887,11 @@ function pruneFailedRemoteBranches(opts) {
7105
7887
  continue;
7106
7888
  }
7107
7889
  if (clock() > sweepDeadline) {
7108
- log.debug(TAG31, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7890
+ log.debug(TAG33, `Remote branch GC budget spent — removed ${result.removed.length}, remaining deferred to next tick`);
7109
7891
  break;
7110
7892
  }
7111
7893
  try {
7112
- execFileSync10("git", ["push", "origin", `:refs/heads/${ref}`], {
7894
+ execFileSync11("git", ["push", "origin", `:refs/heads/${ref}`], {
7113
7895
  cwd: repoRoot,
7114
7896
  stdio: "pipe",
7115
7897
  ...GIT_NETWORK_EXEC
@@ -7118,17 +7900,17 @@ function pruneFailedRemoteBranches(opts) {
7118
7900
  } catch (err) {
7119
7901
  const detail = gitErrorDetail(err);
7120
7902
  if (isTransientGitNetworkError(detail)) {
7121
- log.debug(TAG31, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7903
+ log.debug(TAG33, `Remote branch GC interrupted — remote unreachable: ${detail}`);
7122
7904
  break;
7123
7905
  }
7124
7906
  result.errors.push({ ref, error: detail });
7125
7907
  }
7126
7908
  }
7127
7909
  if (result.removed.length > 0) {
7128
- log.info(TAG31, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7910
+ log.info(TAG33, `Pruned ${result.removed.length} stale remote branch(es) under ${opts.prefix}: ${result.removed.join(", ")}`);
7129
7911
  }
7130
7912
  if (result.errors.length > 0) {
7131
- log.warn(TAG31, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
7913
+ log.warn(TAG33, `Remote branch GC had ${result.errors.length} error(s): ${result.errors.map((e) => `${e.ref}: ${e.error}`).join("; ")}`);
7132
7914
  }
7133
7915
  return result;
7134
7916
  }
@@ -7159,27 +7941,27 @@ class WorktreeGc {
7159
7941
  try {
7160
7942
  runWorktreeGc(this.basePath, this.store);
7161
7943
  } catch (err) {
7162
- log.warn(TAG31, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7944
+ log.warn(TAG33, `GC tick failed: ${err instanceof Error ? err.message : err}`);
7163
7945
  }
7164
7946
  if (this.remoteOpts) {
7165
7947
  try {
7166
7948
  pruneFailedRemoteBranches(this.remoteOpts);
7167
7949
  } catch (err) {
7168
- log.warn(TAG31, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7950
+ log.warn(TAG33, `Remote GC tick failed: ${err instanceof Error ? err.message : err}`);
7169
7951
  }
7170
7952
  }
7171
7953
  }
7172
7954
  }
7173
7955
  function getRepoRoot2() {
7174
7956
  try {
7175
- return execFileSync10("git", ["rev-parse", "--show-toplevel"], {
7957
+ return execFileSync11("git", ["rev-parse", "--show-toplevel"], {
7176
7958
  encoding: "utf-8"
7177
7959
  }).trim();
7178
7960
  } catch {
7179
7961
  return null;
7180
7962
  }
7181
7963
  }
7182
- var TAG31 = "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;
7964
+ var TAG33 = "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;
7183
7965
  var init_worktree_gc = __esm(() => {
7184
7966
  init_log();
7185
7967
  init_worktree();
@@ -7213,12 +7995,12 @@ __export(exports_src, {
7213
7995
  validatePrerequisites: () => validatePrerequisites,
7214
7996
  main: () => main
7215
7997
  });
7216
- import { execFileSync as execFileSync11 } from "node:child_process";
7998
+ import { execFileSync as execFileSync12 } from "node:child_process";
7217
7999
  import { randomUUID as randomUUID2 } from "node:crypto";
7218
8000
  import { createRequire as createRequire2 } from "node:module";
7219
8001
  async function validatePrerequisites(config, banner) {
7220
8002
  try {
7221
- const ver = execFileSync11("claude", ["--version"], {
8003
+ const ver = execFileSync12("claude", ["--version"], {
7222
8004
  encoding: "utf-8"
7223
8005
  }).trim();
7224
8006
  banner.check(`Claude CLI ${ver}`);
@@ -7233,14 +8015,14 @@ async function validatePrerequisites(config, banner) {
7233
8015
  validateGitProviderCli(provider);
7234
8016
  }
7235
8017
  try {
7236
- const status = execFileSync11("git", ["status", "--porcelain"], {
8018
+ const status = execFileSync12("git", ["status", "--porcelain"], {
7237
8019
  encoding: "utf-8"
7238
8020
  }).trim();
7239
8021
  if (status) {
7240
8022
  banner.warn(`Working directory has uncommitted changes:
7241
8023
  ${status}`);
7242
8024
  }
7243
- execFileSync11("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
8025
+ execFileSync12("git", ["rev-parse", "--verify", `origin/${config.agent.worktree.baseBranch}`], {
7244
8026
  encoding: "utf-8",
7245
8027
  stdio: "pipe"
7246
8028
  });
@@ -7283,7 +8065,7 @@ async function main() {
7283
8065
  } catch (err) {
7284
8066
  if (err instanceof ConfigValidationError) {
7285
8067
  banner.fail();
7286
- log.error(TAG32, err.message);
8068
+ log.error(TAG34, err.message);
7287
8069
  process.exit(1);
7288
8070
  }
7289
8071
  throw err;
@@ -7351,6 +8133,7 @@ async function main() {
7351
8133
  daemonPid: process.pid,
7352
8134
  startedAt,
7353
8135
  uptimeMs: Date.now() - startedAt,
8136
+ runner: config.agent.runner,
7354
8137
  workers: pool.snapshotWorkers().map((w) => ({
7355
8138
  ...w,
7356
8139
  phase: null
@@ -7392,7 +8175,7 @@ async function main() {
7392
8175
  if (shuttingDown)
7393
8176
  return;
7394
8177
  shuttingDown = true;
7395
- log.info(TAG32, `Received ${signal}, shutting down gracefully...`);
8178
+ log.info(TAG34, `Received ${signal}, shutting down gracefully...`);
7396
8179
  reconciler.stop();
7397
8180
  mergeMonitor?.stop();
7398
8181
  worktreeGc.stop();
@@ -7402,18 +8185,18 @@ async function main() {
7402
8185
  }
7403
8186
  await watcher.stop();
7404
8187
  await pool.shutdown();
7405
- log.info(TAG32, "Daemon stopped.");
8188
+ log.info(TAG34, "Daemon stopped.");
7406
8189
  process.exit(exitCode);
7407
8190
  };
7408
8191
  process.on("SIGINT", () => shutdown("SIGINT"));
7409
8192
  process.on("SIGTERM", () => shutdown("SIGTERM"));
7410
8193
  process.on("uncaughtException", (err) => {
7411
- log.error(TAG32, `Uncaught exception: ${err.message}`);
8194
+ log.error(TAG34, `Uncaught exception: ${err.message}`);
7412
8195
  exitCode = 1;
7413
8196
  shutdown("uncaughtException");
7414
8197
  });
7415
8198
  process.on("unhandledRejection", (reason) => {
7416
- log.error(TAG32, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
8199
+ log.error(TAG34, `Unhandled rejection: ${reason instanceof Error ? reason.message : String(reason)}`);
7417
8200
  exitCode = 1;
7418
8201
  shutdown("unhandledRejection");
7419
8202
  });
@@ -7466,35 +8249,35 @@ async function handleBroadcast(event, client, pool, config, agentId) {
7466
8249
  if (assignedAgentId === undefined)
7467
8250
  return;
7468
8251
  if (assignedAgentId === agentId) {
7469
- log.info(TAG32, `Broadcast: card ${cardId} assigned to agent`);
8252
+ log.info(TAG34, `Broadcast: card ${cardId} assigned to agent`);
7470
8253
  try {
7471
8254
  await pool.resetAttemptsForReassign(cardId);
7472
8255
  await tryEnqueueCard(cardId, client, pool, config, agentId);
7473
8256
  } catch (err) {
7474
- log.error(TAG32, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
8257
+ log.error(TAG34, `Failed to process assignment: ${err instanceof Error ? err.message : err}`);
7475
8258
  }
7476
8259
  } else if (pool.isCardKnown(cardId)) {
7477
- log.info(TAG32, `Broadcast: card ${cardId} unassigned from agent`);
8260
+ log.info(TAG34, `Broadcast: card ${cardId} unassigned from agent`);
7478
8261
  await pool.removeCard(cardId);
7479
8262
  }
7480
8263
  }
7481
8264
  async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7482
8265
  const { card } = await client.getCard(cardId);
7483
8266
  if (card.assigned_agent_id !== agentId) {
7484
- log.debug(TAG32, `Card ${cardId} no longer assigned to agent — skipping`);
8267
+ log.debug(TAG34, `Card ${cardId} no longer assigned to agent — skipping`);
7485
8268
  return;
7486
8269
  }
7487
8270
  const board = await client.getBoard(config.projectId, { summary: true });
7488
8271
  const columns = board.columns;
7489
8272
  const column = columns.find((c) => c.id === card.column_id);
7490
8273
  if (!column) {
7491
- log.warn(TAG32, `Column not found for card ${cardId}`);
8274
+ log.warn(TAG34, `Column not found for card ${cardId}`);
7492
8275
  return;
7493
8276
  }
7494
8277
  const isPickupColumn = config.agent.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7495
8278
  const isReviewColumn = config.agent.review.enabled && config.agent.review.pickupColumns.some((name) => name.toLowerCase() === column.name.toLowerCase());
7496
8279
  if (!isPickupColumn && !isReviewColumn) {
7497
- log.info(TAG32, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
8280
+ log.info(TAG34, `Card #${card.short_id} is in "${column.name}", not a pickup/review column — skipping`);
7498
8281
  return;
7499
8282
  }
7500
8283
  const mode = isReviewColumn ? "review" : "implement";
@@ -7502,16 +8285,16 @@ async function tryEnqueueCard(cardId, client, pool, config, agentId) {
7502
8285
  const cardLabels = resolveCardLabels(card, labelMap);
7503
8286
  const subtasks = card.subtasks ?? [];
7504
8287
  if (mode === "review" && config.agent.review.approvedLabel && hasLabel(cardLabels, config.agent.review.approvedLabel)) {
7505
- log.debug(TAG32, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
8288
+ log.debug(TAG34, `Card #${card.short_id} already has "${config.agent.review.approvedLabel}" — skipping review`);
7506
8289
  return;
7507
8290
  }
7508
8291
  if (mode === "review" && !extractBranchFromDescription(card.description)) {
7509
- log.info(TAG32, `Card #${card.short_id} has no branch reference — skipping auto-review`);
8292
+ log.info(TAG34, `Card #${card.short_id} has no branch reference — skipping auto-review`);
7510
8293
  return;
7511
8294
  }
7512
8295
  await pool.enqueue(card, column, cardLabels, subtasks, mode);
7513
8296
  }
7514
- var TAG32 = "daemon", PKG_VERSION;
8297
+ var TAG34 = "daemon", PKG_VERSION;
7515
8298
  var init_src = __esm(() => {
7516
8299
  init_board_helpers();
7517
8300
  init_config();