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/CHANGELOG.md +95 -0
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.d.ts.map +1 -1
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/index.js +929 -127
- package/dist/lib/daemon.d.ts +35 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/evals.d.ts +56 -1
- package/dist/lib/evals.d.ts.map +1 -1
- package/dist/lib/presets.d.ts +1 -1
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/telemetry.d.ts +13 -0
- package/dist/lib/telemetry.d.ts.map +1 -1
- package/dist/lib/terminal-card.d.ts +99 -0
- package/dist/lib/terminal-card.d.ts.map +1 -0
- package/package.json +4 -4
- package/templates/evals/ambiguity-benchmark.json +92 -0
- package/templates/prompts/ORCHESTRATION.md +25 -9
- package/templates/prompts/_prompt-template.md +13 -1
- package/templates/prompts/autowork.md +33 -7
- package/templates/prompts/optimizer.md +3 -0
- package/templates/workflows/autowork-cron.yml +33 -0
- package/templates/workflows/trigger-autowork-manual.yml +2 -0
- package/templates/workflows/trigger-autowork-on-bug.yml +2 -0
- package/templates/workflows/trigger-autowork-on-merge.yml +2 -0
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.
|
|
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
|
|
2211
|
+
import pc10 from "picocolors";
|
|
2161
2212
|
|
|
2162
2213
|
// src/lib/runner.ts
|
|
2163
|
-
import
|
|
2164
|
-
import
|
|
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 =
|
|
2292
|
-
if (!
|
|
2856
|
+
const skillsDir = path12.join(targetDir, ".agents", "skills");
|
|
2857
|
+
if (!fs12.existsSync(skillsDir)) return "";
|
|
2293
2858
|
let skillsPrompt = "";
|
|
2294
2859
|
try {
|
|
2295
|
-
const entries =
|
|
2860
|
+
const entries = fs12.readdirSync(skillsDir, { withFileTypes: true });
|
|
2296
2861
|
for (const entry of entries) {
|
|
2297
2862
|
if (entry.isDirectory()) {
|
|
2298
|
-
const skillPath =
|
|
2299
|
-
const fullPath =
|
|
2300
|
-
if (
|
|
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 =
|
|
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 =
|
|
2334
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
2461
|
-
\u{1F680} Launching local agent session for 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(
|
|
3093
|
+
console.log(pc10.dim(` Target issue: #${options.issue}`));
|
|
2464
3094
|
}
|
|
2465
3095
|
if (options.pr) {
|
|
2466
|
-
console.log(
|
|
3096
|
+
console.log(pc10.dim(` Target pull request: #${options.pr}`));
|
|
2467
3097
|
}
|
|
2468
3098
|
if (options.model) {
|
|
2469
|
-
console.log(
|
|
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(
|
|
3105
|
+
console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
|
|
2473
3106
|
} else {
|
|
2474
|
-
console.log(
|
|
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(
|
|
3124
|
+
console.log(pc10.green(result.output));
|
|
2491
3125
|
return;
|
|
2492
3126
|
}
|
|
2493
3127
|
if (result.success) {
|
|
2494
|
-
console.log(
|
|
3128
|
+
console.log(pc10.green(`
|
|
2495
3129
|
\u2713 Local agent session for '${routine}' completed successfully.`));
|
|
2496
3130
|
} else {
|
|
2497
|
-
console.error(
|
|
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(
|
|
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
|
|
3143
|
+
import pc12 from "picocolors";
|
|
2510
3144
|
|
|
2511
3145
|
// src/lib/daemon.ts
|
|
2512
|
-
import
|
|
2513
|
-
import
|
|
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
|
|
3150
|
+
import pc11 from "picocolors";
|
|
2517
3151
|
var execFileAsync3 = promisify3(execFile3);
|
|
2518
3152
|
function getDaemonStatePath(repoRoot) {
|
|
2519
|
-
return
|
|
3153
|
+
return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
|
|
2520
3154
|
}
|
|
2521
3155
|
function readDaemonState(repoRoot) {
|
|
2522
3156
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2523
|
-
if (!
|
|
3157
|
+
if (!fs13.existsSync(statePath)) return null;
|
|
2524
3158
|
try {
|
|
2525
|
-
return JSON.parse(
|
|
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
|
-
|
|
2533
|
-
|
|
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 (
|
|
3171
|
+
if (fs13.existsSync(statePath)) {
|
|
2538
3172
|
try {
|
|
2539
|
-
|
|
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
|
-
|
|
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
|
|
3198
|
+
["pr", "list", "--state", "open", "--draft=false", "--json", "number,headRefName,title"],
|
|
2560
3199
|
{ cwd: repoRoot }
|
|
2561
3200
|
);
|
|
2562
|
-
|
|
3201
|
+
const prs = JSON.parse(stdout);
|
|
3202
|
+
return filterReviewablePRs(prs);
|
|
2563
3203
|
} catch {
|
|
2564
|
-
return
|
|
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 =
|
|
2576
|
-
|
|
2577
|
-
const logFd =
|
|
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(
|
|
3380
|
+
console.log(pc11.cyan(`
|
|
2637
3381
|
\u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
|
|
2638
|
-
console.log(
|
|
2639
|
-
console.log(
|
|
2640
|
-
console.log(
|
|
2641
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
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
|
-
|
|
2680
|
-
|
|
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
|
-
|
|
2689
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
3497
|
+
console.log(pc11.green(`\u2713 Local autowork completed successfully.
|
|
3498
|
+
`));
|
|
2713
3499
|
} else {
|
|
2714
|
-
console.warn(
|
|
3500
|
+
console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
|
|
3501
|
+
`));
|
|
2715
3502
|
}
|
|
2716
3503
|
} catch (err) {
|
|
2717
|
-
console.error(
|
|
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
|
|
3528
|
+
await performReviewDrain();
|
|
2727
3529
|
}
|
|
2728
|
-
if (routines.includes("autowork")) {
|
|
3530
|
+
if (!isStopping && routines.includes("autowork")) {
|
|
2729
3531
|
await runAutoworkCheck();
|
|
2730
3532
|
}
|
|
2731
|
-
const
|
|
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(
|
|
3559
|
+
console.log(pc12.green(`
|
|
2759
3560
|
\u2713 Background agent daemon started successfully.`));
|
|
2760
|
-
console.log(
|
|
2761
|
-
console.log(
|
|
2762
|
-
console.log(
|
|
2763
|
-
console.log(
|
|
2764
|
-
console.log(
|
|
2765
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3585
|
+
console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
|
|
2785
3586
|
} else {
|
|
2786
|
-
console.error(
|
|
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(
|
|
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: ${
|
|
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
|
-
|
|
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: ${
|
|
2817
|
-
console.log(
|
|
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(
|
|
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) => {
|