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