jonah-fleet 1.4.1 → 1.5.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 +63 -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 +578 -97
- package/dist/lib/daemon.d.ts +2 -0
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/presets.d.ts +1 -1
- package/dist/lib/presets.d.ts.map +1 -1
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/terminal-card.d.ts +85 -0
- package/dist/lib/terminal-card.d.ts.map +1 -0
- package/package.json +1 -1
- package/templates/docs/AGENTS.template.md +6 -0
- package/templates/prompts/ORCHESTRATION.md +3 -0
- package/templates/prompts/autowork.md +10 -1
- package/templates/skills/grill-me/SKILL.md +64 -0
- package/templates/skills/grill-me/agents/openai.yaml +5 -0
- package/templates/skills/triage/SKILL.md +1 -1
- package/templates/workflows/trigger-review-routine.yml +4 -3
package/dist/index.js
CHANGED
|
@@ -49,7 +49,8 @@ var PRESET_CONFIGS = {
|
|
|
49
49
|
"diagnosing-bugs",
|
|
50
50
|
"resolving-merge-conflicts",
|
|
51
51
|
"writing-for-agents",
|
|
52
|
-
"triage"
|
|
52
|
+
"triage",
|
|
53
|
+
"grill-me"
|
|
53
54
|
]
|
|
54
55
|
},
|
|
55
56
|
full: {
|
|
@@ -71,6 +72,7 @@ var PRESET_CONFIGS = {
|
|
|
71
72
|
"resolving-merge-conflicts",
|
|
72
73
|
"writing-for-agents",
|
|
73
74
|
"triage",
|
|
75
|
+
"grill-me",
|
|
74
76
|
"to-spec",
|
|
75
77
|
"to-tickets"
|
|
76
78
|
]
|
|
@@ -90,7 +92,7 @@ var ROUTINE_TO_WORKFLOW_MAP = {
|
|
|
90
92
|
"product-planning": [],
|
|
91
93
|
"analytics-review": []
|
|
92
94
|
};
|
|
93
|
-
var FLEET_VERSION = "1.
|
|
95
|
+
var FLEET_VERSION = "1.5.0";
|
|
94
96
|
var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
|
|
95
97
|
|
|
96
98
|
// src/lib/manifest.ts
|
|
@@ -2155,13 +2157,13 @@ async function runTelemetry(options = {}) {
|
|
|
2155
2157
|
}
|
|
2156
2158
|
|
|
2157
2159
|
// src/commands/run.ts
|
|
2158
|
-
import
|
|
2160
|
+
import pc10 from "picocolors";
|
|
2159
2161
|
|
|
2160
2162
|
// src/lib/runner.ts
|
|
2161
|
-
import
|
|
2162
|
-
import
|
|
2163
|
+
import fs12 from "fs";
|
|
2164
|
+
import path12 from "path";
|
|
2163
2165
|
import os2 from "os";
|
|
2164
|
-
import { spawn } from "child_process";
|
|
2166
|
+
import { spawn, execSync as execSync2 } from "child_process";
|
|
2165
2167
|
|
|
2166
2168
|
// src/lib/worktree.ts
|
|
2167
2169
|
import fs10 from "fs";
|
|
@@ -2284,18 +2286,369 @@ async function cleanupStaleWorktrees(repoRoot) {
|
|
|
2284
2286
|
return cleaned;
|
|
2285
2287
|
}
|
|
2286
2288
|
|
|
2289
|
+
// src/lib/terminal-card.ts
|
|
2290
|
+
import fs11 from "fs";
|
|
2291
|
+
import path11 from "path";
|
|
2292
|
+
import pc9 from "picocolors";
|
|
2293
|
+
function sanitizeWorktreePaths(text) {
|
|
2294
|
+
let cleaned = text.replace(/file:\/\/\/[^\s"'()]+?\/\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, "");
|
|
2295
|
+
cleaned = cleaned.replace(/(?:^|[\s"'(`[])(?:\/[^\s"'()]+?)?\.jonah-fleet\/worktrees\/[^/\s"'()]+\//g, (match) => {
|
|
2296
|
+
const prefix = match.charAt(0);
|
|
2297
|
+
return prefix === "/" ? "" : prefix;
|
|
2298
|
+
});
|
|
2299
|
+
cleaned = cleaned.replace(/\[`?([^`\]]+?)`?\]\(file:\/\/\/[^\s)]+\)/g, "`$1`");
|
|
2300
|
+
return cleaned;
|
|
2301
|
+
}
|
|
2302
|
+
function extractExecutionSummary(output) {
|
|
2303
|
+
const summaryHeaderRegex = /#\s+([A-Za-z0-9\s_-]+?Execution\s+Summary[\s\S]*)/i;
|
|
2304
|
+
const match = output.match(summaryHeaderRegex);
|
|
2305
|
+
if (!match) return null;
|
|
2306
|
+
let summary = match[1].trim();
|
|
2307
|
+
const trailingSeparators = [
|
|
2308
|
+
"\u2713 Local peer-review completed",
|
|
2309
|
+
"\u2713 Local autowork completed",
|
|
2310
|
+
"\u2713 Local agent session",
|
|
2311
|
+
"[6:",
|
|
2312
|
+
"Peer Review Watchdog:"
|
|
2313
|
+
];
|
|
2314
|
+
for (const sep of trailingSeparators) {
|
|
2315
|
+
const idx = summary.indexOf(sep);
|
|
2316
|
+
if (idx !== -1) {
|
|
2317
|
+
summary = summary.slice(0, idx).trim();
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
return sanitizeWorktreePaths(summary);
|
|
2321
|
+
}
|
|
2322
|
+
function findLatestRunLog(repoRoot, routine) {
|
|
2323
|
+
const logsDir = path11.join(repoRoot, ".github", "prompts", "logs", routine);
|
|
2324
|
+
if (!fs11.existsSync(logsDir)) return null;
|
|
2325
|
+
try {
|
|
2326
|
+
const files = fs11.readdirSync(logsDir).filter((f) => f.endsWith(".md") && !f.startsWith("_"));
|
|
2327
|
+
if (files.length === 0) return null;
|
|
2328
|
+
files.sort().reverse();
|
|
2329
|
+
return path11.join(logsDir, files[0]);
|
|
2330
|
+
} catch {
|
|
2331
|
+
return null;
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
function parseRunLog(logContent) {
|
|
2335
|
+
const summary = {
|
|
2336
|
+
passes: [],
|
|
2337
|
+
actions: []
|
|
2338
|
+
};
|
|
2339
|
+
const lines = logContent.split("\n");
|
|
2340
|
+
for (const line of lines) {
|
|
2341
|
+
const trimmed = line.trim();
|
|
2342
|
+
if (trimmed.startsWith("|") && trimmed.includes("|")) {
|
|
2343
|
+
const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
|
|
2344
|
+
if (parts.length >= 2) {
|
|
2345
|
+
const key = parts[0].toLowerCase();
|
|
2346
|
+
const value = parts[1].replace(/`/g, "");
|
|
2347
|
+
if (key.includes("routine")) summary.routine = value;
|
|
2348
|
+
if (key.includes("target pr") || key.includes("target issue")) summary.target = value;
|
|
2349
|
+
if (key.includes("decision")) summary.decision = value;
|
|
2350
|
+
if (key.includes("result")) summary.result = value;
|
|
2351
|
+
if (key.includes("duration")) summary.duration = value;
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
let inDoD = false;
|
|
2356
|
+
let inFindings = false;
|
|
2357
|
+
let inActions = false;
|
|
2358
|
+
for (const line of lines) {
|
|
2359
|
+
const trimmed = line.trim();
|
|
2360
|
+
if (trimmed.startsWith("## Definition of Done")) {
|
|
2361
|
+
inDoD = true;
|
|
2362
|
+
inFindings = false;
|
|
2363
|
+
inActions = false;
|
|
2364
|
+
continue;
|
|
2365
|
+
} else if (trimmed.startsWith("## Code Review Findings") || trimmed.startsWith("## Findings")) {
|
|
2366
|
+
inDoD = false;
|
|
2367
|
+
inFindings = true;
|
|
2368
|
+
inActions = false;
|
|
2369
|
+
continue;
|
|
2370
|
+
} else if (trimmed.startsWith("## Execution Trace") || trimmed.startsWith("### Actions Taken")) {
|
|
2371
|
+
inDoD = false;
|
|
2372
|
+
inFindings = false;
|
|
2373
|
+
inActions = true;
|
|
2374
|
+
continue;
|
|
2375
|
+
} else if (trimmed.startsWith("## ")) {
|
|
2376
|
+
inDoD = false;
|
|
2377
|
+
inFindings = false;
|
|
2378
|
+
inActions = false;
|
|
2379
|
+
}
|
|
2380
|
+
if (inDoD && trimmed.startsWith("|") && !trimmed.includes("Criterion") && !trimmed.includes("---")) {
|
|
2381
|
+
const parts = trimmed.split("|").map((p) => p.trim()).filter(Boolean);
|
|
2382
|
+
if (parts.length >= 2) {
|
|
2383
|
+
const criterion = parts[0];
|
|
2384
|
+
const met = parts[1].toUpperCase() === "YES" || parts[1].toUpperCase() === "PASS";
|
|
2385
|
+
const evidence = parts[2] ? ` (${parts[2].slice(0, 60)}...)` : "";
|
|
2386
|
+
summary.passes?.push({
|
|
2387
|
+
name: criterion,
|
|
2388
|
+
status: met ? "pass" : "fail",
|
|
2389
|
+
detail: evidence
|
|
2390
|
+
});
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
if (inActions && (trimmed.startsWith("- ") || trimmed.startsWith("* "))) {
|
|
2394
|
+
summary.actions?.push(sanitizeWorktreePaths(trimmed.slice(2)));
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return summary;
|
|
2398
|
+
}
|
|
2399
|
+
function detectActivePhase(chunk, currentPhase = "Executing routine") {
|
|
2400
|
+
const lower = chunk.toLowerCase();
|
|
2401
|
+
if (lower.includes("\u{1F512} claimed") || lower.includes("claimed by local autowork") || lower.includes("claimed by autowork")) {
|
|
2402
|
+
return "Claimed target issue, starting implementation";
|
|
2403
|
+
}
|
|
2404
|
+
if (lower.includes("starting review (round")) {
|
|
2405
|
+
return "Claimed review window, starting review passes";
|
|
2406
|
+
}
|
|
2407
|
+
if (lower.includes("check-client-boundary")) return "Verifying React Server Component boundaries";
|
|
2408
|
+
if (lower.includes("type-check") || lower.includes("tsc --noemit")) return "Running TypeScript type checks";
|
|
2409
|
+
if (lower.includes("lint") || lower.includes("eslint")) return "Running codebase linter";
|
|
2410
|
+
if (lower.includes("test") || lower.includes("vitest") || lower.includes("jest")) return "Running automated test suite";
|
|
2411
|
+
if (lower.includes("build") || lower.includes("next build") || lower.includes("tsup")) return "Running production build verification";
|
|
2412
|
+
if (lower.includes("code-review") || lower.includes("subagent")) return "Running multi-angle code review passes";
|
|
2413
|
+
if (lower.includes("squash-merge") || lower.includes("pr merge")) return "Squash-merging target PR to main";
|
|
2414
|
+
if (lower.includes("gh issue create") || lower.includes("autonomous issue synthesis")) return "Synthesizing tracking issue";
|
|
2415
|
+
if (lower.includes("--undo") || lower.includes("draft")) return "Bouncing PR back to draft for author fixes";
|
|
2416
|
+
if (lower.includes("issue edit") || lower.includes("pr edit")) return "Linking PR & tracking issues";
|
|
2417
|
+
if (lower.includes("pr comment") || lower.includes("review summary")) return "Submitting review comment";
|
|
2418
|
+
if (lower.includes("worktree")) return "Preparing workspace worktree";
|
|
2419
|
+
return currentPhase;
|
|
2420
|
+
}
|
|
2421
|
+
function detectClaimedIssue(chunk) {
|
|
2422
|
+
const claimMatch = chunk.match(/🔒\s*Claimed[^\n#]*?#(\d+)/i);
|
|
2423
|
+
if (claimMatch) return `Issue #${claimMatch[1]}`;
|
|
2424
|
+
const ghMatch = chunk.match(/gh\s+issue\s+(?:view|edit|comment|develop)\s+(\d+)/i);
|
|
2425
|
+
if (ghMatch) return `Issue #${ghMatch[1]}`;
|
|
2426
|
+
const textMatch = chunk.match(/(?:selected|claimed|claiming|target(?:ing)?|working|candidate)\s+(?:candidate\s+)?issue\s+#?(\d+)/i);
|
|
2427
|
+
if (textMatch) return `Issue #${textMatch[1]}`;
|
|
2428
|
+
const passiveMatch = chunk.match(/issue\s+#(\d+)\s+(?:claimed|selected)/i);
|
|
2429
|
+
if (passiveMatch) return `Issue #${passiveMatch[1]}`;
|
|
2430
|
+
return null;
|
|
2431
|
+
}
|
|
2432
|
+
function detectClaimedPR(chunk) {
|
|
2433
|
+
const reviewMatch = chunk.match(/Starting\s+review[^\n#]*?#(\d+)/i);
|
|
2434
|
+
if (reviewMatch) return `PR #${reviewMatch[1]}`;
|
|
2435
|
+
const prMatch = chunk.match(/(?:selected|target|reviewing)\s+(?:target\s+)?PR:?\s*\[?PR\s*#?(\d+)/i);
|
|
2436
|
+
if (prMatch) return `PR #${prMatch[1]}`;
|
|
2437
|
+
const ghPrMatch = chunk.match(/gh\s+pr\s+(?:view|diff|checkout|review)\s+(\d+)/i);
|
|
2438
|
+
if (ghPrMatch) return `PR #${ghPrMatch[1]}`;
|
|
2439
|
+
return null;
|
|
2440
|
+
}
|
|
2441
|
+
function renderSummaryCard(options) {
|
|
2442
|
+
const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 60), 86);
|
|
2443
|
+
const horizontal = "\u2500".repeat(width - 2);
|
|
2444
|
+
const rawSummary = options.output ? extractExecutionSummary(options.output) : null;
|
|
2445
|
+
let parsedFromLog = null;
|
|
2446
|
+
if (options.repoRoot) {
|
|
2447
|
+
const latestLog = findLatestRunLog(options.repoRoot, options.routine);
|
|
2448
|
+
if (latestLog) {
|
|
2449
|
+
try {
|
|
2450
|
+
const content = fs11.readFileSync(latestLog, "utf8");
|
|
2451
|
+
parsedFromLog = parseRunLog(content);
|
|
2452
|
+
parsedFromLog.logPath = path11.relative(options.repoRoot, latestLog);
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
let target = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : "";
|
|
2458
|
+
if (!target && parsedFromLog?.target) target = parsedFromLog.target;
|
|
2459
|
+
let decision = parsedFromLog?.decision || "";
|
|
2460
|
+
if (!decision && rawSummary) {
|
|
2461
|
+
const decisionMatch = rawSummary.match(/\*\*Final Action\*\*:\s*([^\n]+)/i);
|
|
2462
|
+
if (decisionMatch) decision = decisionMatch[1].replace(/[`*]/g, "").trim();
|
|
2463
|
+
}
|
|
2464
|
+
const durationStr = options.durationMs ? `${Math.round(options.durationMs / 1e3)}s` : parsedFromLog?.duration || "";
|
|
2465
|
+
const lines = [];
|
|
2466
|
+
lines.push(pc9.cyan(`\u250C${horizontal}\u2510`));
|
|
2467
|
+
const title = ` Jonah Fleet Routine: ${pc9.bold(options.routine.toUpperCase())} `;
|
|
2468
|
+
lines.push(
|
|
2469
|
+
pc9.cyan("\u2502") + ` ${pc9.bold(pc9.white(options.routine.toUpperCase()))}` + (target ? ` \xB7 ${pc9.yellow(target)}` : "") + (durationStr ? pc9.dim(` (${durationStr})`) : "") + " ".repeat(
|
|
2470
|
+
Math.max(
|
|
2471
|
+
1,
|
|
2472
|
+
width - 4 - options.routine.length - target.length - (durationStr ? durationStr.length + 3 : 0)
|
|
2473
|
+
)
|
|
2474
|
+
) + pc9.cyan("\u2502")
|
|
2475
|
+
);
|
|
2476
|
+
if (decision) {
|
|
2477
|
+
let decisionBadge = pc9.green(`\u2714 ${decision}`);
|
|
2478
|
+
if (/bounce|draft|reject|fail/i.test(decision)) {
|
|
2479
|
+
decisionBadge = pc9.yellow(`\u26A0\uFE0F ${decision}`);
|
|
2480
|
+
} else if (/escalat/i.test(decision)) {
|
|
2481
|
+
decisionBadge = pc9.red(`\u{1F6A8} ${decision}`);
|
|
2482
|
+
}
|
|
2483
|
+
lines.push(
|
|
2484
|
+
pc9.cyan("\u2502") + ` Action: ${decisionBadge}` + " ".repeat(Math.max(1, width - 11 - decision.length)) + pc9.cyan("\u2502")
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
|
|
2488
|
+
if (rawSummary) {
|
|
2489
|
+
const summaryLines = rawSummary.split("\n");
|
|
2490
|
+
for (const rawLine of summaryLines) {
|
|
2491
|
+
const line = rawLine.trim();
|
|
2492
|
+
if (!line) continue;
|
|
2493
|
+
if (line.startsWith("# ")) continue;
|
|
2494
|
+
if (line.startsWith("---")) continue;
|
|
2495
|
+
if (line.startsWith("**Selected Target") || line.startsWith("**Final Action") || line.startsWith("**Mode**:")) {
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
if (line.startsWith("### ")) {
|
|
2499
|
+
const heading = line.replace("### ", "").trim();
|
|
2500
|
+
lines.push(
|
|
2501
|
+
pc9.cyan("\u2502") + ` ${pc9.bold(pc9.cyan(heading))}` + " ".repeat(Math.max(1, width - 3 - heading.length)) + pc9.cyan("\u2502")
|
|
2502
|
+
);
|
|
2503
|
+
} else if (line.startsWith("- ") || line.startsWith("* ")) {
|
|
2504
|
+
const item = sanitizeWorktreePaths(line.slice(2)).trim();
|
|
2505
|
+
const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
|
|
2506
|
+
const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
|
|
2507
|
+
if (plainLen <= width - 6) {
|
|
2508
|
+
lines.push(pc9.cyan("\u2502") + ` \u2022 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
|
|
2509
|
+
} else {
|
|
2510
|
+
const truncated = formatted.slice(0, width - 10) + "...";
|
|
2511
|
+
lines.push(pc9.cyan("\u2502") + ` \u2022 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
|
|
2512
|
+
}
|
|
2513
|
+
} else if (/^[0-9]+\.\s+/.test(line)) {
|
|
2514
|
+
const item = sanitizeWorktreePaths(line.replace(/^[0-9]+\.\s+/, "")).trim();
|
|
2515
|
+
const formatted = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, (_, text) => pc9.bold(text)).replace(/`([^`]+)`/g, (_, code) => pc9.yellow(code));
|
|
2516
|
+
const plainLen = item.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/\*\*([^*]+)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").length;
|
|
2517
|
+
if (plainLen <= width - 6) {
|
|
2518
|
+
lines.push(pc9.cyan("\u2502") + ` \u2714 ${formatted}` + " ".repeat(Math.max(1, width - 5 - plainLen)) + pc9.cyan("\u2502"));
|
|
2519
|
+
} else {
|
|
2520
|
+
const truncated = formatted.slice(0, width - 10) + "...";
|
|
2521
|
+
lines.push(pc9.cyan("\u2502") + ` \u2714 ${truncated}` + " ".repeat(Math.max(1, width - 5 - (width - 7))) + pc9.cyan("\u2502"));
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
} else if (parsedFromLog && parsedFromLog.passes && parsedFromLog.passes.length > 0) {
|
|
2526
|
+
lines.push(pc9.cyan("\u2502") + ` ${pc9.bold("Verification Passes:")}` + " ".repeat(Math.max(1, width - 23)) + pc9.cyan("\u2502"));
|
|
2527
|
+
for (const pass of parsedFromLog.passes.slice(0, 6)) {
|
|
2528
|
+
const icon = pass.status === "pass" ? pc9.green("\u2714") : pc9.red("\u2716");
|
|
2529
|
+
const text = `${pass.name}${pass.detail || ""}`;
|
|
2530
|
+
const plainLen = text.length + 4;
|
|
2531
|
+
const truncated = plainLen > width - 6 ? text.slice(0, width - 10) + "..." : text;
|
|
2532
|
+
lines.push(pc9.cyan("\u2502") + ` ${icon} ${truncated}` + " ".repeat(Math.max(1, width - 5 - truncated.length)) + pc9.cyan("\u2502"));
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
if (parsedFromLog?.logPath) {
|
|
2536
|
+
lines.push(pc9.cyan(`\u251C${horizontal}\u2524`));
|
|
2537
|
+
const logInfo = ` Run log: ${pc9.dim(parsedFromLog.logPath)}`;
|
|
2538
|
+
const logPlain = ` Run log: ${parsedFromLog.logPath}`;
|
|
2539
|
+
lines.push(pc9.cyan("\u2502") + logInfo + " ".repeat(Math.max(1, width - 2 - logPlain.length)) + pc9.cyan("\u2502"));
|
|
2540
|
+
}
|
|
2541
|
+
lines.push(pc9.cyan(`\u2514${horizontal}\u2518`));
|
|
2542
|
+
return lines.join("\n");
|
|
2543
|
+
}
|
|
2544
|
+
function renderErrorCard(options) {
|
|
2545
|
+
const width = Math.min(Math.max((process.stdout.columns || 80) - 4, 60), 86);
|
|
2546
|
+
const horizontal = "\u2500".repeat(width - 2);
|
|
2547
|
+
const lines = [];
|
|
2548
|
+
lines.push(pc9.red(`\u250C${horizontal}\u2510`));
|
|
2549
|
+
const target = options.pr ? ` \xB7 PR #${options.pr}` : options.issue ? ` \xB7 Issue #${options.issue}` : "";
|
|
2550
|
+
const durationStr = options.durationMs ? ` (${Math.round(options.durationMs / 1e3)}s)` : "";
|
|
2551
|
+
const routineUpper = options.routine.toUpperCase();
|
|
2552
|
+
const header = ` \u2717 Routine '${routineUpper}' Failed (Exit Code ${options.exitCode})${target}${durationStr}`;
|
|
2553
|
+
const headerPlain = ` \u2717 Routine '${routineUpper}' Failed (Exit Code ${options.exitCode})${target}${durationStr}`;
|
|
2554
|
+
lines.push(
|
|
2555
|
+
pc9.red("\u2502") + pc9.bold(pc9.red(header.slice(0, width - 3))) + " ".repeat(Math.max(1, width - 2 - headerPlain.length)) + pc9.red("\u2502")
|
|
2556
|
+
);
|
|
2557
|
+
lines.push(pc9.red(`\u251C${horizontal}\u2524`));
|
|
2558
|
+
const logPath = path11.join(options.repoRoot, ".jonah-fleet", "daemon.log");
|
|
2559
|
+
lines.push(pc9.red("\u2502") + pc9.yellow(" Recent Log Output:") + " ".repeat(Math.max(1, width - 21)) + pc9.red("\u2502"));
|
|
2560
|
+
if (fs11.existsSync(logPath)) {
|
|
2561
|
+
try {
|
|
2562
|
+
const logContent = fs11.readFileSync(logPath, "utf8");
|
|
2563
|
+
const allLines = logContent.split("\n").filter((l) => l.trim().length > 0);
|
|
2564
|
+
const tailLines = allLines.slice(-10);
|
|
2565
|
+
for (const line of tailLines) {
|
|
2566
|
+
const cleaned = sanitizeWorktreePaths(line).trim();
|
|
2567
|
+
const truncated = cleaned.length > width - 6 ? cleaned.slice(0, width - 9) + "..." : cleaned;
|
|
2568
|
+
lines.push(pc9.red("\u2502") + pc9.dim(` ${truncated}`) + " ".repeat(Math.max(1, width - 4 - truncated.length)) + pc9.red("\u2502"));
|
|
2569
|
+
}
|
|
2570
|
+
} catch {
|
|
2571
|
+
lines.push(
|
|
2572
|
+
pc9.red("\u2502") + pc9.dim(" (Could not read .jonah-fleet/daemon.log)") + " ".repeat(Math.max(1, width - 45)) + pc9.red("\u2502")
|
|
2573
|
+
);
|
|
2574
|
+
}
|
|
2575
|
+
} else {
|
|
2576
|
+
lines.push(pc9.red("\u2502") + pc9.dim(" (No daemon.log found)") + " ".repeat(Math.max(1, width - 26)) + pc9.red("\u2502"));
|
|
2577
|
+
}
|
|
2578
|
+
lines.push(pc9.red(`\u251C${horizontal}\u2524`));
|
|
2579
|
+
const relLogPath = path11.relative(options.repoRoot, logPath) || ".jonah-fleet/daemon.log";
|
|
2580
|
+
const footer = ` Full trace: ${relLogPath}`;
|
|
2581
|
+
const truncatedFooter = footer.slice(0, width - 4);
|
|
2582
|
+
lines.push(pc9.red("\u2502") + pc9.dim(truncatedFooter) + " ".repeat(Math.max(1, width - 2 - truncatedFooter.length)) + pc9.red("\u2502"));
|
|
2583
|
+
lines.push(pc9.red(`\u2514${horizontal}\u2518`));
|
|
2584
|
+
return lines.join("\n");
|
|
2585
|
+
}
|
|
2586
|
+
var TerminalSpinner = class {
|
|
2587
|
+
frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
2588
|
+
currentFrame = 0;
|
|
2589
|
+
intervalId = null;
|
|
2590
|
+
startTime = 0;
|
|
2591
|
+
message = "";
|
|
2592
|
+
isRunning = false;
|
|
2593
|
+
isTTY;
|
|
2594
|
+
constructor() {
|
|
2595
|
+
this.isTTY = Boolean(process.stderr.isTTY);
|
|
2596
|
+
}
|
|
2597
|
+
start(initialMessage) {
|
|
2598
|
+
this.message = initialMessage;
|
|
2599
|
+
this.startTime = Date.now();
|
|
2600
|
+
this.isRunning = true;
|
|
2601
|
+
if (!this.isTTY) {
|
|
2602
|
+
process.stderr.write(`[jonah-fleet] ${initialMessage}
|
|
2603
|
+
`);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
this.intervalId = setInterval(() => {
|
|
2607
|
+
this.render();
|
|
2608
|
+
}, 80);
|
|
2609
|
+
}
|
|
2610
|
+
update(newMessage) {
|
|
2611
|
+
this.message = newMessage;
|
|
2612
|
+
if (!this.isTTY) {
|
|
2613
|
+
process.stderr.write(`[jonah-fleet] ${newMessage}
|
|
2614
|
+
`);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
render() {
|
|
2618
|
+
if (!this.isRunning || !this.isTTY) return;
|
|
2619
|
+
const frame = pc9.cyan(this.frames[this.currentFrame]);
|
|
2620
|
+
this.currentFrame = (this.currentFrame + 1) % this.frames.length;
|
|
2621
|
+
const elapsedSeconds = Math.floor((Date.now() - this.startTime) / 1e3);
|
|
2622
|
+
const mins = Math.floor(elapsedSeconds / 60);
|
|
2623
|
+
const secs = elapsedSeconds % 60;
|
|
2624
|
+
const timeStr = pc9.dim(`[${mins}m ${secs < 10 ? "0" : ""}${secs}s]`);
|
|
2625
|
+
process.stderr.write(`\r\x1B[K ${frame} ${this.message} ${timeStr}`);
|
|
2626
|
+
}
|
|
2627
|
+
stop() {
|
|
2628
|
+
if (!this.isRunning) return;
|
|
2629
|
+
this.isRunning = false;
|
|
2630
|
+
if (this.intervalId) {
|
|
2631
|
+
clearInterval(this.intervalId);
|
|
2632
|
+
this.intervalId = null;
|
|
2633
|
+
}
|
|
2634
|
+
if (this.isTTY) {
|
|
2635
|
+
process.stderr.write("\r\x1B[K");
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
|
|
2287
2640
|
// src/lib/runner.ts
|
|
2288
2641
|
function discoverSkillsPrompt(targetDir) {
|
|
2289
|
-
const skillsDir =
|
|
2290
|
-
if (!
|
|
2642
|
+
const skillsDir = path12.join(targetDir, ".agents", "skills");
|
|
2643
|
+
if (!fs12.existsSync(skillsDir)) return "";
|
|
2291
2644
|
let skillsPrompt = "";
|
|
2292
2645
|
try {
|
|
2293
|
-
const entries =
|
|
2646
|
+
const entries = fs12.readdirSync(skillsDir, { withFileTypes: true });
|
|
2294
2647
|
for (const entry of entries) {
|
|
2295
2648
|
if (entry.isDirectory()) {
|
|
2296
|
-
const skillPath =
|
|
2297
|
-
const fullPath =
|
|
2298
|
-
if (
|
|
2649
|
+
const skillPath = path12.join(".agents", "skills", entry.name, "SKILL.md");
|
|
2650
|
+
const fullPath = path12.join(targetDir, skillPath);
|
|
2651
|
+
if (fs12.existsSync(fullPath)) {
|
|
2299
2652
|
skillsPrompt += `Read and follow ${skillPath}. `;
|
|
2300
2653
|
}
|
|
2301
2654
|
}
|
|
@@ -2322,14 +2675,14 @@ function buildRoutinePrompt(targetDir, routine, options = {}) {
|
|
|
2322
2675
|
return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
|
|
2323
2676
|
}
|
|
2324
2677
|
async function runLocalRoutine(options) {
|
|
2325
|
-
const targetDir =
|
|
2678
|
+
const targetDir = path12.resolve(options.targetDir);
|
|
2326
2679
|
const routine = options.routine;
|
|
2327
2680
|
const model = options.model || "gemini-3.7-flash-high";
|
|
2328
2681
|
const printTimeout = options.printTimeout || "30m";
|
|
2329
2682
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2330
2683
|
const hostname = os2.hostname();
|
|
2331
|
-
const promptFile =
|
|
2332
|
-
if (!
|
|
2684
|
+
const promptFile = path12.join(targetDir, ".github", "prompts", `${routine}.md`);
|
|
2685
|
+
if (!fs12.existsSync(promptFile)) {
|
|
2333
2686
|
throw new Error(`Routine prompt file not found: ${promptFile}`);
|
|
2334
2687
|
}
|
|
2335
2688
|
let branchName = `agent/${routine}-${timestamp}`;
|
|
@@ -2382,7 +2735,19 @@ Timeout: ${printTimeout}`,
|
|
|
2382
2735
|
];
|
|
2383
2736
|
let output = "";
|
|
2384
2737
|
let exitCode = 0;
|
|
2738
|
+
const startTime = Date.now();
|
|
2739
|
+
const logDir = path12.join(targetDir, ".jonah-fleet");
|
|
2740
|
+
fs12.mkdirSync(logDir, { recursive: true });
|
|
2741
|
+
const logFilePath = path12.join(logDir, "daemon.log");
|
|
2742
|
+
let targetLabel = options.pr ? `PR #${options.pr}` : options.issue ? `Issue #${options.issue}` : routine;
|
|
2743
|
+
let dynamicTargetDetected = Boolean(options.pr || options.issue);
|
|
2744
|
+
let activePhase = "Starting session...";
|
|
2745
|
+
const spinner = !options.verbose ? new TerminalSpinner() : null;
|
|
2746
|
+
if (spinner) {
|
|
2747
|
+
spinner.start(`${targetLabel}: ${activePhase}`);
|
|
2748
|
+
}
|
|
2385
2749
|
const cleanup = async () => {
|
|
2750
|
+
spinner?.stop();
|
|
2386
2751
|
if (worktreePath && !options.keepWorktree) {
|
|
2387
2752
|
await removeWorktree(targetDir, worktreePath, { deleteBranch: false }).catch(() => {
|
|
2388
2753
|
});
|
|
@@ -2394,6 +2759,40 @@ Timeout: ${printTimeout}`,
|
|
|
2394
2759
|
};
|
|
2395
2760
|
process.once("SIGINT", sigintHandler);
|
|
2396
2761
|
process.once("SIGTERM", sigintHandler);
|
|
2762
|
+
const processChunk = (chunk, isStderr = false) => {
|
|
2763
|
+
output += chunk;
|
|
2764
|
+
try {
|
|
2765
|
+
fs12.appendFileSync(logFilePath, chunk, "utf8");
|
|
2766
|
+
} catch {
|
|
2767
|
+
}
|
|
2768
|
+
if (!dynamicTargetDetected) {
|
|
2769
|
+
const detected = routine === "peer-review" ? detectClaimedPR(chunk) : detectClaimedIssue(chunk);
|
|
2770
|
+
if (detected) {
|
|
2771
|
+
dynamicTargetDetected = true;
|
|
2772
|
+
targetLabel = detected;
|
|
2773
|
+
options.onTargetDetected?.(detected);
|
|
2774
|
+
if (spinner) {
|
|
2775
|
+
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
if (options.onLog) {
|
|
2780
|
+
options.onLog(chunk);
|
|
2781
|
+
}
|
|
2782
|
+
if (options.verbose) {
|
|
2783
|
+
if (isStderr) {
|
|
2784
|
+
process.stderr.write(chunk);
|
|
2785
|
+
} else {
|
|
2786
|
+
process.stdout.write(chunk);
|
|
2787
|
+
}
|
|
2788
|
+
} else if (spinner) {
|
|
2789
|
+
const newPhase = detectActivePhase(chunk, activePhase);
|
|
2790
|
+
if (newPhase !== activePhase) {
|
|
2791
|
+
activePhase = newPhase;
|
|
2792
|
+
spinner.update(`${targetLabel}: ${activePhase}`);
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
};
|
|
2397
2796
|
try {
|
|
2398
2797
|
exitCode = await new Promise((resolve, reject) => {
|
|
2399
2798
|
const child = spawn("agy", args, {
|
|
@@ -2402,37 +2801,56 @@ Timeout: ${printTimeout}`,
|
|
|
2402
2801
|
stdio: ["inherit", "pipe", "pipe"]
|
|
2403
2802
|
});
|
|
2404
2803
|
child.stdout?.on("data", (data) => {
|
|
2405
|
-
|
|
2406
|
-
output += chunk;
|
|
2407
|
-
if (options.onLog) {
|
|
2408
|
-
options.onLog(chunk);
|
|
2409
|
-
} else {
|
|
2410
|
-
process.stdout.write(chunk);
|
|
2411
|
-
}
|
|
2804
|
+
processChunk(data.toString(), false);
|
|
2412
2805
|
});
|
|
2413
2806
|
child.stderr?.on("data", (data) => {
|
|
2414
|
-
|
|
2415
|
-
output += chunk;
|
|
2416
|
-
if (options.onLog) {
|
|
2417
|
-
options.onLog(chunk);
|
|
2418
|
-
} else {
|
|
2419
|
-
process.stderr.write(chunk);
|
|
2420
|
-
}
|
|
2807
|
+
processChunk(data.toString(), true);
|
|
2421
2808
|
});
|
|
2422
2809
|
child.on("error", (err) => {
|
|
2810
|
+
spinner?.stop();
|
|
2423
2811
|
reject(err);
|
|
2424
2812
|
});
|
|
2425
2813
|
child.on("close", (code) => {
|
|
2814
|
+
spinner?.stop();
|
|
2426
2815
|
resolve(code ?? 0);
|
|
2427
2816
|
});
|
|
2428
2817
|
});
|
|
2429
2818
|
} finally {
|
|
2819
|
+
spinner?.stop();
|
|
2430
2820
|
process.removeListener("SIGINT", sigintHandler);
|
|
2431
2821
|
process.removeListener("SIGTERM", sigintHandler);
|
|
2432
2822
|
if (!options.keepWorktree) {
|
|
2433
2823
|
await cleanup();
|
|
2434
2824
|
}
|
|
2435
2825
|
}
|
|
2826
|
+
if (options.showCard !== false && !options.verbose) {
|
|
2827
|
+
const durationMs = Date.now() - startTime;
|
|
2828
|
+
const effectiveIssue = options.issue || (targetLabel.startsWith("Issue #") ? targetLabel.replace("Issue #", "") : void 0);
|
|
2829
|
+
const effectivePR = options.pr || (targetLabel.startsWith("PR #") ? targetLabel.replace("PR #", "") : void 0);
|
|
2830
|
+
if (exitCode === 0) {
|
|
2831
|
+
console.log(
|
|
2832
|
+
"\n" + renderSummaryCard({
|
|
2833
|
+
routine,
|
|
2834
|
+
output,
|
|
2835
|
+
repoRoot: targetDir,
|
|
2836
|
+
issue: effectiveIssue,
|
|
2837
|
+
pr: effectivePR,
|
|
2838
|
+
durationMs
|
|
2839
|
+
}) + "\n"
|
|
2840
|
+
);
|
|
2841
|
+
} else {
|
|
2842
|
+
console.log(
|
|
2843
|
+
"\n" + renderErrorCard({
|
|
2844
|
+
routine,
|
|
2845
|
+
exitCode,
|
|
2846
|
+
repoRoot: targetDir,
|
|
2847
|
+
issue: effectiveIssue,
|
|
2848
|
+
pr: effectivePR,
|
|
2849
|
+
durationMs
|
|
2850
|
+
}) + "\n"
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2436
2854
|
return {
|
|
2437
2855
|
success: exitCode === 0,
|
|
2438
2856
|
exitCode,
|
|
@@ -2448,28 +2866,31 @@ async function runRoutineCommand(routine, options = {}) {
|
|
|
2448
2866
|
const manifest = loadManifest(cwd);
|
|
2449
2867
|
if (!manifest) {
|
|
2450
2868
|
console.warn(
|
|
2451
|
-
|
|
2869
|
+
pc10.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
|
|
2452
2870
|
);
|
|
2453
2871
|
} else if (manifest.routines && manifest.routines[routine] === false) {
|
|
2454
2872
|
console.warn(
|
|
2455
|
-
|
|
2873
|
+
pc10.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
|
|
2456
2874
|
);
|
|
2457
2875
|
}
|
|
2458
|
-
console.log(
|
|
2459
|
-
\u{1F680} Launching local agent session for routine: ${
|
|
2876
|
+
console.log(pc10.cyan(`
|
|
2877
|
+
\u{1F680} Launching local agent session for routine: ${pc10.bold(routine)}`));
|
|
2460
2878
|
if (options.issue) {
|
|
2461
|
-
console.log(
|
|
2879
|
+
console.log(pc10.dim(` Target issue: #${options.issue}`));
|
|
2462
2880
|
}
|
|
2463
2881
|
if (options.pr) {
|
|
2464
|
-
console.log(
|
|
2882
|
+
console.log(pc10.dim(` Target pull request: #${options.pr}`));
|
|
2465
2883
|
}
|
|
2466
2884
|
if (options.model) {
|
|
2467
|
-
console.log(
|
|
2885
|
+
console.log(pc10.dim(` Model override: ${options.model}`));
|
|
2886
|
+
}
|
|
2887
|
+
if (options.verbose) {
|
|
2888
|
+
console.log(pc10.dim(` Verbose output: Enabled (streaming raw tokens)`));
|
|
2468
2889
|
}
|
|
2469
2890
|
if (options.worktree !== false) {
|
|
2470
|
-
console.log(
|
|
2891
|
+
console.log(pc10.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
|
|
2471
2892
|
} else {
|
|
2472
|
-
console.log(
|
|
2893
|
+
console.log(pc10.yellow(` Workspace isolation: Disabled (running in current directory)`));
|
|
2473
2894
|
}
|
|
2474
2895
|
console.log("");
|
|
2475
2896
|
try {
|
|
@@ -2482,59 +2903,60 @@ async function runRoutineCommand(routine, options = {}) {
|
|
|
2482
2903
|
printTimeout: options.timeout,
|
|
2483
2904
|
noWorktree: options.worktree === false,
|
|
2484
2905
|
keepWorktree: options.keepWorktree,
|
|
2485
|
-
dryRun: options.dryRun
|
|
2906
|
+
dryRun: options.dryRun,
|
|
2907
|
+
verbose: options.verbose
|
|
2486
2908
|
});
|
|
2487
2909
|
if (options.dryRun) {
|
|
2488
|
-
console.log(
|
|
2910
|
+
console.log(pc10.green(result.output));
|
|
2489
2911
|
return;
|
|
2490
2912
|
}
|
|
2491
2913
|
if (result.success) {
|
|
2492
|
-
console.log(
|
|
2914
|
+
console.log(pc10.green(`
|
|
2493
2915
|
\u2713 Local agent session for '${routine}' completed successfully.`));
|
|
2494
2916
|
} else {
|
|
2495
|
-
console.error(
|
|
2917
|
+
console.error(pc10.red(`
|
|
2496
2918
|
\u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
|
|
2497
2919
|
process.exit(result.exitCode);
|
|
2498
2920
|
}
|
|
2499
2921
|
} catch (error) {
|
|
2500
|
-
console.error(
|
|
2922
|
+
console.error(pc10.red(`
|
|
2501
2923
|
\u2717 Failed to execute routine '${routine}': ${error.message}`));
|
|
2502
2924
|
process.exit(1);
|
|
2503
2925
|
}
|
|
2504
2926
|
}
|
|
2505
2927
|
|
|
2506
2928
|
// src/commands/daemon.ts
|
|
2507
|
-
import
|
|
2929
|
+
import pc12 from "picocolors";
|
|
2508
2930
|
|
|
2509
2931
|
// src/lib/daemon.ts
|
|
2510
|
-
import
|
|
2511
|
-
import
|
|
2932
|
+
import fs13 from "fs";
|
|
2933
|
+
import path13 from "path";
|
|
2512
2934
|
import { spawn as spawn2, execFile as execFile3 } from "child_process";
|
|
2513
2935
|
import { promisify as promisify3 } from "util";
|
|
2514
|
-
import
|
|
2936
|
+
import pc11 from "picocolors";
|
|
2515
2937
|
var execFileAsync3 = promisify3(execFile3);
|
|
2516
2938
|
function getDaemonStatePath(repoRoot) {
|
|
2517
|
-
return
|
|
2939
|
+
return path13.join(repoRoot, ".jonah-fleet", "daemon.json");
|
|
2518
2940
|
}
|
|
2519
2941
|
function readDaemonState(repoRoot) {
|
|
2520
2942
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2521
|
-
if (!
|
|
2943
|
+
if (!fs13.existsSync(statePath)) return null;
|
|
2522
2944
|
try {
|
|
2523
|
-
return JSON.parse(
|
|
2945
|
+
return JSON.parse(fs13.readFileSync(statePath, "utf8"));
|
|
2524
2946
|
} catch {
|
|
2525
2947
|
return null;
|
|
2526
2948
|
}
|
|
2527
2949
|
}
|
|
2528
2950
|
function writeDaemonState(repoRoot, state) {
|
|
2529
2951
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2530
|
-
|
|
2531
|
-
|
|
2952
|
+
fs13.mkdirSync(path13.dirname(statePath), { recursive: true });
|
|
2953
|
+
fs13.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
2532
2954
|
}
|
|
2533
2955
|
function clearDaemonState(repoRoot) {
|
|
2534
2956
|
const statePath = getDaemonStatePath(repoRoot);
|
|
2535
|
-
if (
|
|
2957
|
+
if (fs13.existsSync(statePath)) {
|
|
2536
2958
|
try {
|
|
2537
|
-
|
|
2959
|
+
fs13.unlinkSync(statePath);
|
|
2538
2960
|
} catch {
|
|
2539
2961
|
}
|
|
2540
2962
|
}
|
|
@@ -2570,9 +2992,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
|
|
|
2570
2992
|
const reviewInterval = options.reviewInterval || 3;
|
|
2571
2993
|
const autoworkInterval = options.autoworkInterval || options.interval || 30;
|
|
2572
2994
|
const routines = options.routines || ["peer-review", "autowork"];
|
|
2573
|
-
const logFilePath =
|
|
2574
|
-
|
|
2575
|
-
const logFd =
|
|
2995
|
+
const logFilePath = path13.join(repoRoot, ".jonah-fleet", "daemon.log");
|
|
2996
|
+
fs13.mkdirSync(path13.dirname(logFilePath), { recursive: true });
|
|
2997
|
+
const logFd = fs13.openSync(logFilePath, "a");
|
|
2576
2998
|
const cliPath = process.argv[1];
|
|
2577
2999
|
const args = [
|
|
2578
3000
|
"daemon",
|
|
@@ -2587,6 +3009,9 @@ async function startBackgroundDaemon(repoRoot, options = {}) {
|
|
|
2587
3009
|
if (options.model) {
|
|
2588
3010
|
args.push("--model", options.model);
|
|
2589
3011
|
}
|
|
3012
|
+
if (options.verbose) {
|
|
3013
|
+
args.push("--verbose");
|
|
3014
|
+
}
|
|
2590
3015
|
const child = spawn2(process.execPath, [cliPath, ...args], {
|
|
2591
3016
|
cwd: repoRoot,
|
|
2592
3017
|
detached: true,
|
|
@@ -2631,19 +3056,48 @@ async function runDaemonLoop(repoRoot, options = {}) {
|
|
|
2631
3056
|
status: "idle"
|
|
2632
3057
|
};
|
|
2633
3058
|
writeDaemonState(repoRoot, state);
|
|
2634
|
-
console.log(
|
|
3059
|
+
console.log(pc11.cyan(`
|
|
2635
3060
|
\u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
|
|
2636
|
-
console.log(
|
|
2637
|
-
console.log(
|
|
2638
|
-
console.log(
|
|
2639
|
-
console.log(
|
|
3061
|
+
console.log(pc11.dim(` PID: ${process.pid}`));
|
|
3062
|
+
console.log(pc11.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
|
|
3063
|
+
console.log(pc11.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
|
|
3064
|
+
console.log(pc11.dim(` Working Directory: ${repoRoot}
|
|
2640
3065
|
`));
|
|
2641
3066
|
let isStopping = false;
|
|
2642
3067
|
let isWorking = false;
|
|
3068
|
+
const reviewIntervalMs = reviewInterval * 60 * 1e3;
|
|
3069
|
+
const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
|
|
3070
|
+
let nextReviewCheckTime = Date.now() + (routines.includes("peer-review") ? reviewIntervalMs : Infinity);
|
|
3071
|
+
let nextAutoworkCheckTime = Date.now() + (routines.includes("autowork") ? autoworkIntervalMs : Infinity);
|
|
3072
|
+
let lastOpenPRCount = void 0;
|
|
3073
|
+
const clearTicker = () => {
|
|
3074
|
+
if (process.stderr.isTTY && !options.verbose) {
|
|
3075
|
+
process.stderr.write("\r\x1B[K");
|
|
3076
|
+
}
|
|
3077
|
+
};
|
|
3078
|
+
const updateTicker = () => {
|
|
3079
|
+
if (isStopping || isWorking || options.verbose || !process.stderr.isTTY) return;
|
|
3080
|
+
const now = Date.now();
|
|
3081
|
+
const nextCheck = Math.min(nextReviewCheckTime, nextAutoworkCheckTime);
|
|
3082
|
+
const diffMs = Math.max(0, nextCheck - now);
|
|
3083
|
+
const remainingSecs = Math.ceil(diffMs / 1e3);
|
|
3084
|
+
const mins = Math.floor(remainingSecs / 60);
|
|
3085
|
+
const secs = remainingSecs % 60;
|
|
3086
|
+
const timeStr = `${mins}m ${secs < 10 ? "0" : ""}${secs}s`;
|
|
3087
|
+
const prStr = lastOpenPRCount !== void 0 ? ` (${lastOpenPRCount} ready PRs)` : "";
|
|
3088
|
+
process.stderr.write(
|
|
3089
|
+
`\r\x1B[K${pc11.dim("[" + (/* @__PURE__ */ new Date()).toLocaleTimeString() + "]")} \u{1F4A4} ${pc11.dim("Watchdog Idle \xB7 Next check in " + timeStr + prStr)}`
|
|
3090
|
+
);
|
|
3091
|
+
};
|
|
3092
|
+
const tickerInterval = setInterval(updateTicker, 1e3);
|
|
2643
3093
|
const handleStop = async () => {
|
|
2644
3094
|
if (isStopping) return;
|
|
2645
3095
|
isStopping = true;
|
|
2646
|
-
|
|
3096
|
+
clearInterval(tickerInterval);
|
|
3097
|
+
clearInterval(reviewTimer);
|
|
3098
|
+
clearInterval(autoworkTimer);
|
|
3099
|
+
clearTicker();
|
|
3100
|
+
console.log(pc11.yellow(`
|
|
2647
3101
|
Stopping local agent daemon...`));
|
|
2648
3102
|
clearDaemonState(repoRoot);
|
|
2649
3103
|
await cleanupStaleWorktrees(repoRoot);
|
|
@@ -2656,36 +3110,52 @@ Stopping local agent daemon...`));
|
|
|
2656
3110
|
state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2657
3111
|
writeDaemonState(repoRoot, state);
|
|
2658
3112
|
const openPRCount = await countOpenReadyPRs(repoRoot);
|
|
3113
|
+
lastOpenPRCount = openPRCount;
|
|
2659
3114
|
if (openPRCount === 0) {
|
|
2660
|
-
|
|
3115
|
+
if (options.verbose) {
|
|
3116
|
+
console.log(pc11.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
|
|
3117
|
+
}
|
|
3118
|
+
nextReviewCheckTime = Date.now() + reviewIntervalMs;
|
|
3119
|
+
updateTicker();
|
|
2661
3120
|
return;
|
|
2662
3121
|
}
|
|
2663
3122
|
try {
|
|
2664
3123
|
isWorking = true;
|
|
3124
|
+
clearTicker();
|
|
2665
3125
|
state.status = "working";
|
|
2666
3126
|
state.activeRoutine = "peer-review";
|
|
2667
3127
|
writeDaemonState(repoRoot, state);
|
|
2668
|
-
console.log(
|
|
3128
|
+
console.log(pc11.cyan(`
|
|
2669
3129
|
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
|
|
2670
3130
|
await cleanupStaleWorktrees(repoRoot);
|
|
2671
3131
|
const result = await runLocalRoutine({
|
|
2672
3132
|
targetDir: repoRoot,
|
|
2673
3133
|
routine: "peer-review",
|
|
2674
3134
|
model: options.model,
|
|
2675
|
-
|
|
3135
|
+
verbose: options.verbose,
|
|
3136
|
+
noWorktree: false,
|
|
3137
|
+
onTargetDetected: (target) => {
|
|
3138
|
+
state.activeTarget = target;
|
|
3139
|
+
writeDaemonState(repoRoot, state);
|
|
3140
|
+
}
|
|
2676
3141
|
});
|
|
2677
3142
|
if (result.success) {
|
|
2678
|
-
console.log(
|
|
3143
|
+
console.log(pc11.green(`\u2713 Local peer-review completed successfully.
|
|
3144
|
+
`));
|
|
2679
3145
|
} else {
|
|
2680
|
-
console.warn(
|
|
3146
|
+
console.warn(pc11.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.
|
|
3147
|
+
`));
|
|
2681
3148
|
}
|
|
2682
3149
|
} catch (err) {
|
|
2683
|
-
console.error(
|
|
3150
|
+
console.error(pc11.red(`\u2717 Error in peer-review: ${err.message}`));
|
|
2684
3151
|
} finally {
|
|
2685
3152
|
isWorking = false;
|
|
2686
3153
|
state.status = "idle";
|
|
2687
3154
|
state.activeRoutine = void 0;
|
|
3155
|
+
state.activeTarget = void 0;
|
|
2688
3156
|
writeDaemonState(repoRoot, state);
|
|
3157
|
+
nextReviewCheckTime = Date.now() + reviewIntervalMs;
|
|
3158
|
+
updateTicker();
|
|
2689
3159
|
}
|
|
2690
3160
|
};
|
|
2691
3161
|
const runAutoworkCheck = async () => {
|
|
@@ -2694,30 +3164,41 @@ Stopping local agent daemon...`));
|
|
|
2694
3164
|
writeDaemonState(repoRoot, state);
|
|
2695
3165
|
try {
|
|
2696
3166
|
isWorking = true;
|
|
3167
|
+
clearTicker();
|
|
2697
3168
|
state.status = "working";
|
|
2698
3169
|
state.activeRoutine = "autowork";
|
|
2699
3170
|
writeDaemonState(repoRoot, state);
|
|
2700
|
-
console.log(
|
|
3171
|
+
console.log(pc11.cyan(`
|
|
2701
3172
|
[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
|
|
2702
3173
|
await cleanupStaleWorktrees(repoRoot);
|
|
2703
3174
|
const result = await runLocalRoutine({
|
|
2704
3175
|
targetDir: repoRoot,
|
|
2705
3176
|
routine: "autowork",
|
|
2706
3177
|
model: options.model,
|
|
2707
|
-
|
|
3178
|
+
verbose: options.verbose,
|
|
3179
|
+
noWorktree: false,
|
|
3180
|
+
onTargetDetected: (target) => {
|
|
3181
|
+
state.activeTarget = target;
|
|
3182
|
+
writeDaemonState(repoRoot, state);
|
|
3183
|
+
}
|
|
2708
3184
|
});
|
|
2709
3185
|
if (result.success) {
|
|
2710
|
-
console.log(
|
|
3186
|
+
console.log(pc11.green(`\u2713 Local autowork completed successfully.
|
|
3187
|
+
`));
|
|
2711
3188
|
} else {
|
|
2712
|
-
console.warn(
|
|
3189
|
+
console.warn(pc11.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.
|
|
3190
|
+
`));
|
|
2713
3191
|
}
|
|
2714
3192
|
} catch (err) {
|
|
2715
|
-
console.error(
|
|
3193
|
+
console.error(pc11.red(`\u2717 Error in autowork: ${err.message}`));
|
|
2716
3194
|
} finally {
|
|
2717
3195
|
isWorking = false;
|
|
2718
3196
|
state.status = "idle";
|
|
2719
3197
|
state.activeRoutine = void 0;
|
|
3198
|
+
state.activeTarget = void 0;
|
|
2720
3199
|
writeDaemonState(repoRoot, state);
|
|
3200
|
+
nextAutoworkCheckTime = Date.now() + autoworkIntervalMs;
|
|
3201
|
+
updateTicker();
|
|
2721
3202
|
}
|
|
2722
3203
|
};
|
|
2723
3204
|
if (routines.includes("peer-review")) {
|
|
@@ -2726,8 +3207,6 @@ Stopping local agent daemon...`));
|
|
|
2726
3207
|
if (routines.includes("autowork")) {
|
|
2727
3208
|
await runAutoworkCheck();
|
|
2728
3209
|
}
|
|
2729
|
-
const reviewIntervalMs = reviewInterval * 60 * 1e3;
|
|
2730
|
-
const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
|
|
2731
3210
|
const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
|
|
2732
3211
|
const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
|
|
2733
3212
|
await new Promise(() => {
|
|
@@ -2744,7 +3223,8 @@ async function runDaemonCommand(action, options = {}) {
|
|
|
2744
3223
|
autoworkInterval: options.autoworkInterval ? parseInt(options.autoworkInterval, 10) : options.interval ? parseInt(options.interval, 10) : void 0,
|
|
2745
3224
|
routines: options.routines ? options.routines.split(",").map((r) => r.trim()) : void 0,
|
|
2746
3225
|
model: options.model,
|
|
2747
|
-
foreground: options.foreground
|
|
3226
|
+
foreground: options.foreground,
|
|
3227
|
+
verbose: options.verbose
|
|
2748
3228
|
};
|
|
2749
3229
|
if (act === "start") {
|
|
2750
3230
|
if (options.foreground) {
|
|
@@ -2753,16 +3233,16 @@ async function runDaemonCommand(action, options = {}) {
|
|
|
2753
3233
|
}
|
|
2754
3234
|
try {
|
|
2755
3235
|
const state2 = await startBackgroundDaemon(cwd, daemonOpts);
|
|
2756
|
-
console.log(
|
|
3236
|
+
console.log(pc12.green(`
|
|
2757
3237
|
\u2713 Background agent daemon started successfully.`));
|
|
2758
|
-
console.log(
|
|
2759
|
-
console.log(
|
|
2760
|
-
console.log(
|
|
2761
|
-
console.log(
|
|
2762
|
-
console.log(
|
|
2763
|
-
console.log(
|
|
3238
|
+
console.log(pc12.dim(` PID: ${state2.pid}`));
|
|
3239
|
+
console.log(pc12.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
|
|
3240
|
+
console.log(pc12.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
|
|
3241
|
+
console.log(pc12.dim(` Routines: ${state2.routines.join(", ")}`));
|
|
3242
|
+
console.log(pc12.dim(` Log file: .jonah-fleet/daemon.log`));
|
|
3243
|
+
console.log(pc12.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
|
|
2764
3244
|
} catch (err) {
|
|
2765
|
-
console.error(
|
|
3245
|
+
console.error(pc12.red(`
|
|
2766
3246
|
\u2717 Failed to start daemon: ${err.message}`));
|
|
2767
3247
|
process.exit(1);
|
|
2768
3248
|
}
|
|
@@ -2770,18 +3250,18 @@ async function runDaemonCommand(action, options = {}) {
|
|
|
2770
3250
|
}
|
|
2771
3251
|
if (act === "stop") {
|
|
2772
3252
|
if (!isDaemonRunning(cwd)) {
|
|
2773
|
-
console.log(
|
|
3253
|
+
console.log(pc12.yellow(`
|
|
2774
3254
|
\u26A0\uFE0F No local agent daemon is currently running in this repository.`));
|
|
2775
3255
|
return;
|
|
2776
3256
|
}
|
|
2777
3257
|
const state2 = readDaemonState(cwd);
|
|
2778
|
-
console.log(
|
|
3258
|
+
console.log(pc12.cyan(`
|
|
2779
3259
|
Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
2780
3260
|
const stopped = await stopDaemon(cwd);
|
|
2781
3261
|
if (stopped) {
|
|
2782
|
-
console.log(
|
|
3262
|
+
console.log(pc12.green(`\u2713 Local agent daemon stopped successfully.`));
|
|
2783
3263
|
} else {
|
|
2784
|
-
console.error(
|
|
3264
|
+
console.error(pc12.red(`\u2717 Could not terminate daemon process.`));
|
|
2785
3265
|
process.exit(1);
|
|
2786
3266
|
}
|
|
2787
3267
|
return;
|
|
@@ -2793,17 +3273,18 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
|
2793
3273
|
const running = isDaemonRunning(cwd);
|
|
2794
3274
|
const state = readDaemonState(cwd);
|
|
2795
3275
|
const activeWorktrees = await listActiveWorktrees(cwd);
|
|
2796
|
-
console.log(
|
|
3276
|
+
console.log(pc12.cyan(`
|
|
2797
3277
|
\u{1F916} Jonah Fleet Local Daemon Status
|
|
2798
3278
|
`));
|
|
2799
3279
|
if (running && state) {
|
|
2800
|
-
console.log(` Status: ${
|
|
3280
|
+
console.log(` Status: ${pc12.green(pc12.bold("RUNNING"))}`);
|
|
2801
3281
|
console.log(` PID: ${state.pid}`);
|
|
2802
3282
|
console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
|
|
2803
3283
|
console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
|
|
2804
3284
|
console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
|
|
2805
3285
|
console.log(` Routines: ${state.routines.join(", ")}`);
|
|
2806
|
-
|
|
3286
|
+
const workingDesc = state.activeRoutine + (state.activeTarget ? ` (${pc12.bold(state.activeTarget)})` : "");
|
|
3287
|
+
console.log(` Current State: ${state.status === "working" ? pc12.yellow("WORKING on " + workingDesc) : pc12.green("IDLE")}`);
|
|
2807
3288
|
if (state.lastReviewCheckAt) {
|
|
2808
3289
|
console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
|
|
2809
3290
|
}
|
|
@@ -2811,13 +3292,13 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
|
2811
3292
|
console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
|
|
2812
3293
|
}
|
|
2813
3294
|
} else {
|
|
2814
|
-
console.log(` Status: ${
|
|
2815
|
-
console.log(
|
|
3295
|
+
console.log(` Status: ${pc12.gray("STOPPED")}`);
|
|
3296
|
+
console.log(pc12.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
|
|
2816
3297
|
}
|
|
2817
3298
|
console.log(`
|
|
2818
3299
|
Active Worktrees: ${activeWorktrees.length}`);
|
|
2819
3300
|
for (const wt of activeWorktrees) {
|
|
2820
|
-
console.log(
|
|
3301
|
+
console.log(pc12.dim(` - [${wt.branch}] ${wt.path}`));
|
|
2821
3302
|
}
|
|
2822
3303
|
console.log("");
|
|
2823
3304
|
}
|
|
@@ -2825,10 +3306,10 @@ Stopping background agent daemon (PID ${state2?.pid})...`));
|
|
|
2825
3306
|
// src/index.ts
|
|
2826
3307
|
var program = new Command();
|
|
2827
3308
|
program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
|
|
2828
|
-
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) => {
|
|
3309
|
+
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) => {
|
|
2829
3310
|
await runRoutineCommand(routine, options);
|
|
2830
3311
|
});
|
|
2831
|
-
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) => {
|
|
3312
|
+
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) => {
|
|
2832
3313
|
await runDaemonCommand(action, options);
|
|
2833
3314
|
});
|
|
2834
3315
|
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) => {
|