jonah-fleet 1.4.2 → 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.4.2";
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:")));
@@ -2157,11 +2208,11 @@ async function runTelemetry(options = {}) {
2157
2208
  }
2158
2209
 
2159
2210
  // src/commands/run.ts
2160
- import pc9 from "picocolors";
2211
+ import pc10 from "picocolors";
2161
2212
 
2162
2213
  // src/lib/runner.ts
2163
- import fs11 from "fs";
2164
- import path11 from "path";
2214
+ import fs12 from "fs";
2215
+ import path12 from "path";
2165
2216
  import os2 from "os";
2166
2217
  import { spawn, execSync as execSync2 } from "child_process";
2167
2218
 
@@ -2286,18 +2337,532 @@ async function cleanupStaleWorktrees(repoRoot) {
2286
2337
  return cleaned;
2287
2338
  }
2288
2339
 
2340
+ // src/lib/terminal-card.ts
2341
+ import fs11 from "fs";
2342
+ import path11 from "path";
2343
+ import { execFileSync } from "child_process";
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
+ }
2397
+ function sanitizeWorktreePaths(text) {
2398
+ let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
2399
+ cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
2400
+ const prefix = match.charAt(0);
2401
+ return prefix === "/" ? "" : prefix;
2402
+ });
2403
+ cleaned = cleaned.replace(/\[`?([^`\]]+?)`?\]\(file:\/\/\/[^\s)]+\)/g, "`$1`");
2404
+ return cleaned;
2405
+ }
2406
+ function extractExecutionSummary(output) {
2407
+ const summaryHeaderRegex = /#{1,3}\s+([A-Za-z0-9\s_-]*?(?:Execution|Review|Autowork)\s+Summary[\s\S]*)/i;
2408
+ const match = output.match(summaryHeaderRegex);
2409
+ if (!match) return null;
2410
+ let summary = match[1].trim();
2411
+ const trailingSeparators = [
2412
+ "\u2713 Local peer-review completed",
2413
+ "\u2713 Local autowork completed",
2414
+ "\u2713 Local agent session",
2415
+ "Peer Review Watchdog:",
2416
+ "Autowork Backlog Scan:"
2417
+ ];
2418
+ for (const sep of trailingSeparators) {
2419
+ const idx = summary.indexOf(sep);
2420
+ if (idx !== -1) {
2421
+ summary = summary.slice(0, idx).trim();
2422
+ }
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
+ }
2428
+ return sanitizeWorktreePaths(summary);
2429
+ }
2430
+ function findLatestRunLog(repoRoot, routine) {
2431
+ const logsDir = path11.join(repoRoot, ".github", "prompts", "logs", routine);
2432
+ if (!fs11.existsSync(logsDir)) return null;
2433
+ try {
2434
+ const files = fs11.readdirSync(logsDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
2435
+ if (files.length === 0) return null;
2436
+ files.sort().reverse();
2437
+ return path11.join(logsDir, files[0]);
2438
+ } catch {
2439
+ return null;
2440
+ }
2441
+ }
2442
+ function parseRunLog(logContent) {
2443
+ const summary = {
2444
+ passes: [],
2445
+ actions: []
2446
+ };
2447
+ const lines = logContent.split("\n");
2448
+ let inMetadata = false;
2449
+ let inDoD = false;
2450
+ let inFindings = false;
2451
+ let inActions = false;
2452
+ for (const line of lines) {
2453
+ const trimmed = line.trim();
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;
2462
+ inDoD = true;
2463
+ inFindings = false;
2464
+ inActions = false;
2465
+ continue;
2466
+ } else if (trimmed.startsWith("## Code Review Findings") || trimmed.startsWith("## Findings")) {
2467
+ inMetadata = false;
2468
+ inDoD = false;
2469
+ inFindings = true;
2470
+ inActions = false;
2471
+ continue;
2472
+ } else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken") || trimmed.startsWith("## Artifacts")) {
2473
+ inMetadata = false;
2474
+ inDoD = false;
2475
+ inFindings = false;
2476
+ inActions = trimmed.startsWith("### Actions Taken") || trimmed.startsWith("## Actions Taken");
2477
+ continue;
2478
+ } else if (trimmed.startsWith("## ")) {
2479
+ inMetadata = false;
2480
+ inDoD = false;
2481
+ inFindings = false;
2482
+ inActions = false;
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
+ }
2499
+ if (inDoD && trimmed.startsWith("|") && !trimmed.includes("Criterion") && !trimmed.includes("---")) {
2500
+ const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
2501
+ if (parts.length >= 2) {
2502
+ const criterion = parts[0];
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
+ }
2515
+ }
2516
+ }
2517
+ if (inActions && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
2518
+ summary.actions?.push(sanitizeWorktreePaths(trimmed.slice(2)));
2519
+ }
2520
+ }
2521
+ return summary;
2522
+ }
2523
+ function detectActivePhase(chunk, currentPhase = "Executing routine") {
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
+ }
2528
+ if (lower.includes("\u{1F512} claimed") || lower.includes("claimed by local autowork") || lower.includes("claimed by autowork")) {
2529
+ return "Claimed target issue, starting implementation";
2530
+ }
2531
+ if (lower.includes("addressing review findings") || lower.includes("fixing review findings")) {
2532
+ return "Claimed bounced PR, addressing review findings";
2533
+ }
2534
+ if (lower.includes("starting review (round")) {
2535
+ return "Claimed review window, starting review passes";
2536
+ }
2537
+ if (lower.includes("check-client-boundary")) return "Verifying React Server Component boundaries";
2538
+ if (lower.includes("type-check") || lower.includes("tsc --noemit")) return "Running TypeScript type checks";
2539
+ if (lower.includes("lint") || lower.includes("eslint")) return "Running codebase linter";
2540
+ if (lower.includes("test") || lower.includes("vitest") || lower.includes("jest")) return "Running automated test suite";
2541
+ if (lower.includes("build") || lower.includes("next build") || lower.includes("tsup")) return "Running production build verification";
2542
+ if (lower.includes("code-review") || lower.includes("subagent")) return "Running multi-angle code review passes";
2543
+ if (lower.includes("squash-merge") || lower.includes("pr merge")) return "Squash-merging target PR to main";
2544
+ if (lower.includes("gh issue create") || lower.includes("autonomous issue synthesis")) return "Synthesizing tracking issue";
2545
+ if (lower.includes("--undo") || lower.includes("draft")) return "Bouncing PR back to draft for author fixes";
2546
+ if (lower.includes("issue edit") || lower.includes("pr edit")) return "Linking PR & tracking issues";
2547
+ if (lower.includes("pr comment") || lower.includes("review summary")) return "Submitting review comment";
2548
+ if (lower.includes("worktree")) return "Preparing workspace worktree";
2549
+ return currentPhase;
2550
+ }
2551
+ function detectClaimedIssue(chunk) {
2552
+ const claimMatch = chunk.match(/🔒\s*Claimed[^\n#]*?#(\d+)/i);
2553
+ if (claimMatch) return `Issue #${claimMatch[1]}`;
2554
+ const ghMatch = chunk.match(/gh\s+issue\s+(?:view|edit|comment|develop)\s+(\d+)/i);
2555
+ if (ghMatch) return `Issue #${ghMatch[1]}`;
2556
+ const textMatch = chunk.match(/(?:selected|claimed|claiming|target(?:ing)?|working|candidate)\s+(?:candidate\s+)?issue\s+#?(\d+)/i);
2557
+ if (textMatch) return `Issue #${textMatch[1]}`;
2558
+ const passiveMatch = chunk.match(/issue\s+#(\d+)\s+(?:claimed|selected)/i);
2559
+ if (passiveMatch) return `Issue #${passiveMatch[1]}`;
2560
+ return null;
2561
+ }
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]}`;
2565
+ const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
2566
+ if (reviewMatch) return `PR #${reviewMatch[1]}`;
2567
+ const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
2568
+ if (prMatch) return `PR #${prMatch[1]}`;
2569
+ const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review|edit|ready)\s+(\d+)/i);
2570
+ if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
2571
+ return null;
2572
+ }
2573
+ function renderSummaryCard(options) {
2574
+ const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 64), 90);
2575
+ const horizontal = "\u2500".repeat(width - 2);
2576
+ const rawSummary = options.output ? extractExecutionSummary(options.output) : null;
2577
+ let parsedFromLog = null;
2578
+ if (options.repoRoot) {
2579
+ const latestLog = findLatestRunLog(options.repoRoot, options.routine);
2580
+ if (latestLog) {
2581
+ try {
2582
+ const content = fs11.readFileSync(latestLog, "utf8");
2583
+ parsedFromLog = parseRunLog(content);
2584
+ parsedFromLog.logPath = path11.relative(options.repoRoot, latestLog);
2585
+ } catch {
2586
+ }
2587
+ }
2588
+ }
2589
+ let target = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : "";
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
+ }
2612
+ let decision = parsedFromLog?.decision || "";
2613
+ if (!decision && rawSummary) {
2614
+ const decisionMatch = rawSummary.match(/\*\*Final Action\*\*:\s*([^\n]+)/i);
2615
+ if (decisionMatch) decision = decisionMatch[1].replace(/[`*]/g, "").trim();
2616
+ }
2617
+ const durationStr = options.durationMs ? `${Math.round(options.durationMs / 1e3)}s` : parsedFromLog?.duration || "";
2618
+ const lines = [];
2619
+ lines.push(pc9.cyan(`\u250C${horizontal}\u2510`));
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);
2625
+ lines.push(
2626
+ pc9.cyan("\u2502") + ` ${headerContent}` + " ".repeat(Math.max(1, width - 3 - headerPlain.length)) + pc9.cyan("\u2502")
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
+ }
2640
+ if (decision) {
2641
+ let decisionBadge = pc9.green(`\u2714 ${decision}`);
2642
+ if (/bounce|draft|reject|fail/i.test(decision)) {
2643
+ decisionBadge = pc9.yellow(`\u26A0\uFE0F ${decision}`);
2644
+ } else if (/escalat/i.test(decision)) {
2645
+ decisionBadge = pc9.red(`\u{1F6A8} ${decision}`);
2646
+ }
2647
+ const decisionPlain = ` Action: ${decision}`;
2648
+ lines.push(
2649
+ pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 3 - decisionPlain.length)) + pc9.cyan("\u2502")
2650
+ );
2651
+ }
2652
+ lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
2653
+ if (rawSummary) {
2654
+ const summaryLines = rawSummary.split("\n");
2655
+ for (const rawLine of summaryLines) {
2656
+ const line = rawLine.trim();
2657
+ if (!line) continue;
2658
+ if (line.startsWith("# ")) continue;
2659
+ if (line.startsWith("---")) continue;
2660
+ if (line.startsWith("**Selected Target") || line.startsWith("**Final Action") || line.startsWith("**Mode**:")) {
2661
+ continue;
2662
+ }
2663
+ if (line.startsWith("### ")) {
2664
+ const heading = line.replace("### ", "").trim();
2665
+ lines.push(
2666
+ pc9.cyan("\u2502") + ` ${pc9.bold(pc9.cyan(heading))}` + " ".repeat(Math.max(1, width - 3 - heading.length)) + pc9.cyan("\u2502")
2667
+ );
2668
+ } else if (line.startsWith("- ") || line.startsWith("* ")) {
2669
+ const item = sanitizeWorktreePaths(line.slice(2)).trim();
2670
+ const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
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
+ }
2684
+ }
2685
+ } else if (/^[0-9]+\.\s+/.test(line)) {
2686
+ const item = sanitizeWorktreePaths(line.replace(/^[0-9]+\.\s+/, "")).trim();
2687
+ const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
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
+ }
2701
+ }
2702
+ }
2703
+ }
2704
+ } else if (parsedFromLog && parsedFromLog.passes && parsedFromLog.passes.length > 0) {
2705
+ lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Verification Passes:")}` + " ".repeat(Math.max(1, width - 23)) + pc9.cyan("\u2502"));
2706
+ for (const pass of parsedFromLog.passes.slice(0, 6)) {
2707
+ const icon = pass.status === "pass" ? pc9.green("\u2714") : pc9.red("\u2716");
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
+ }
2747
+ }
2748
+ }
2749
+ if (parsedFromLog?.logPath) {
2750
+ lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
2751
+ const logInfo = ` Run log: ${pc9.dim(parsedFromLog.logPath)}`;
2752
+ const logPlain = ` Run log: ${parsedFromLog.logPath}`;
2753
+ lines.push(pc9.cyan("\u2502") + logInfo + " ".repeat(Math.max(1, width - 2 - logPlain.length)) + pc9.cyan("\u2502"));
2754
+ }
2755
+ lines.push(pc9.cyan(`\u2514${horizontal}\u2518`));
2756
+ return lines.join("\n");
2757
+ }
2758
+ function renderErrorCard(options) {
2759
+ const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 60), 86);
2760
+ const horizontal = "\u2500".repeat(width - 2);
2761
+ const lines = [];
2762
+ lines.push(pc9.red(`\u250C${horizontal}\u2510`));
2763
+ const target = options.pr ? ` \xB7 PR #${options.pr}` : options.issue ? ` \xB7 Issue #${options.issue}` : "";
2764
+ const durationStr = options.durationMs ? ` (${Math.round(options.durationMs / 1e3)}s)` : "";
2765
+ const routineUpper = options.routine.toUpperCase();
2766
+ const header = ` \u2717 Routine '${routineUpper}' Failed (Exit Code ${options.exitCode})${target}${durationStr}`;
2767
+ const headerPlain = ` \u2717 Routine '${routineUpper}' Failed (Exit Code ${options.exitCode})${target}${durationStr}`;
2768
+ lines.push(
2769
+ pc9.red("\u2502") + pc9.bold(pc9.red(header.slice(0, width - 3))) + " ".repeat(Math.max(1, width - 2 - headerPlain.length)) + pc9.red("\u2502")
2770
+ );
2771
+ lines.push(pc9.red(`\u251C${horizontal}\u2524`));
2772
+ const logPath = path11.join(options.repoRoot, ".jonah-fleet", "daemon.log");
2773
+ lines.push(pc9.red("\u2502") + pc9.yellow(" Recent Log Output:") + " ".repeat(Math.max(1, width - 21)) + pc9.red("\u2502"));
2774
+ if (fs11.existsSync(logPath)) {
2775
+ try {
2776
+ const logContent = fs11.readFileSync(logPath, "utf8");
2777
+ const allLines = logContent.split("\n").filter((l) => l.trim().length > 0);
2778
+ const tailLines = allLines.slice(-10);
2779
+ for (const line of tailLines) {
2780
+ const cleaned = sanitizeWorktreePaths(line).trim();
2781
+ const truncated = cleaned.length > width - 6 ? cleaned.slice(0, width - 9) + "..." : cleaned;
2782
+ lines.push(pc9.red("\u2502") + pc9.dim(` ${truncated}`) + " ".repeat(Math.max(1, width - 4 - truncated.length)) + pc9.red("\u2502"));
2783
+ }
2784
+ } catch {
2785
+ lines.push(
2786
+ pc9.red("\u2502") + pc9.dim(" (Could not read .jonah-fleet/daemon.log)") + " ".repeat(Math.max(1, width - 45)) + pc9.red("\u2502")
2787
+ );
2788
+ }
2789
+ } else {
2790
+ lines.push(pc9.red("\u2502") + pc9.dim(" (No daemon.log found)") + " ".repeat(Math.max(1, width - 26)) + pc9.red("\u2502"));
2791
+ }
2792
+ lines.push(pc9.red(`\u251C${horizontal}\u2524`));
2793
+ const relLogPath = path11.relative(options.repoRoot, logPath) || ".jonah-fleet/daemon.log";
2794
+ const footer = ` Full trace: ${relLogPath}`;
2795
+ const truncatedFooter = footer.slice(0, width - 4);
2796
+ lines.push(pc9.red("\u2502") + pc9.dim(truncatedFooter) + " ".repeat(Math.max(1, width - 2 - truncatedFooter.length)) + pc9.red("\u2502"));
2797
+ lines.push(pc9.red(`\u2514${horizontal}\u2518`));
2798
+ return lines.join("\n");
2799
+ }
2800
+ var TerminalSpinner = class {
2801
+ frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
2802
+ currentFrame = 0;
2803
+ intervalId = null;
2804
+ startTime = 0;
2805
+ message = "";
2806
+ isRunning = false;
2807
+ isTTY;
2808
+ constructor() {
2809
+ this.isTTY = Boolean(process.stderr.isTTY);
2810
+ }
2811
+ start(initialMessage) {
2812
+ this.message = initialMessage;
2813
+ this.startTime = Date.now();
2814
+ this.isRunning = true;
2815
+ if (!this.isTTY) {
2816
+ process.stderr.write(`[jonah-fleet] ${initialMessage}
2817
+ `);
2818
+ return;
2819
+ }
2820
+ this.intervalId = setInterval(() => {
2821
+ this.render();
2822
+ }, 80);
2823
+ }
2824
+ update(newMessage) {
2825
+ this.message = newMessage;
2826
+ if (!this.isTTY) {
2827
+ process.stderr.write(`[jonah-fleet] ${newMessage}
2828
+ `);
2829
+ }
2830
+ }
2831
+ render() {
2832
+ if (!this.isRunning || !this.isTTY) return;
2833
+ const frame = pc9.cyan(this.frames[this.currentFrame]);
2834
+ this.currentFrame = (this.currentFrame + 1) % this.frames.length;
2835
+ const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
2836
+ const mins = Math.floor(elapsedSeconds / 60);
2837
+ const secs = elapsedSeconds % 60;
2838
+ const timeStr = pc9.dim(`[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`);
2839
+ process.stderr.write(`\r\x1B[K ${frame} ${this.message} ${timeStr}`);
2840
+ }
2841
+ stop() {
2842
+ if (!this.isRunning) return;
2843
+ this.isRunning = false;
2844
+ if (this.intervalId) {
2845
+ clearInterval(this.intervalId);
2846
+ this.intervalId = null;
2847
+ }
2848
+ if (this.isTTY) {
2849
+ process.stderr.write("\r\x1B[K");
2850
+ }
2851
+ }
2852
+ };
2853
+
2289
2854
  // src/lib/runner.ts
2290
2855
  function discoverSkillsPrompt(targetDir) {
2291
- const skillsDir = path11.join(targetDir, ".agents", "skills");
2292
- if (!fs11.existsSync(skillsDir)) return "";
2856
+ const skillsDir = path12.join(targetDir, ".agents", "skills");
2857
+ if (!fs12.existsSync(skillsDir)) return "";
2293
2858
  let skillsPrompt = "";
2294
2859
  try {
2295
- const entries = fs11.readdirSync(skillsDir, { withFileTypes: true });
2860
+ const entries = fs12.readdirSync(skillsDir, { withFileTypes: true });
2296
2861
  for (const entry of entries) {
2297
2862
  if (entry.isDirectory()) {
2298
- const skillPath = path11.join(".agents", "skills", entry.name, "SKILL.md");
2299
- const fullPath = path11.join(targetDir, skillPath);
2300
- if (fs11.existsSync(fullPath)) {
2863
+ const skillPath = path12.join(".agents", "skills", entry.name, "SKILL.md");
2864
+ const fullPath = path12.join(targetDir, skillPath);
2865
+ if (fs12.existsSync(fullPath)) {
2301
2866
  skillsPrompt += `Read and follow ${skillPath}. `;
2302
2867
  }
2303
2868
  }
@@ -2324,14 +2889,14 @@ function buildRoutinePrompt(targetDir, routine, options = {}) {
2324
2889
  return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
2325
2890
  }
2326
2891
  async function runLocalRoutine(options) {
2327
- const targetDir = path11.resolve(options.targetDir);
2892
+ const targetDir = path12.resolve(options.targetDir);
2328
2893
  const routine = options.routine;
2329
2894
  const model = options.model || "gemini-3.7-flash-high";
2330
2895
  const printTimeout = options.printTimeout || "30m";
2331
2896
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2332
2897
  const hostname = os2.hostname();
2333
- const promptFile = path11.join(targetDir, ".github", "prompts", `${routine}.md`);
2334
- if (!fs11.existsSync(promptFile)) {
2898
+ const promptFile = path12.join(targetDir, ".github", "prompts", `${routine}.md`);
2899
+ if (!fs12.existsSync(promptFile)) {
2335
2900
  throw new Error(`Routine prompt file not found: ${promptFile}`);
2336
2901
  }
2337
2902
  let branchName = `agent/${routine}-${timestamp}`;
@@ -2384,7 +2949,19 @@ Timeout: ${printTimeout}`,
2384
2949
  ];
2385
2950
  let output = "";
2386
2951
  let exitCode = 0;
2952
+ const startTime = Date.now();
2953
+ const logDir = path12.join(targetDir, ".jonah-fleet");
2954
+ fs12.mkdirSync(logDir, { recursive: true });
2955
+ const logFilePath = path12.join(logDir, "daemon.log");
2956
+ let targetLabel = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : routine;
2957
+ let dynamicTargetDetected = Boolean(options.pr || options.issue);
2958
+ let activePhase = "Starting session...";
2959
+ const spinner = !options.verbose ? new TerminalSpinner() : null;
2960
+ if (spinner) {
2961
+ spinner.start(`${targetLabel}: ${activePhase}`);
2962
+ }
2387
2963
  const cleanup = async () => {
2964
+ spinner?.stop();
2388
2965
  if (worktreePath && !options.keepWorktree) {
2389
2966
  await removeWorktree(targetDir, worktreePath, { deleteBranch: false }).catch(() => {
2390
2967
  });
@@ -2396,6 +2973,40 @@ Timeout: ${printTimeout}`,
2396
2973
  };
2397
2974
  process.once("SIGINT", sigintHandler);
2398
2975
  process.once("SIGTERM", sigintHandler);
2976
+ const processChunk = (chunk, isStderr = false) => {
2977
+ output += chunk;
2978
+ try {
2979
+ fs12.appendFileSync(logFilePath, chunk, "utf8");
2980
+ } catch {
2981
+ }
2982
+ if (!dynamicTargetDetected) {
2983
+ const detected = routine === "peer-review" ? detectClaimedPR(chunk) : detectClaimedIssue(chunk);
2984
+ if (detected) {
2985
+ dynamicTargetDetected = true;
2986
+ targetLabel = detected;
2987
+ options.onTargetDetected?.(detected);
2988
+ if (spinner) {
2989
+ spinner.update(`${targetLabel}: ${activePhase}`);
2990
+ }
2991
+ }
2992
+ }
2993
+ if (options.onLog) {
2994
+ options.onLog(chunk);
2995
+ }
2996
+ if (options.verbose) {
2997
+ if (isStderr) {
2998
+ process.stderr.write(chunk);
2999
+ } else {
3000
+ process.stdout.write(chunk);
3001
+ }
3002
+ } else if (spinner) {
3003
+ const newPhase = detectActivePhase(chunk, activePhase);
3004
+ if (newPhase !== activePhase) {
3005
+ activePhase = newPhase;
3006
+ spinner.update(`${targetLabel}: ${activePhase}`);
3007
+ }
3008
+ }
3009
+ };
2399
3010
  try {
2400
3011
  exitCode = await new Promise((resolve, reject) => {
2401
3012
  const child = spawn("agy", args, {
@@ -2404,37 +3015,56 @@ Timeout: ${printTimeout}`,
2404
3015
  stdio: ["inherit", "pipe", "pipe"]
2405
3016
  });
2406
3017
  child.stdout?.on("data", (data) => {
2407
- const chunk = data.toString();
2408
- output += chunk;
2409
- if (options.onLog) {
2410
- options.onLog(chunk);
2411
- } else {
2412
- process.stdout.write(chunk);
2413
- }
3018
+ processChunk(data.toString(), false);
2414
3019
  });
2415
3020
  child.stderr?.on("data", (data) => {
2416
- const chunk = data.toString();
2417
- output += chunk;
2418
- if (options.onLog) {
2419
- options.onLog(chunk);
2420
- } else {
2421
- process.stderr.write(chunk);
2422
- }
3021
+ processChunk(data.toString(), true);
2423
3022
  });
2424
3023
  child.on("error", (err) => {
3024
+ spinner?.stop();
2425
3025
  reject(err);
2426
3026
  });
2427
3027
  child.on("close", (code) => {
3028
+ spinner?.stop();
2428
3029
  resolve(code ?? 0);
2429
3030
  });
2430
3031
  });
2431
3032
  } finally {
3033
+ spinner?.stop();
2432
3034
  process.removeListener("SIGINT", sigintHandler);
2433
3035
  process.removeListener("SIGTERM", sigintHandler);
2434
3036
  if (!options.keepWorktree) {
2435
3037
  await cleanup();
2436
3038
  }
2437
3039
  }
3040
+ if (options.showCard !== false && !options.verbose) {
3041
+ const durationMs = Date.now() - startTime;
3042
+ const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
3043
+ const effectivePR = options.pr || (targetLabel.startsWith("PR #") ? targetLabel.replace("PR #", "") : void 0);
3044
+ if (exitCode === 0) {
3045
+ console.log(
3046
+ "\n" + renderSummaryCard({
3047
+ routine,
3048
+ output,
3049
+ repoRoot: targetDir,
3050
+ issue: effectiveIssue,
3051
+ pr: effectivePR,
3052
+ durationMs
3053
+ }) + "\n"
3054
+ );
3055
+ } else {
3056
+ console.log(
3057
+ "\n" + renderErrorCard({
3058
+ routine,
3059
+ exitCode,
3060
+ repoRoot: targetDir,
3061
+ issue: effectiveIssue,
3062
+ pr: effectivePR,
3063
+ durationMs
3064
+ }) + "\n"
3065
+ );
3066
+ }
3067
+ }
2438
3068
  return {
2439
3069
  success: exitCode === 0,
2440
3070
  exitCode,
@@ -2450,28 +3080,31 @@ async function runRoutineCommand(routine, options = {}) {
2450
3080
  const manifest = loadManifest(cwd);
2451
3081
  if (!manifest) {
2452
3082
  console.warn(
2453
- pc9.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
3083
+ pc10.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
2454
3084
  );
2455
3085
  } else if (manifest.routines && manifest.routines[routine] === false) {
2456
3086
  console.warn(
2457
- pc9.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
3087
+ pc10.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
2458
3088
  );
2459
3089
  }
2460
- console.log(pc9.cyan(`
2461
- \u{1F680} Launching local agent session for routine: ${pc9.bold(routine)}`));
3090
+ console.log(pc10.cyan(`
3091
+ \u{1F680} Launching local agent session for routine: ${pc10.bold(routine)}`));
2462
3092
  if (options.issue) {
2463
- console.log(pc9.dim(` Target issue: #${options.issue}`));
3093
+ console.log(pc10.dim(` Target issue: #${options.issue}`));
2464
3094
  }
2465
3095
  if (options.pr) {
2466
- console.log(pc9.dim(` Target pull request: #${options.pr}`));
3096
+ console.log(pc10.dim(` Target pull request: #${options.pr}`));
2467
3097
  }
2468
3098
  if (options.model) {
2469
- console.log(pc9.dim(` Model override: ${options.model}`));
3099
+ console.log(pc10.dim(` Model override: ${options.model}`));
3100
+ }
3101
+ if (options.verbose) {
3102
+ console.log(pc10.dim(` Verbose output: Enabled (streaming raw tokens)`));
2470
3103
  }
2471
3104
  if (options.worktree !== false) {
2472
- console.log(pc9.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
3105
+ console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
2473
3106
  } else {
2474
- console.log(pc9.yellow(` Workspace isolation: Disabled (running in current directory)`));
3107
+ console.log(pc10.yellow(` Workspace isolation: Disabled (running in current directory)`));
2475
3108
  }
2476
3109
  console.log("");
2477
3110
  try {
@@ -2484,59 +3117,60 @@ async function runRoutineCommand(routine, options = {}) {
2484
3117
  printTimeout: options.timeout,
2485
3118
  noWorktree: options.worktree === false,
2486
3119
  keepWorktree: options.keepWorktree,
2487
- dryRun: options.dryRun
3120
+ dryRun: options.dryRun,
3121
+ verbose: options.verbose
2488
3122
  });
2489
3123
  if (options.dryRun) {
2490
- console.log(pc9.green(result.output));
3124
+ console.log(pc10.green(result.output));
2491
3125
  return;
2492
3126
  }
2493
3127
  if (result.success) {
2494
- console.log(pc9.green(`
3128
+ console.log(pc10.green(`
2495
3129
  \u2713 Local agent session for '${routine}' completed successfully.`));
2496
3130
  } else {
2497
- console.error(pc9.red(`
3131
+ console.error(pc10.red(`
2498
3132
  \u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
2499
3133
  process.exit(result.exitCode);
2500
3134
  }
2501
3135
  } catch (error) {
2502
- console.error(pc9.red(`
3136
+ console.error(pc10.red(`
2503
3137
  \u2717 Failed to execute routine '${routine}': ${error.message}`));
2504
3138
  process.exit(1);
2505
3139
  }
2506
3140
  }
2507
3141
 
2508
3142
  // src/commands/daemon.ts
2509
- import pc11 from "picocolors";
3143
+ import pc12 from "picocolors";
2510
3144
 
2511
3145
  // src/lib/daemon.ts
2512
- import fs12 from "fs";
2513
- import path12 from "path";
3146
+ import fs13 from "fs";
3147
+ import path13 from "path";
2514
3148
  import { spawn as spawn2, execFile as execFile3 } from "child_process";
2515
3149
  import { promisify as promisify3 } from "util";
2516
- import pc10 from "picocolors";
3150
+ import pc11 from "picocolors";
2517
3151
  var execFileAsync3 = promisify3(execFile3);
2518
3152
  function getDaemonStatePath(repoRoot) {
2519
- return path12.join(repoRoot, ".jonah-fleet", "daemon.json");
3153
+ return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
2520
3154
  }
2521
3155
  function readDaemonState(repoRoot) {
2522
3156
  const statePath = getDaemonStatePath(repoRoot);
2523
- if (!fs12.existsSync(statePath)) return null;
3157
+ if (!fs13.existsSync(statePath)) return null;
2524
3158
  try {
2525
- return JSON.parse(fs12.readFileSync(statePath, "utf8"));
3159
+ return JSON.parse(fs13.readFileSync(statePath, "utf8"));
2526
3160
  } catch {
2527
3161
  return null;
2528
3162
  }
2529
3163
  }
2530
3164
  function writeDaemonState(repoRoot, state) {
2531
3165
  const statePath = getDaemonStatePath(repoRoot);
2532
- fs12.mkdirSync(path12.dirname(statePath), { recursive: true });
2533
- fs12.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
3166
+ fs13.mkdirSync(path13.dirname(statePath), { recursive: true });
3167
+ fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
2534
3168
  }
2535
3169
  function clearDaemonState(repoRoot) {
2536
3170
  const statePath = getDaemonStatePath(repoRoot);
2537
- if (fs12.existsSync(statePath)) {
3171
+ if (fs13.existsSync(statePath)) {
2538
3172
  try {
2539
- fs12.unlinkSync(statePath);
3173
+ fs13.unlinkSync(statePath);
2540
3174
  } catch {
2541
3175
  }
2542
3176
  }
@@ -2552,18 +3186,28 @@ function isDaemonRunning(repoRoot) {
2552
3186
  return false;
2553
3187
  }
2554
3188
  }
2555
- 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) {
2556
3195
  try {
2557
3196
  const { stdout } = await execFileAsync3(
2558
3197
  "gh",
2559
- ["pr", "list", "--state", "open", "--draft=false", "--json", "number", "--jq", "length"],
3198
+ ["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
2560
3199
  { cwd: repoRoot }
2561
3200
  );
2562
- return parseInt(stdout.trim(), 10) || 0;
3201
+ const prs = JSON.parse(stdout);
3202
+ return filterReviewablePRs(prs);
2563
3203
  } catch {
2564
- return 0;
3204
+ return [];
2565
3205
  }
2566
3206
  }
3207
+ async function countOpenReadyPRs(repoRoot) {
3208
+ const prs = await getOpenReviewablePRs(repoRoot);
3209
+ return prs.length;
3210
+ }
2567
3211
  async function startBackgroundDaemon(repoRoot, options = {}) {
2568
3212
  if (isDaemonRunning(repoRoot)) {
2569
3213
  const existing = readDaemonState(repoRoot);
@@ -2572,9 +3216,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
2572
3216
  const reviewInterval = options.reviewInterval || 3;
2573
3217
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
2574
3218
  const routines = options.routines || ["peer-review", "autowork"];
2575
- const logFilePath = path12.join(repoRoot, ".jonah-fleet", "daemon.log");
2576
- fs12.mkdirSync(path12.dirname(logFilePath), { recursive: true });
2577
- const logFd = fs12.openSync(logFilePath, "a");
3219
+ const logFilePath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
3220
+ fs13.mkdirSync(path13.dirname(logFilePath), { recursive: true });
3221
+ const logFd = fs13.openSync(logFilePath, "a");
2578
3222
  const cliPath = process.argv[1];
2579
3223
  const args = [
2580
3224
  "daemon",
@@ -2589,6 +3233,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
2589
3233
  if (options.model) {
2590
3234
  args.push("--model", options.model);
2591
3235
  }
3236
+ if (options.verbose) {
3237
+ args.push("--verbose");
3238
+ }
2592
3239
  const child = spawn2(process.execPath, [cliPath, ...args], {
2593
3240
  cwd: repoRoot,
2594
3241
  detached: true,
@@ -2620,6 +3267,103 @@ async function stopDaemon(repoRoot) {
2620
3267
  return false;
2621
3268
  }
2622
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
+ }
2623
3367
  async function runDaemonLoop(repoRoot, options = {}) {
2624
3368
  const reviewInterval = options.reviewInterval || 3;
2625
3369
  const autoworkInterval = options.autoworkInterval || options.interval || 30;
@@ -2633,19 +3377,48 @@ async function runDaemonLoop(repoRoot, options = {}) {
2633
3377
  status: "idle"
2634
3378
  };
2635
3379
  writeDaemonState(repoRoot, state);
2636
- console.log(pc10.cyan(`
3380
+ console.log(pc11.cyan(`
2637
3381
  \u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
2638
- console.log(pc10.dim(` PID: ${process.pid}`));
2639
- console.log(pc10.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
2640
- console.log(pc10.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
2641
- console.log(pc10.dim(` Working Directory: ${repoRoot}
3382
+ console.log(pc11.dim(` PID: ${process.pid}`));
3383
+ console.log(pc11.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
3384
+ console.log(pc11.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
3385
+ console.log(pc11.dim(` Working Directory: ${repoRoot}
2642
3386
  `));
2643
3387
  let isStopping = false;
2644
3388
  let isWorking = false;
3389
+ const reviewIntervalMs = reviewInterval * 60 * 1e3;
3390
+ const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
3391
+ let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
3392
+ let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
3393
+ let lastOpenPRCount = void 0;
3394
+ const clearTicker = () => {
3395
+ if (process.stderr.isTTY && !options.verbose) {
3396
+ process.stderr.write("\r\x1B[K");
3397
+ }
3398
+ };
3399
+ const updateTicker = () => {
3400
+ if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
3401
+ const now = Date.now();
3402
+ const nextCheck = Math.min(nextReviewCheckTime, nextAutoworkCheckTime);
3403
+ const diffMs = Math.max(0, nextCheck - now);
3404
+ const remainingSecs = Math.ceil(diffMs / 1e3);
3405
+ const mins = Math.floor(remainingSecs / 60);
3406
+ const secs = remainingSecs % 60;
3407
+ const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
3408
+ const prStr = lastOpenPRCount !== void 0 ? ` (${lastOpenPRCount} ready PRs)` : "";
3409
+ process.stderr.write(
3410
+ `\r\x1B[K${pc11.dim("[" + (/* @__PURE__ */ new Date()).toLocaleTimeString() + "]")} \u{1F4A4} ${pc11.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}`
3411
+ );
3412
+ };
3413
+ const tickerInterval = setInterval(updateTicker, 1e3);
2645
3414
  const handleStop = async () => {
2646
3415
  if (isStopping) return;
2647
3416
  isStopping = true;
2648
- console.log(pc10.yellow(`
3417
+ clearInterval(tickerInterval);
3418
+ clearInterval(reviewTimer);
3419
+ clearInterval(autoworkTimer);
3420
+ clearTicker();
3421
+ console.log(pc11.yellow(`
2649
3422
  Stopping local agent daemon...`));
2650
3423
  clearDaemonState(repoRoot);
2651
3424
  await cleanupStaleWorktrees(repoRoot);
@@ -2653,84 +3426,111 @@ Stopping local agent daemon...`));
2653
3426
  };
2654
3427
  process.once("SIGINT", handleStop);
2655
3428
  process.once("SIGTERM", handleStop);
2656
- const runReviewCheck = async () => {
3429
+ const performReviewDrain = async () => {
2657
3430
  if (isStopping || isWorking) return;
2658
- state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
2659
- writeDaemonState(repoRoot, state);
2660
- const openPRCount = await countOpenReadyPRs(repoRoot);
2661
- if (openPRCount === 0) {
2662
- console.log(pc10.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
2663
- return;
2664
- }
2665
3431
  try {
2666
3432
  isWorking = true;
2667
- state.status = "working";
2668
- state.activeRoutine = "peer-review";
2669
- writeDaemonState(repoRoot, state);
2670
- console.log(pc10.cyan(`
2671
- [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
2672
- await cleanupStaleWorktrees(repoRoot);
2673
- const result = await runLocalRoutine({
2674
- targetDir: repoRoot,
2675
- routine: "peer-review",
2676
- model: options.model,
2677
- noWorktree: false
3433
+ await drainReviewQueue({
3434
+ repoRoot,
3435
+ state,
3436
+ options,
3437
+ isStopping: () => isStopping,
3438
+ clearTicker
2678
3439
  });
2679
- if (result.success) {
2680
- console.log(pc10.green(`\u2713 Local peer-review completed successfully.`));
2681
- } else {
2682
- console.warn(pc10.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.`));
2683
- }
2684
- } catch (err) {
2685
- console.error(pc10.red(`\u2717 Error in peer-review: ${err.message}`));
3440
+ const prs = await getOpenReviewablePRs(repoRoot);
3441
+ lastOpenPRCount = prs.length;
2686
3442
  } finally {
2687
3443
  isWorking = false;
2688
- state.status = "idle";
2689
- state.activeRoutine = void 0;
2690
- writeDaemonState(repoRoot, state);
3444
+ nextReviewCheckTime = Date.now() + reviewIntervalMs;
3445
+ updateTicker();
2691
3446
  }
2692
3447
  };
2693
3448
  const runAutoworkCheck = async () => {
2694
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
+ }
2695
3474
  state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
2696
3475
  writeDaemonState(repoRoot, state);
2697
3476
  try {
2698
3477
  isWorking = true;
3478
+ clearTicker();
2699
3479
  state.status = "working";
2700
3480
  state.activeRoutine = "autowork";
2701
3481
  writeDaemonState(repoRoot, state);
2702
- console.log(pc10.cyan(`
3482
+ console.log(pc11.cyan(`
2703
3483
  [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
2704
3484
  await cleanupStaleWorktrees(repoRoot);
2705
3485
  const result = await runLocalRoutine({
2706
3486
  targetDir: repoRoot,
2707
3487
  routine: "autowork",
2708
3488
  model: options.model,
2709
- noWorktree: false
3489
+ verbose: options.verbose,
3490
+ noWorktree: false,
3491
+ onTargetDetected: (target) => {
3492
+ state.activeTarget = target;
3493
+ writeDaemonState(repoRoot, state);
3494
+ }
2710
3495
  });
2711
3496
  if (result.success) {
2712
- console.log(pc10.green(`\u2713 Local autowork completed successfully.`));
3497
+ console.log(pc11.green(`\u2713 Local autowork completed successfully.
3498
+ `));
2713
3499
  } else {
2714
- console.warn(pc10.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.`));
3500
+ console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
3501
+ `));
2715
3502
  }
2716
3503
  } catch (err) {
2717
- console.error(pc10.red(`\u2717 Error in autowork: ${err.message}`));
3504
+ console.error(pc11.red(`\u2717 Error in autowork: ${err.message}`));
2718
3505
  } finally {
2719
3506
  isWorking = false;
2720
3507
  state.status = "idle";
2721
3508
  state.activeRoutine = void 0;
3509
+ state.activeTarget = void 0;
2722
3510
  writeDaemonState(repoRoot, state);
3511
+ nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
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
+ }
2723
3525
  }
2724
3526
  };
2725
3527
  if (routines.includes("peer-review")) {
2726
- await runReviewCheck();
3528
+ await performReviewDrain();
2727
3529
  }
2728
- if (routines.includes("autowork")) {
3530
+ if (!isStopping && routines.includes("autowork")) {
2729
3531
  await runAutoworkCheck();
2730
3532
  }
2731
- const reviewIntervalMs = reviewInterval * 60 * 1e3;
2732
- const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
2733
- const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
3533
+ const reviewTimer = setInterval(performReviewDrain, reviewIntervalMs);
2734
3534
  const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
2735
3535
  await new Promise(() => {
2736
3536
  });
@@ -2746,7 +3546,8 @@ async function runDaemonCommand(action, options = {}) {
2746
3546
  autoworkInterval: options.autoworkInterval ? parseInt(options.autoworkInterval, 10) : options.interval ? parseInt(options.interval, 10) : void 0,
2747
3547
  routines: options.routines ? options.routines.split(",").map((r) => r.trim()) : void 0,
2748
3548
  model: options.model,
2749
- foreground: options.foreground
3549
+ foreground: options.foreground,
3550
+ verbose: options.verbose
2750
3551
  };
2751
3552
  if (act === "start") {
2752
3553
  if (options.foreground) {
@@ -2755,16 +3556,16 @@ async function runDaemonCommand(action, options = {}) {
2755
3556
  }
2756
3557
  try {
2757
3558
  const state2 = await startBackgroundDaemon(cwd, daemonOpts);
2758
- console.log(pc11.green(`
3559
+ console.log(pc12.green(`
2759
3560
  \u2713 Background agent daemon started successfully.`));
2760
- console.log(pc11.dim(` PID: ${state2.pid}`));
2761
- console.log(pc11.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
2762
- console.log(pc11.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
2763
- console.log(pc11.dim(` Routines: ${state2.routines.join(", ")}`));
2764
- console.log(pc11.dim(` Log file: .jonah-fleet/daemon.log`));
2765
- console.log(pc11.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
3561
+ console.log(pc12.dim(` PID: ${state2.pid}`));
3562
+ console.log(pc12.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
3563
+ console.log(pc12.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
3564
+ console.log(pc12.dim(` Routines: ${state2.routines.join(", ")}`));
3565
+ console.log(pc12.dim(` Log file: .jonah-fleet/daemon.log`));
3566
+ console.log(pc12.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
2766
3567
  } catch (err) {
2767
- console.error(pc11.red(`
3568
+ console.error(pc12.red(`
2768
3569
  \u2717 Failed to start daemon: ${err.message}`));
2769
3570
  process.exit(1);
2770
3571
  }
@@ -2772,18 +3573,18 @@ async function runDaemonCommand(action, options = {}) {
2772
3573
  }
2773
3574
  if (act === "stop") {
2774
3575
  if (!isDaemonRunning(cwd)) {
2775
- console.log(pc11.yellow(`
3576
+ console.log(pc12.yellow(`
2776
3577
  \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
2777
3578
  return;
2778
3579
  }
2779
3580
  const state2 = readDaemonState(cwd);
2780
- console.log(pc11.cyan(`
3581
+ console.log(pc12.cyan(`
2781
3582
  Stopping background agent daemon (PID ${state2?.pid})...`));
2782
3583
  const stopped = await stopDaemon(cwd);
2783
3584
  if (stopped) {
2784
- console.log(pc11.green(`\u2713 Local agent daemon stopped successfully.`));
3585
+ console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
2785
3586
  } else {
2786
- console.error(pc11.red(`\u2717 Could not terminate daemon process.`));
3587
+ console.error(pc12.red(`\u2717 Could not terminate daemon process.`));
2787
3588
  process.exit(1);
2788
3589
  }
2789
3590
  return;
@@ -2795,17 +3596,18 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
2795
3596
  const running = isDaemonRunning(cwd);
2796
3597
  const state = readDaemonState(cwd);
2797
3598
  const activeWorktrees = await listActiveWorktrees(cwd);
2798
- console.log(pc11.cyan(`
3599
+ console.log(pc12.cyan(`
2799
3600
  \u{1F916} Jonah Fleet Local Daemon Status
2800
3601
  `));
2801
3602
  if (running && state) {
2802
- console.log(` Status: ${pc11.green(pc11.bold("RUNNING"))}`);
3603
+ console.log(` Status: ${pc12.green(pc12.bold("RUNNING"))}`);
2803
3604
  console.log(` PID: ${state.pid}`);
2804
3605
  console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
2805
3606
  console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
2806
3607
  console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
2807
3608
  console.log(` Routines: ${state.routines.join(", ")}`);
2808
- console.log(` Current State: ${state.status === "working" ? pc11.yellow("WORKING on " + state.activeRoutine) : pc11.green("IDLE")}`);
3609
+ const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
3610
+ console.log(` Current State: ${state.status === "working" ? pc12.yellow("WORKING on " + workingDesc) : pc12.green("IDLE")}`);
2809
3611
  if (state.lastReviewCheckAt) {
2810
3612
  console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
2811
3613
  }
@@ -2813,13 +3615,13 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
2813
3615
  console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
2814
3616
  }
2815
3617
  } else {
2816
- console.log(` Status: ${pc11.gray("STOPPED")}`);
2817
- console.log(pc11.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
3618
+ console.log(` Status: ${pc12.gray("STOPPED")}`);
3619
+ console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
2818
3620
  }
2819
3621
  console.log(`
2820
3622
  Active Worktrees: ${activeWorktrees.length}`);
2821
3623
  for (const wt of activeWorktrees) {
2822
- console.log(pc11.dim(` - [${wt.branch}] ${wt.path}`));
3624
+ console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
2823
3625
  }
2824
3626
  console.log("");
2825
3627
  }
@@ -2827,10 +3629,10 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
2827
3629
  // src/index.ts
2828
3630
  var program = new Command();
2829
3631
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
2830
- program.command("run <routine>").description("Run a specific prompt routine locally in an isolated git worktree").option("-i, --issue <number>", "Targeted issue number for autowork").option("-p, --pr <number>", "Targeted pull request number for peer-review").option("-m, --model <model>", "LLM model override (defaults to gemini-3.7-flash-high)").option("--timeout <duration>", "CLI execution print timeout (default: 30m)").option("--no-worktree", "Execute directly in current directory without creating a git worktree").option("--keep-worktree", "Preserve the git worktree after routine execution completes").option("-d, --dry-run", "Preview prompt and execution parameters without launching agent").action(async (routine, options) => {
3632
+ program.command("run <routine>").description("Run a specific prompt routine locally in an isolated git worktree").option("-i, --issue <number>", "Targeted issue number for autowork").option("-p, --pr <number>", "Targeted pull request number for peer-review").option("-m, --model <model>", "LLM model override (defaults to gemini-3.7-flash-high)").option("--timeout <duration>", "CLI execution print timeout (default: 30m)").option("--no-worktree", "Execute directly in current directory without creating a git worktree").option("--keep-worktree", "Preserve the git worktree after routine execution completes").option("-d, --dry-run", "Preview prompt and execution parameters without launching agent").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (routine, options) => {
2831
3633
  await runRoutineCommand(routine, options);
2832
3634
  });
2833
- program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Legacy global polling interval in minutes (default: 30)").option("--review-interval <minutes>", "Peer Review watchdog cadence in minutes (default: 3)").option("--autowork-interval <minutes>", "Autowork backlog cadence in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: peer-review,autowork)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").action(async (action, options) => {
3635
+ program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Legacy global polling interval in minutes (default: 30)").option("--review-interval <minutes>", "Peer Review watchdog cadence in minutes (default: 3)").option("--autowork-interval <minutes>", "Autowork backlog cadence in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: peer-review,autowork)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").option("-v, --verbose", "Stream raw agent tokens and logs directly to stdout").action(async (action, options) => {
2834
3636
  await runDaemonCommand(action, options);
2835
3637
  });
2836
3638
  program.command("init").description("Initialize Jonah Fleet configuration, routines, workflows, and skills in the current repo").option("-p, --preset <preset>", "Preset profile to install (minimal | standard | full)", "standard").option("-f, --force", "Force overwrite existing files", false).option("--stack <stack>", "Override detected tech stack name").option("--package-manager <pm>", "Override package manager (npm, pnpm, yarn, bun, uv, poetry, cargo, go)").option("--test-cmd <cmd>", "Override test execution command").option("--build-cmd <cmd>", "Override build execution command").option("--interactive", "Force interactive prompts for stack configuration").option("--no-interactive", "Disable interactive prompts").action(async (options) => {