jonah-fleet 1.5.0 → 1.6.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.
package/dist/index.js CHANGED
@@ -92,7 +92,7 @@ var ROUTINE_TO_WORKFLOW_MAP = {
92
92
  "product-planning": [],
93
93
  "analytics-review": []
94
94
  };
95
- var FLEET_VERSION = "1.5.0";
95
+ var FLEET_VERSION = "1.6.0";
96
96
  var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
97
97
 
98
98
  // src/lib/manifest.ts
@@ -1759,6 +1759,8 @@ function parseLogToTelemetry(content, options = {}) {
1759
1759
  failureCategory = "infeasible";
1760
1760
  } else if (lower.includes("conflict")) {
1761
1761
  failureCategory = "merge_conflict";
1762
+ } else if (lower.includes("ambiguous") || lower.includes("needs-info") || lower.includes("clarification") || lower.includes("acceptance criteria")) {
1763
+ failureCategory = "ambiguous_spec";
1762
1764
  }
1763
1765
  }
1764
1766
  const inputTokens = parseInt((metadata["input tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
@@ -1783,6 +1785,16 @@ function parseLogToTelemetry(content, options = {}) {
1783
1785
  }
1784
1786
  }
1785
1787
  const promptSha = metadata["prompt sha"];
1788
+ const lowerContent = content.toLowerCase();
1789
+ const ambiguityGateTriggered = lowerContent.includes("ambiguity & missing acceptance criteria gate") || lowerContent.includes("ambiguity gate") || lowerContent.includes("clarifications needed before implementation") || lowerContent.includes("needs-info") && (lowerContent.includes("clarification") || lowerContent.includes("questions"));
1790
+ const needsInfoApplied = lowerContent.includes("needs-info");
1791
+ let questionsAskedCount;
1792
+ if (ambiguityGateTriggered) {
1793
+ const questionLines = content.split("\n").filter(
1794
+ (line) => (line.trim().startsWith("1.") || line.trim().startsWith("2.") || line.trim().startsWith("3.") || line.trim().startsWith("-")) && line.includes("?")
1795
+ );
1796
+ questionsAskedCount = questionLines.length > 0 ? questionLines.length : 2;
1797
+ }
1786
1798
  return {
1787
1799
  schemaVersion: "1.0.0",
1788
1800
  routine,
@@ -1800,7 +1812,10 @@ function parseLogToTelemetry(content, options = {}) {
1800
1812
  durationSeconds,
1801
1813
  iterationsUsed,
1802
1814
  maxIterations,
1803
- promptSha
1815
+ promptSha,
1816
+ ambiguityGateTriggered: ambiguityGateTriggered || void 0,
1817
+ questionsAskedCount,
1818
+ needsInfoApplied: needsInfoApplied || void 0
1804
1819
  };
1805
1820
  }
1806
1821
  function checkWeeklyBudgetLimit(usedTokens, ceilingTokens = GLOBAL_WEEKLY_TOKEN_BUDGET) {
@@ -1859,7 +1874,10 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1859
1874
  failureCount: 0,
1860
1875
  bouncedCount: 0,
1861
1876
  avgDurationSeconds: 0,
1862
- avgIterationsUsed: 0
1877
+ avgIterationsUsed: 0,
1878
+ ambiguityGatesTriggered: 0,
1879
+ questionsAskedCount: 0,
1880
+ needsInfoAppliedCount: 0
1863
1881
  };
1864
1882
  }
1865
1883
  const r = byRoutine[s.routine];
@@ -1877,6 +1895,9 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1877
1895
  if (s.iterationsUsed) {
1878
1896
  r.avgIterationsUsed = (r.avgIterationsUsed * (r.runCount - 1) + s.iterationsUsed) / r.runCount;
1879
1897
  }
1898
+ if (s.ambiguityGateTriggered) r.ambiguityGatesTriggered++;
1899
+ if (s.questionsAskedCount) r.questionsAskedCount += s.questionsAskedCount;
1900
+ if (s.needsInfoApplied) r.needsInfoAppliedCount++;
1880
1901
  if (!byRepository[s.repository]) {
1881
1902
  byRepository[s.repository] = {
1882
1903
  repository: s.repository,
@@ -1894,6 +1915,21 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1894
1915
  if (s.result === "SUCCESS") repoObj.successCount++;
1895
1916
  else if (s.result === "FAILURE") repoObj.failureCount++;
1896
1917
  }
1918
+ let totalAmbiguityGatesTriggered = 0;
1919
+ let totalQuestionsAsked = 0;
1920
+ let totalNeedsInfoApplied = 0;
1921
+ for (const s of summaries) {
1922
+ if (s.ambiguityGateTriggered) totalAmbiguityGatesTriggered++;
1923
+ if (s.questionsAskedCount) totalQuestionsAsked += s.questionsAskedCount;
1924
+ if (s.needsInfoApplied) totalNeedsInfoApplied++;
1925
+ }
1926
+ const estimatedTokensSaved = totalAmbiguityGatesTriggered * 5e4;
1927
+ const ambiguity = {
1928
+ totalAmbiguityGatesTriggered,
1929
+ totalQuestionsAsked,
1930
+ needsInfoAppliedCount: totalNeedsInfoApplied,
1931
+ estimatedTokensSaved
1932
+ };
1897
1933
  const totalTokens = totalInputTokens + totalOutputTokens;
1898
1934
  const budget = checkWeeklyBudgetLimit(totalTokens, budgetCeiling);
1899
1935
  return {
@@ -1910,6 +1946,7 @@ function aggregateFleetTelemetry(summaries, options = {}) {
1910
1946
  byRoutine,
1911
1947
  byRepository,
1912
1948
  failureCategories,
1949
+ ambiguity,
1913
1950
  events: summaries
1914
1951
  };
1915
1952
  }
@@ -2046,6 +2083,20 @@ function renderTelemetryDashboard(telemetry, options = {}) {
2046
2083
  );
2047
2084
  }
2048
2085
  }
2086
+ const amb = telemetry.ambiguity;
2087
+ lines.push("\n" + pc7.bold("\u2753 Inquisitive Stance & Ambiguity Gate Signals:"));
2088
+ lines.push(
2089
+ ` \u2022 Ambiguity Gate Triggers: ${pc7.bold(amb.totalAmbiguityGatesTriggered.toString())} runs stopped to request clarification`
2090
+ );
2091
+ lines.push(
2092
+ ` \u2022 Clarifying Questions Posed: ${pc7.bold(amb.totalQuestionsAsked.toString())} targeted questions`
2093
+ );
2094
+ lines.push(
2095
+ ` \u2022 Needs-Info Labels Applied: ${pc7.bold(amb.needsInfoAppliedCount.toString())}`
2096
+ );
2097
+ lines.push(
2098
+ ` \u2022 Est. Wasted Tokens Averted: ~${pc7.bold(pc7.green(formatTokens2(amb.estimatedTokensSaved)))} tokens (avoided speculative builds)`
2099
+ );
2049
2100
  const failKeys = Object.keys(telemetry.failureCategories);
2050
2101
  if (failKeys.length > 0) {
2051
2102
  lines.push("\n" + pc7.bold(pc7.red("\u26A0\uFE0F Failure Categories Breakdown:")));
@@ -2289,7 +2340,60 @@ async function cleanupStaleWorktrees(repoRoot) {
2289
2340
  // src/lib/terminal-card.ts
2290
2341
  import fs11 from "fs";
2291
2342
  import path11 from "path";
2343
+ import { execFileSync } from "child_process";
2292
2344
  import pc9 from "picocolors";
2345
+ function stripAnsi(text) {
2346
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
2347
+ }
2348
+ function wrapText(text, maxWidth) {
2349
+ if (maxWidth <= 0) return [text];
2350
+ const words = text.split(/\s+/).filter(Boolean);
2351
+ if (words.length === 0) return [];
2352
+ const lines = [];
2353
+ let current = "";
2354
+ for (const word of words) {
2355
+ if (!current) {
2356
+ current = word;
2357
+ } else {
2358
+ const proposed = current + " " + word;
2359
+ if (stripAnsi(proposed).length <= maxWidth) {
2360
+ current = proposed;
2361
+ } else {
2362
+ lines.push(current);
2363
+ current = word;
2364
+ }
2365
+ }
2366
+ }
2367
+ if (current) lines.push(current);
2368
+ return lines;
2369
+ }
2370
+ function fetchTargetTitle(repoRoot, target) {
2371
+ try {
2372
+ const prMatch = target.match(/PR\s*#?(\d+)/i);
2373
+ if (prMatch) {
2374
+ const stdout = execFileSync("gh", ["pr", "view", prMatch[1], "--json", "title", "-q", ".title"], {
2375
+ cwd: repoRoot,
2376
+ encoding: "utf8",
2377
+ stdio: ["ignore", "pipe", "ignore"],
2378
+ timeout: 4e3
2379
+ });
2380
+ return stdout.trim() || null;
2381
+ }
2382
+ const issueMatch = target.match(/Issue\s*#?(\d+)/i);
2383
+ if (issueMatch) {
2384
+ const stdout = execFileSync("gh", ["issue", "view", issueMatch[1], "--json", "title", "-q", ".title"], {
2385
+ cwd: repoRoot,
2386
+ encoding: "utf8",
2387
+ stdio: ["ignore", "pipe", "ignore"],
2388
+ timeout: 4e3
2389
+ });
2390
+ return stdout.trim() || null;
2391
+ }
2392
+ } catch {
2393
+ return null;
2394
+ }
2395
+ return null;
2396
+ }
2293
2397
  function sanitizeWorktreePaths(text) {
2294
2398
  let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
2295
2399
  cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
@@ -2300,7 +2404,7 @@ function sanitizeWorktreePaths(text) {
2300
2404
  return cleaned;
2301
2405
  }
2302
2406
  function extractExecutionSummary(output) {
2303
- const summaryHeaderRegex = /#\s+([A-Za-z0-9\s_-]+?Execution\s+Summary[\s\S]*)/i;
2407
+ const summaryHeaderRegex = /#{1,3}\s+([A-Za-z0-9\s_-]*?(?:Execution|Review|Autowork)\s+Summary[\s\S]*)/i;
2304
2408
  const match = output.match(summaryHeaderRegex);
2305
2409
  if (!match) return null;
2306
2410
  let summary = match[1].trim();
@@ -2308,8 +2412,8 @@ function extractExecutionSummary(output) {
2308
2412
  "\u2713 Local peer-review completed",
2309
2413
  "\u2713 Local autowork completed",
2310
2414
  "\u2713 Local agent session",
2311
- "[6:",
2312
- "Peer Review Watchdog:"
2415
+ "Peer Review Watchdog:",
2416
+ "Autowork Backlog Scan:"
2313
2417
  ];
2314
2418
  for (const sep of trailingSeparators) {
2315
2419
  const idx = summary.indexOf(sep);
@@ -2317,6 +2421,10 @@ function extractExecutionSummary(output) {
2317
2421
  summary = summary.slice(0, idx).trim();
2318
2422
  }
2319
2423
  }
2424
+ const timestampMatch = summary.match(/\n\s*\[\d{1,2}:\d{2}:\d{2}\s*(?:AM|PM)?\][\s\S]*/);
2425
+ if (timestampMatch && timestampMatch.index !== void 0) {
2426
+ summary = summary.slice(0, timestampMatch.index).trim();
2427
+ }
2320
2428
  return sanitizeWorktreePaths(summary);
2321
2429
  }
2322
2430
  function findLatestRunLog(repoRoot, routine) {
@@ -2337,57 +2445,73 @@ function parseRunLog(logContent) {
2337
2445
  actions: []
2338
2446
  };
2339
2447
  const lines = logContent.split("\n");
2340
- for (const line of lines) {
2341
- const trimmed = line.trim();
2342
- if (trimmed.startsWith("|") && trimmed.includes("|")) {
2343
- const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2344
- if (parts.length >= 2) {
2345
- const key = parts[0].toLowerCase();
2346
- const value = parts[1].replace(/`/g, "");
2347
- if (key.includes("routine")) summary.routine = value;
2348
- if (key.includes("target pr") || key.includes("target issue")) summary.target = value;
2349
- if (key.includes("decision")) summary.decision = value;
2350
- if (key.includes("result")) summary.result = value;
2351
- if (key.includes("duration")) summary.duration = value;
2352
- }
2353
- }
2354
- }
2448
+ let inMetadata = false;
2355
2449
  let inDoD = false;
2356
2450
  let inFindings = false;
2357
2451
  let inActions = false;
2358
2452
  for (const line of lines) {
2359
2453
  const trimmed = line.trim();
2360
- if (trimmed.startsWith("## Definition of Done")) {
2454
+ if (trimmed.startsWith("## Metadata")) {
2455
+ inMetadata = true;
2456
+ inDoD = false;
2457
+ inFindings = false;
2458
+ inActions = false;
2459
+ continue;
2460
+ } else if (trimmed.startsWith("## Definition of Done")) {
2461
+ inMetadata = false;
2361
2462
  inDoD = true;
2362
2463
  inFindings = false;
2363
2464
  inActions = false;
2364
2465
  continue;
2365
2466
  } else if (trimmed.startsWith("## Code Review Findings") || trimmed.startsWith("## Findings")) {
2467
+ inMetadata = false;
2366
2468
  inDoD = false;
2367
2469
  inFindings = true;
2368
2470
  inActions = false;
2369
2471
  continue;
2370
- } else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken")) {
2472
+ } else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken") || trimmed.startsWith("## Artifacts")) {
2473
+ inMetadata = false;
2371
2474
  inDoD = false;
2372
2475
  inFindings = false;
2373
- inActions = true;
2476
+ inActions = trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken");
2374
2477
  continue;
2375
2478
  } else if (trimmed.startsWith("## ")) {
2479
+ inMetadata = false;
2376
2480
  inDoD = false;
2377
2481
  inFindings = false;
2378
2482
  inActions = false;
2379
2483
  }
2484
+ if (inMetadata && trimmed.startsWith("|") && trimmed.includes("|")) {
2485
+ const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2486
+ if (parts.length >= 2) {
2487
+ const key = parts[0].toLowerCase();
2488
+ const value = parts[1].replace(/[`*]/g, "").trim();
2489
+ if (key === "routine") summary.routine = value;
2490
+ if (key === "target pr" || key === "target issue" || key === "target") {
2491
+ summary.target = value;
2492
+ }
2493
+ if (key === "decision") summary.decision = value;
2494
+ if (key === "result") summary.result = value;
2495
+ if (key === "duration") summary.duration = value;
2496
+ if (key === "title" || key === "pr title" || key === "issue title") summary.title = value;
2497
+ }
2498
+ }
2380
2499
  if (inDoD && trimmed.startsWith("|") && !trimmed.includes("Criterion") && !trimmed.includes("---")) {
2381
2500
  const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2382
2501
  if (parts.length >= 2) {
2383
2502
  const criterion = parts[0];
2384
- const met = parts[1].toUpperCase() === "YES" || parts[1].toUpperCase() === "PASS";
2385
- const evidence = parts[2] ? ` (${parts[2].slice(0, 60)}...)` : "";
2386
- summary.passes?.push({
2387
- name: criterion,
2388
- status: met ? "pass" : "fail",
2389
- detail: evidence
2390
- });
2503
+ const metRaw = parts[1].toUpperCase();
2504
+ const met = metRaw === "YES" || metRaw === "PASS";
2505
+ const evidence = parts[2] ? parts[2].trim() : "";
2506
+ const isConditionalScan = criterion.toLowerCase().startsWith("if in scan mode and no eligible");
2507
+ const isNA = evidence.toLowerCase().includes("n/a") || metRaw === "N/A";
2508
+ if (!isConditionalScan && !isNA) {
2509
+ summary.passes?.push({
2510
+ name: criterion,
2511
+ status: met ? "pass" : "fail",
2512
+ detail: evidence ? ` (${evidence})` : ""
2513
+ });
2514
+ }
2391
2515
  }
2392
2516
  }
2393
2517
  if (inActions && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
@@ -2398,9 +2522,15 @@ function parseRunLog(logContent) {
2398
2522
  }
2399
2523
  function detectActivePhase(chunk, currentPhase = "Executing routine") {
2400
2524
  const lower = chunk.toLowerCase();
2525
+ if (lower.includes("\u{1F512} addressing review findings") || lower.includes("addressing review findings by")) {
2526
+ return "Claimed bounced PR, addressing review findings";
2527
+ }
2401
2528
  if (lower.includes("\u{1F512} claimed") || lower.includes("claimed by local autowork") || lower.includes("claimed by autowork")) {
2402
2529
  return "Claimed target issue, starting implementation";
2403
2530
  }
2531
+ if (lower.includes("addressing review findings") || lower.includes("fixing review findings")) {
2532
+ return "Claimed bounced PR, addressing review findings";
2533
+ }
2404
2534
  if (lower.includes("starting review (round")) {
2405
2535
  return "Claimed review window, starting review passes";
2406
2536
  }
@@ -2430,16 +2560,18 @@ function detectClaimedIssue(chunk) {
2430
2560
  return null;
2431
2561
  }
2432
2562
  function detectClaimedPR(chunk) {
2563
+ const findingMatch = chunk.match(/(?:addressing\s+review\s+findings|fixing\s+review\s+findings)[^\n#]*?#(\d+)/i);
2564
+ if (findingMatch) return `PR #${findingMatch[1]}`;
2433
2565
  const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
2434
2566
  if (reviewMatch) return `PR #${reviewMatch[1]}`;
2435
2567
  const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
2436
2568
  if (prMatch) return `PR #${prMatch[1]}`;
2437
- const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review)\s+(\d+)/i);
2569
+ const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready)\s+(\d+)/i);
2438
2570
  if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2439
2571
  return null;
2440
2572
  }
2441
2573
  function renderSummaryCard(options) {
2442
- const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 60), 86);
2574
+ const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
2443
2575
  const horizontal = "\u2500".repeat(width - 2);
2444
2576
  const rawSummary = options.output ? extractExecutionSummary(options.output) : null;
2445
2577
  let parsedFromLog = null;
@@ -2455,7 +2587,28 @@ function renderSummaryCard(options) {
2455
2587
  }
2456
2588
  }
2457
2589
  let target = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : "";
2458
- if (!target && parsedFromLog?.target) target = parsedFromLog.target;
2590
+ if (!target && parsedFromLog?.target && parsedFromLog.target.toUpperCase() !== "YES") {
2591
+ const rawTarget = parsedFromLog.target;
2592
+ target = rawTarget.startsWith("#") ? options.routine === "peer-review" ? `PR ${rawTarget}` : `Issue ${rawTarget}` : rawTarget;
2593
+ }
2594
+ if (!target && options.output) {
2595
+ const targetMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[?PR\s*#?(\d+)\]?/i) || options.output.match(/Starting\s+review[^\n#]*?#(\d+)/i) || options.output.match(/Target(?:ing)?\s+(?:issue|PR)\s*#?(\d+)/i) || options.output.match(/Candidate\s+issue\s*#?(\d+)/i);
2596
+ if (targetMatch) {
2597
+ target = options.routine === "peer-review" ? `PR #${targetMatch[1]}` : `Issue #${targetMatch[1]}`;
2598
+ }
2599
+ }
2600
+ let title = options.title || parsedFromLog?.title || "";
2601
+ if (!title && rawSummary) {
2602
+ const titleMatch = rawSummary.match(/\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || rawSummary.match(/Selected\s+Target\s+PR:?\s*\[.*?\]\([^)]+\)\s*\(([^)]+)\)/i) || rawSummary.match(/PR\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
2603
+ if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
2604
+ }
2605
+ if (!title && options.output) {
2606
+ const titleMatch = options.output.match(/Selected\s+Target\s+PR:?\s*\[PR\s*#?\d+\s*\((`?[^`)]+`?)\)\]/i) || options.output.match(/Selected\s+candidate\s+issue\s*#?\d+[:\s]+`?([^`\n]+)`?/i);
2607
+ if (titleMatch) title = titleMatch[1].replace(/[`*]/g, "").trim();
2608
+ }
2609
+ if (!title && options.repoRoot && target) {
2610
+ title = fetchTargetTitle(options.repoRoot, target) || "";
2611
+ }
2459
2612
  let decision = parsedFromLog?.decision || "";
2460
2613
  if (!decision && rawSummary) {
2461
2614
  const decisionMatch = rawSummary.match(/\*\*Final Action\*\*:\s*([^\n]+)/i);
@@ -2464,15 +2617,26 @@ function renderSummaryCard(options) {
2464
2617
  const durationStr = options.durationMs ? `${Math.round(options.durationMs / 1e3)}s` : parsedFromLog?.duration || "";
2465
2618
  const lines = [];
2466
2619
  lines.push(pc9.cyan(`\u250C${horizontal}\u2510`));
2467
- const title = ` Jonah Fleet Routine: ${pc9.bold(options.routine.toUpperCase())} `;
2620
+ const headerParts = [pc9.bold(pc9.white(options.routine.toUpperCase()))];
2621
+ if (target) headerParts.push(pc9.yellow(target));
2622
+ if (durationStr) headerParts.push(pc9.dim(`(${durationStr})`));
2623
+ const headerContent = headerParts.join(" \xB7 ");
2624
+ const headerPlain = stripAnsi(headerContent);
2468
2625
  lines.push(
2469
- pc9.cyan("\u2502") + ` ${pc9.bold(pc9.white(options.routine.toUpperCase()))}` + (target ? ` \xB7 ${pc9.yellow(target)}` : "") + (durationStr ? pc9.dim(` (${durationStr})`) : "") + " ".repeat(
2470
- Math.max(
2471
- 1,
2472
- width - 4 - options.routine.length - target.length - (durationStr ? durationStr.length + 3 : 0)
2473
- )
2474
- ) + pc9.cyan("\u2502")
2626
+ pc9.cyan("\u2502") + ` ${headerContent}` + " ".repeat(Math.max(1, width - 3 - headerPlain.length)) + pc9.cyan("\u2502")
2475
2627
  );
2628
+ if (title) {
2629
+ const titlePrefix = " Title: ";
2630
+ const wrappedTitle = wrapText(title, width - 4 - titlePrefix.length);
2631
+ for (let i = 0; i < wrappedTitle.length; i++) {
2632
+ const prefix = i === 0 ? pc9.dim(titlePrefix) : " ".repeat(titlePrefix.length);
2633
+ const text = wrappedTitle[i];
2634
+ const plainLen = titlePrefix.length + stripAnsi(text).length;
2635
+ lines.push(
2636
+ pc9.cyan("\u2502") + ` ${prefix}${pc9.white(pc9.bold(text))}` + " ".repeat(Math.max(1, width - 3 - plainLen)) + pc9.cyan("\u2502")
2637
+ );
2638
+ }
2639
+ }
2476
2640
  if (decision) {
2477
2641
  let decisionBadge = pc9.green(`\u2714 ${decision}`);
2478
2642
  if (/bounce|draft|reject|fail/i.test(decision)) {
@@ -2480,8 +2644,9 @@ function renderSummaryCard(options) {
2480
2644
  } else if (/escalat/i.test(decision)) {
2481
2645
  decisionBadge = pc9.red(`\u{1F6A8} ${decision}`);
2482
2646
  }
2647
+ const decisionPlain = ` Action: ${decision}`;
2483
2648
  lines.push(
2484
- pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 11 - decision.length)) + pc9.cyan("\u2502")
2649
+ pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 3 - decisionPlain.length)) + pc9.cyan("\u2502")
2485
2650
  );
2486
2651
  }
2487
2652
  lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
@@ -2503,22 +2668,36 @@ function renderSummaryCard(options) {
2503
2668
  } else if (line.startsWith("- ") || line.startsWith("* ")) {
2504
2669
  const item = sanitizeWorktreePaths(line.slice(2)).trim();
2505
2670
  const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
2506
- const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
2507
- if (plainLen <= width - 6) {
2508
- lines.push(pc9.cyan("\u2502") + ` \u2022 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
2509
- } else {
2510
- const truncated = formatted.slice(0, width - 10) + "...";
2511
- lines.push(pc9.cyan("\u2502") + ` \u2022 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
2671
+ const wrapped = wrapText(formatted, width - 8);
2672
+ for (let i = 0; i < wrapped.length; i++) {
2673
+ const wLine = wrapped[i];
2674
+ const wPlain = stripAnsi(wLine);
2675
+ if (i === 0) {
2676
+ lines.push(
2677
+ pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2678
+ );
2679
+ } else {
2680
+ lines.push(
2681
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2682
+ );
2683
+ }
2512
2684
  }
2513
2685
  } else if (/^[0-9]+\.\s+/.test(line)) {
2514
2686
  const item = sanitizeWorktreePaths(line.replace(/^[0-9]+\.\s+/, "")).trim();
2515
2687
  const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
2516
- const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
2517
- if (plainLen <= width - 6) {
2518
- lines.push(pc9.cyan("\u2502") + ` \u2714 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
2519
- } else {
2520
- const truncated = formatted.slice(0, width - 10) + "...";
2521
- lines.push(pc9.cyan("\u2502") + ` \u2714 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
2688
+ const wrapped = wrapText(formatted, width - 8);
2689
+ for (let i = 0; i < wrapped.length; i++) {
2690
+ const wLine = wrapped[i];
2691
+ const wPlain = stripAnsi(wLine);
2692
+ if (i === 0) {
2693
+ lines.push(
2694
+ pc9.cyan("\u2502") + ` \u2714 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2695
+ );
2696
+ } else {
2697
+ lines.push(
2698
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2699
+ );
2700
+ }
2522
2701
  }
2523
2702
  }
2524
2703
  }
@@ -2526,10 +2705,45 @@ function renderSummaryCard(options) {
2526
2705
  lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Verification Passes:")}` + " ".repeat(Math.max(1, width - 23)) + pc9.cyan("\u2502"));
2527
2706
  for (const pass of parsedFromLog.passes.slice(0, 6)) {
2528
2707
  const icon = pass.status === "pass" ? pc9.green("\u2714") : pc9.red("\u2716");
2529
- const text = `${pass.name}${pass.detail || ""}`;
2530
- const plainLen = text.length + 4;
2531
- const truncated = plainLen > width - 6 ? text.slice(0, width - 10) + "..." : text;
2532
- lines.push(pc9.cyan("\u2502") + ` ${icon} ${truncated}` + " ".repeat(Math.max(1, width - 5 - truncated.length)) + pc9.cyan("\u2502"));
2708
+ let criterionName = pass.name;
2709
+ const colonIdx = criterionName.indexOf(":");
2710
+ if (colonIdx > 10 && colonIdx < 40) {
2711
+ criterionName = criterionName.slice(0, colonIdx);
2712
+ }
2713
+ const passText = `${criterionName}${pass.detail || ""}`;
2714
+ const wrapped = wrapText(passText, width - 8);
2715
+ for (let i = 0; i < wrapped.length; i++) {
2716
+ const wLine = wrapped[i];
2717
+ const wPlain = stripAnsi(wLine);
2718
+ if (i === 0) {
2719
+ lines.push(
2720
+ pc9.cyan("\u2502") + ` ${icon} ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2721
+ );
2722
+ } else {
2723
+ lines.push(
2724
+ pc9.cyan("\u2502") + ` ${pc9.dim(wLine)}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2725
+ );
2726
+ }
2727
+ }
2728
+ }
2729
+ if (parsedFromLog.actions && parsedFromLog.actions.length > 0) {
2730
+ lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Actions Taken:")}` + " ".repeat(Math.max(1, width - 16)) + pc9.cyan("\u2502"));
2731
+ for (const action of parsedFromLog.actions.slice(0, 4)) {
2732
+ const wrapped = wrapText(action, width - 8);
2733
+ for (let i = 0; i < wrapped.length; i++) {
2734
+ const wLine = wrapped[i];
2735
+ const wPlain = stripAnsi(wLine);
2736
+ if (i === 0) {
2737
+ lines.push(
2738
+ pc9.cyan("\u2502") + ` \u2022 ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2739
+ );
2740
+ } else {
2741
+ lines.push(
2742
+ pc9.cyan("\u2502") + ` ${wLine}` + " ".repeat(Math.max(1, width - 5 - wPlain.length)) + pc9.cyan("\u2502")
2743
+ );
2744
+ }
2745
+ }
2746
+ }
2533
2747
  }
2534
2748
  }
2535
2749
  if (parsedFromLog?.logPath) {
@@ -2972,18 +3186,28 @@ function isDaemonRunning(repoRoot) {
2972
3186
  return false;
2973
3187
  }
2974
3188
  }
2975
- async function countOpenReadyPRs(repoRoot) {
3189
+ function filterReviewablePRs(prs) {
3190
+ return (prs || []).filter(
3191
+ (pr) => pr && typeof pr.number === "number" && !pr.headRefName?.startsWith("release-please--") && !pr.title?.startsWith("chore(main): release")
3192
+ );
3193
+ }
3194
+ async function getOpenReviewablePRs(repoRoot) {
2976
3195
  try {
2977
3196
  const { stdout } = await execFileAsync3(
2978
3197
  "gh",
2979
- ["pr", "list", "--state", "open", "--draft=false", "--json", "number", "--jq", "length"],
3198
+ ["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
2980
3199
  { cwd: repoRoot }
2981
3200
  );
2982
- return parseInt(stdout.trim(), 10) || 0;
3201
+ const prs = JSON.parse(stdout);
3202
+ return filterReviewablePRs(prs);
2983
3203
  } catch {
2984
- return 0;
3204
+ return [];
2985
3205
  }
2986
3206
  }
3207
+ async function countOpenReadyPRs(repoRoot) {
3208
+ const prs = await getOpenReviewablePRs(repoRoot);
3209
+ return prs.length;
3210
+ }
2987
3211
  async function startBackgroundDaemon(repoRoot, options = {}) {
2988
3212
  if (isDaemonRunning(repoRoot)) {
2989
3213
  const existing = readDaemonState(repoRoot);
@@ -3043,6 +3267,103 @@ async function stopDaemon(repoRoot) {
3043
3267
  return false;
3044
3268
  }
3045
3269
  }
3270
+ async function drainReviewQueue(drainOptions) {
3271
+ const {
3272
+ repoRoot,
3273
+ state,
3274
+ options = {},
3275
+ isStopping = () => false,
3276
+ clearTicker,
3277
+ getPRs = getOpenReviewablePRs,
3278
+ runRoutine = runLocalRoutine,
3279
+ onAttempted
3280
+ } = drainOptions;
3281
+ if (isStopping()) return;
3282
+ if (state) {
3283
+ state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3284
+ writeDaemonState(repoRoot, state);
3285
+ }
3286
+ let reviewablePRs = await getPRs(repoRoot);
3287
+ if (reviewablePRs.length === 0) {
3288
+ if (options.verbose) {
3289
+ console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
3290
+ }
3291
+ return;
3292
+ }
3293
+ const attemptedPRNumbers = /* @__PURE__ */ new Set();
3294
+ while (!isStopping() && reviewablePRs.length > 0) {
3295
+ const candidatePRs = reviewablePRs.filter((pr) => !attemptedPRNumbers.has(pr.number));
3296
+ if (candidatePRs.length === 0) {
3297
+ if (options.verbose) {
3298
+ console.log(
3299
+ pc11.dim(
3300
+ `[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] All ${reviewablePRs.length} remaining ready PR(s) were already evaluated in this drain pass.`
3301
+ )
3302
+ );
3303
+ }
3304
+ break;
3305
+ }
3306
+ const totalRemaining = candidatePRs.length;
3307
+ let targetPRStr = void 0;
3308
+ try {
3309
+ if (clearTicker) clearTicker();
3310
+ if (state) {
3311
+ state.status = "working";
3312
+ state.activeRoutine = "peer-review";
3313
+ writeDaemonState(repoRoot, state);
3314
+ }
3315
+ console.log(
3316
+ pc11.cyan(
3317
+ `
3318
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Draining PR backlog (${totalRemaining} PR(s) remaining). Starting review session...`
3319
+ )
3320
+ );
3321
+ await cleanupStaleWorktrees(repoRoot);
3322
+ const result = await runRoutine({
3323
+ targetDir: repoRoot,
3324
+ routine: "peer-review",
3325
+ model: options.model,
3326
+ verbose: options.verbose,
3327
+ noWorktree: false,
3328
+ onTargetDetected: (target) => {
3329
+ targetPRStr = target;
3330
+ if (state) {
3331
+ state.activeTarget = target;
3332
+ writeDaemonState(repoRoot, state);
3333
+ }
3334
+ }
3335
+ });
3336
+ const activeTargetStr = targetPRStr;
3337
+ const match = activeTargetStr?.match(/PR\s*#?([0-9]+)/i);
3338
+ const prNum = match ? parseInt(match[1], 10) : candidatePRs[0]?.number;
3339
+ if (typeof prNum === "number") {
3340
+ attemptedPRNumbers.add(prNum);
3341
+ onAttempted?.(prNum);
3342
+ }
3343
+ if (result.success) {
3344
+ console.log(pc11.green(`\u2713 Local peer-review completed successfully.
3345
+ `));
3346
+ } else {
3347
+ console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
3348
+ `));
3349
+ }
3350
+ } catch (err) {
3351
+ console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
3352
+ if (candidatePRs[0]) {
3353
+ attemptedPRNumbers.add(candidatePRs[0].number);
3354
+ onAttempted?.(candidatePRs[0].number);
3355
+ }
3356
+ } finally {
3357
+ if (state) {
3358
+ state.status = "idle";
3359
+ state.activeRoutine = void 0;
3360
+ state.activeTarget = void 0;
3361
+ writeDaemonState(repoRoot, state);
3362
+ }
3363
+ }
3364
+ reviewablePRs = await getPRs(repoRoot);
3365
+ }
3366
+ }
3046
3367
  async function runDaemonLoop(repoRoot, options = {}) {
3047
3368
  const reviewInterval = options.reviewInterval || 3;
3048
3369
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
@@ -3105,61 +3426,51 @@ Stopping local agent daemon...`));
3105
3426
  };
3106
3427
  process.once("SIGINT", handleStop);
3107
3428
  process.once("SIGTERM", handleStop);
3108
- const runReviewCheck = async () => {
3429
+ const performReviewDrain = async () => {
3109
3430
  if (isStopping || isWorking) return;
3110
- state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3111
- writeDaemonState(repoRoot, state);
3112
- const openPRCount = await countOpenReadyPRs(repoRoot);
3113
- lastOpenPRCount = openPRCount;
3114
- if (openPRCount === 0) {
3115
- if (options.verbose) {
3116
- console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
3117
- }
3118
- nextReviewCheckTime = Date.now() + reviewIntervalMs;
3119
- updateTicker();
3120
- return;
3121
- }
3122
3431
  try {
3123
3432
  isWorking = true;
3124
- clearTicker();
3125
- state.status = "working";
3126
- state.activeRoutine = "peer-review";
3127
- writeDaemonState(repoRoot, state);
3128
- console.log(pc11.cyan(`
3129
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
3130
- await cleanupStaleWorktrees(repoRoot);
3131
- const result = await runLocalRoutine({
3132
- targetDir: repoRoot,
3133
- routine: "peer-review",
3134
- model: options.model,
3135
- verbose: options.verbose,
3136
- noWorktree: false,
3137
- onTargetDetected: (target) => {
3138
- state.activeTarget = target;
3139
- writeDaemonState(repoRoot, state);
3140
- }
3433
+ await drainReviewQueue({
3434
+ repoRoot,
3435
+ state,
3436
+ options,
3437
+ isStopping: () => isStopping,
3438
+ clearTicker
3141
3439
  });
3142
- if (result.success) {
3143
- console.log(pc11.green(`\u2713 Local peer-review completed successfully.
3144
- `));
3145
- } else {
3146
- console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
3147
- `));
3148
- }
3149
- } catch (err) {
3150
- console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
3440
+ const prs = await getOpenReviewablePRs(repoRoot);
3441
+ lastOpenPRCount = prs.length;
3151
3442
  } finally {
3152
3443
  isWorking = false;
3153
- state.status = "idle";
3154
- state.activeRoutine = void 0;
3155
- state.activeTarget = void 0;
3156
- writeDaemonState(repoRoot, state);
3157
3444
  nextReviewCheckTime = Date.now() + reviewIntervalMs;
3158
3445
  updateTicker();
3159
3446
  }
3160
3447
  };
3161
3448
  const runAutoworkCheck = async () => {
3162
3449
  if (isStopping || isWorking || !routines.includes("autowork")) return;
3450
+ if (routines.includes("peer-review")) {
3451
+ const pendingPRs = await countOpenReadyPRs(repoRoot);
3452
+ if (pendingPRs > 0) {
3453
+ console.log(
3454
+ pc11.cyan(
3455
+ `
3456
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u23F3 Autowork paused: draining ${pendingPRs} reviewable PR(s) first...`
3457
+ )
3458
+ );
3459
+ await performReviewDrain();
3460
+ const remainingPRs = await countOpenReadyPRs(repoRoot);
3461
+ if (remainingPRs > 0) {
3462
+ console.log(
3463
+ pc11.yellow(
3464
+ `
3465
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u26A0\uFE0F Review backlog still has ${remainingPRs} pending PR(s). Postponing autowork session.`
3466
+ )
3467
+ );
3468
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3469
+ updateTicker();
3470
+ return;
3471
+ }
3472
+ }
3473
+ }
3163
3474
  state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
3164
3475
  writeDaemonState(repoRoot, state);
3165
3476
  try {
@@ -3199,15 +3510,27 @@ Stopping local agent daemon...`));
3199
3510
  writeDaemonState(repoRoot, state);
3200
3511
  nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
3201
3512
  updateTicker();
3513
+ if (!isStopping && routines.includes("peer-review")) {
3514
+ const newPRCount = await countOpenReadyPRs(repoRoot);
3515
+ if (newPRCount > 0) {
3516
+ console.log(
3517
+ pc11.cyan(
3518
+ `
3519
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F504} Post-autowork convergence: Found ${newPRCount} ready PR(s). Initiating review sweep...`
3520
+ )
3521
+ );
3522
+ await performReviewDrain();
3523
+ }
3524
+ }
3202
3525
  }
3203
3526
  };
3204
3527
  if (routines.includes("peer-review")) {
3205
- await runReviewCheck();
3528
+ await performReviewDrain();
3206
3529
  }
3207
- if (routines.includes("autowork")) {
3530
+ if (!isStopping && routines.includes("autowork")) {
3208
3531
  await runAutoworkCheck();
3209
3532
  }
3210
- const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
3533
+ const reviewTimer = setInterval(performReviewDrain, reviewIntervalMs);
3211
3534
  const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
3212
3535
  await new Promise(() => {
3213
3536
  });