jonah-fleet 1.0.0 → 1.1.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 +9 -0
- package/README.md +20 -1
- package/dist/index.js +582 -32
- package/package.json +1 -1
- package/schema.json +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to `jonah-fleet` will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.1.0] - 2026-08-24
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Multi-repository fleet monitoring command (`jonah-fleet monitor` / `jonah-fleet status --fleet`).
|
|
12
|
+
- Global repository registry manager (`~/.jonah-fleet/config.json`) with `--add` and `--remove` CLI flags.
|
|
13
|
+
- Real-time active claim inspection and stale claim detection (> 6h with no open PR).
|
|
14
|
+
- 7-day rolling token spend tracking and cost estimation from routine run logs.
|
|
15
|
+
- Live watch mode (`--watch`, `--interval <sec>`) and JSON output (`--json`).
|
|
16
|
+
|
|
8
17
|
## [1.0.0] - 2026-08-24
|
|
9
18
|
|
|
10
19
|
### Added
|
package/README.md
CHANGED
|
@@ -66,6 +66,25 @@ To synchronize with the latest fleet version:
|
|
|
66
66
|
npx jonah-fleet sync
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
### 4. Multi-repo Fleet Monitoring
|
|
70
|
+
|
|
71
|
+
Monitor health, in-flight autowork claims, open PR review loops, and token usage across your entire fleet:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# Register repositories to your fleet registry
|
|
75
|
+
npx jonah-fleet monitor --add juliendurandeu/Jonah-RuPaul
|
|
76
|
+
npx jonah-fleet monitor --add juliendurandeu/jonah-newsletter-gemini
|
|
77
|
+
|
|
78
|
+
# View terminal dashboard
|
|
79
|
+
npx jonah-fleet monitor
|
|
80
|
+
|
|
81
|
+
# Live watch mode with auto-refresh
|
|
82
|
+
npx jonah-fleet monitor --watch --interval 10
|
|
83
|
+
|
|
84
|
+
# Output as JSON
|
|
85
|
+
npx jonah-fleet monitor --json
|
|
86
|
+
```
|
|
87
|
+
|
|
69
88
|
---
|
|
70
89
|
|
|
71
90
|
## 🛠️ Repository Layout
|
|
@@ -130,4 +149,4 @@ Each target project contains an `agents-manifest.json` at its root:
|
|
|
130
149
|
|
|
131
150
|
## 📄 License
|
|
132
151
|
|
|
133
|
-
MIT © Julien
|
|
152
|
+
MIT © Julien Durand
|
package/dist/index.js
CHANGED
|
@@ -80,7 +80,7 @@ var ROUTINE_TO_WORKFLOW_MAP = {
|
|
|
80
80
|
"dependency-update-security-check": ["dependency-check-cron.yml"],
|
|
81
81
|
"product-planning": []
|
|
82
82
|
};
|
|
83
|
-
var FLEET_VERSION = "1.
|
|
83
|
+
var FLEET_VERSION = "1.1.0";
|
|
84
84
|
var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
|
|
85
85
|
|
|
86
86
|
// src/lib/manifest.ts
|
|
@@ -363,75 +363,622 @@ Run 'jonah-fleet sync --force' to apply updates.
|
|
|
363
363
|
}
|
|
364
364
|
|
|
365
365
|
// src/commands/status.ts
|
|
366
|
+
import pc5 from "picocolors";
|
|
367
|
+
|
|
368
|
+
// src/commands/monitor.ts
|
|
369
|
+
import pc4 from "picocolors";
|
|
370
|
+
|
|
371
|
+
// src/lib/global-config.ts
|
|
372
|
+
import fs4 from "fs";
|
|
373
|
+
import path4 from "path";
|
|
374
|
+
import os from "os";
|
|
375
|
+
function getDefaultGlobalConfigPath() {
|
|
376
|
+
const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path4.join(os.homedir(), ".jonah-fleet");
|
|
377
|
+
return path4.join(baseDir, "config.json");
|
|
378
|
+
}
|
|
379
|
+
function loadGlobalConfig(customPath) {
|
|
380
|
+
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
381
|
+
if (!fs4.existsSync(filePath)) {
|
|
382
|
+
return { repositories: [] };
|
|
383
|
+
}
|
|
384
|
+
try {
|
|
385
|
+
const raw = fs4.readFileSync(filePath, "utf8");
|
|
386
|
+
const parsed = JSON.parse(raw);
|
|
387
|
+
return {
|
|
388
|
+
repositories: Array.isArray(parsed.repositories) ? parsed.repositories : []
|
|
389
|
+
};
|
|
390
|
+
} catch {
|
|
391
|
+
return { repositories: [] };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function saveGlobalConfig(config, customPath) {
|
|
395
|
+
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
396
|
+
const dir = path4.dirname(filePath);
|
|
397
|
+
if (!fs4.existsSync(dir)) {
|
|
398
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
399
|
+
}
|
|
400
|
+
fs4.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
401
|
+
}
|
|
402
|
+
function addGlobalRepository(repo, customPath) {
|
|
403
|
+
const config = loadGlobalConfig(customPath);
|
|
404
|
+
const normalized = repo.trim();
|
|
405
|
+
if (!normalized) return config;
|
|
406
|
+
if (!config.repositories.includes(normalized)) {
|
|
407
|
+
config.repositories.push(normalized);
|
|
408
|
+
saveGlobalConfig(config, customPath);
|
|
409
|
+
}
|
|
410
|
+
return config;
|
|
411
|
+
}
|
|
412
|
+
function removeGlobalRepository(repo, customPath) {
|
|
413
|
+
const config = loadGlobalConfig(customPath);
|
|
414
|
+
const normalized = repo.trim();
|
|
415
|
+
config.repositories = config.repositories.filter((r) => r !== normalized);
|
|
416
|
+
saveGlobalConfig(config, customPath);
|
|
417
|
+
return config;
|
|
418
|
+
}
|
|
419
|
+
function getFleetRepositories(cwd, customGlobalConfigPath) {
|
|
420
|
+
const targetDir = cwd || process.cwd();
|
|
421
|
+
const manifest = loadManifest(targetDir);
|
|
422
|
+
const manifestRepos = Array.isArray(manifest?.repositories) ? manifest.repositories : [];
|
|
423
|
+
const globalConfig = loadGlobalConfig(customGlobalConfigPath);
|
|
424
|
+
const globalRepos = globalConfig.repositories;
|
|
425
|
+
const set = /* @__PURE__ */ new Set();
|
|
426
|
+
for (const r of manifestRepos) {
|
|
427
|
+
if (r && typeof r === "string" && r.trim()) set.add(r.trim());
|
|
428
|
+
}
|
|
429
|
+
for (const r of globalRepos) {
|
|
430
|
+
if (r && typeof r === "string" && r.trim()) set.add(r.trim());
|
|
431
|
+
}
|
|
432
|
+
return Array.from(set);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/lib/fleet-query.ts
|
|
436
|
+
import { execFile } from "child_process";
|
|
437
|
+
import fs5 from "fs";
|
|
438
|
+
import path5 from "path";
|
|
439
|
+
import { promisify } from "util";
|
|
440
|
+
var execFileAsync = promisify(execFile);
|
|
441
|
+
var defaultGhExecutor = async (args) => {
|
|
442
|
+
try {
|
|
443
|
+
const { stdout } = await execFileAsync("gh", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
444
|
+
return stdout;
|
|
445
|
+
} catch (err) {
|
|
446
|
+
if (err.stdout) return err.stdout;
|
|
447
|
+
throw err;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
function parseLogMetadata(content) {
|
|
451
|
+
const lines = content.split("\n");
|
|
452
|
+
const meta = {};
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
const match = line.match(/^\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
|
|
455
|
+
if (!match) continue;
|
|
456
|
+
const key = match[1].trim().toLowerCase();
|
|
457
|
+
let val = match[2].trim().replace(/`/g, "");
|
|
458
|
+
if (key === "routine") {
|
|
459
|
+
meta.routine = val;
|
|
460
|
+
} else if (key === "timestamp") {
|
|
461
|
+
meta.timestamp = val;
|
|
462
|
+
} else if (key === "result") {
|
|
463
|
+
meta.result = val;
|
|
464
|
+
} else if (key === "input tokens") {
|
|
465
|
+
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
466
|
+
if (!isNaN(num)) meta.inputTokens = num;
|
|
467
|
+
} else if (key === "output tokens") {
|
|
468
|
+
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
469
|
+
if (!isNaN(num)) meta.outputTokens = num;
|
|
470
|
+
} else if (key === "estimated cost") {
|
|
471
|
+
const num = parseFloat(val.replace(/[^0-9.]/g, ""));
|
|
472
|
+
if (!isNaN(num)) meta.estimatedCost = num;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (meta.timestamp || meta.routine) {
|
|
476
|
+
return meta;
|
|
477
|
+
}
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
function computeTokenSpendFromLogs(logContents, now = Date.now()) {
|
|
481
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
482
|
+
const cutoff = now - SEVEN_DAYS_MS;
|
|
483
|
+
let inputTokens = 0;
|
|
484
|
+
let outputTokens = 0;
|
|
485
|
+
let cost = 0;
|
|
486
|
+
let runs = 0;
|
|
487
|
+
for (const content of logContents) {
|
|
488
|
+
const meta = parseLogMetadata(content);
|
|
489
|
+
if (!meta || !meta.timestamp) continue;
|
|
490
|
+
const logTime = new Date(meta.timestamp).getTime();
|
|
491
|
+
if (isNaN(logTime) || logTime < cutoff) continue;
|
|
492
|
+
runs++;
|
|
493
|
+
if (meta.inputTokens) inputTokens += meta.inputTokens;
|
|
494
|
+
if (meta.outputTokens) outputTokens += meta.outputTokens;
|
|
495
|
+
if (meta.estimatedCost) cost += meta.estimatedCost;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
sevenDayInputTokens: inputTokens,
|
|
499
|
+
sevenDayOutputTokens: outputTokens,
|
|
500
|
+
sevenDayTotalTokens: inputTokens + outputTokens,
|
|
501
|
+
sevenDayEstimatedCost: cost,
|
|
502
|
+
recentRunCount: runs
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
function parseClaimFromIssue(issue, openPRs = [], now = Date.now()) {
|
|
506
|
+
if (!issue.assignees || issue.assignees.length === 0) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const comments = issue.comments || [];
|
|
510
|
+
let latestClaimComment = null;
|
|
511
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
512
|
+
if (comments[i].body && comments[i].body.includes("\u{1F512} Claimed by autowork run")) {
|
|
513
|
+
latestClaimComment = comments[i];
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!latestClaimComment) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const claimTime = new Date(latestClaimComment.createdAt).getTime();
|
|
521
|
+
const ageMs = now - (isNaN(claimTime) ? now : claimTime);
|
|
522
|
+
const ageHours = Math.max(0, ageMs / (1e3 * 60 * 60));
|
|
523
|
+
const issueNumStr = `#${issue.number}`;
|
|
524
|
+
const hasOpenPR = openPRs.some((pr) => {
|
|
525
|
+
const bodyMatch = pr.body && pr.body.includes(issueNumStr);
|
|
526
|
+
const titleMatch = pr.title && pr.title.includes(issueNumStr);
|
|
527
|
+
const branchMatch = pr.headRefName && pr.headRefName.includes(`issue-${issue.number}`);
|
|
528
|
+
return bodyMatch || titleMatch || branchMatch;
|
|
529
|
+
});
|
|
530
|
+
const isStale = ageHours > 6 && !hasOpenPR;
|
|
531
|
+
return {
|
|
532
|
+
issueNumber: issue.number,
|
|
533
|
+
title: issue.title,
|
|
534
|
+
assignee: issue.assignees[0]?.login || latestClaimComment.author?.login || "unknown",
|
|
535
|
+
claimedAt: latestClaimComment.createdAt,
|
|
536
|
+
ageHours,
|
|
537
|
+
isStale,
|
|
538
|
+
url: issue.url
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor, now = Date.now()) {
|
|
542
|
+
const result = {
|
|
543
|
+
repo: repoIdentifier,
|
|
544
|
+
activeClaims: [],
|
|
545
|
+
openPRs: [],
|
|
546
|
+
tokenUsage: {
|
|
547
|
+
sevenDayInputTokens: 0,
|
|
548
|
+
sevenDayOutputTokens: 0,
|
|
549
|
+
sevenDayTotalTokens: 0,
|
|
550
|
+
sevenDayEstimatedCost: 0,
|
|
551
|
+
recentRunCount: 0
|
|
552
|
+
},
|
|
553
|
+
staleWarnings: []
|
|
554
|
+
};
|
|
555
|
+
try {
|
|
556
|
+
if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
|
|
557
|
+
const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
|
|
558
|
+
if (fs5.existsSync(manifestPath)) {
|
|
559
|
+
try {
|
|
560
|
+
const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
|
|
561
|
+
result.fleetVersion = raw.version;
|
|
562
|
+
result.preset = raw.preset;
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
try {
|
|
568
|
+
const manifestRaw = await executor([
|
|
569
|
+
"api",
|
|
570
|
+
`repos/${repoIdentifier}/contents/agents-manifest.json`
|
|
571
|
+
]);
|
|
572
|
+
const parsed = JSON.parse(manifestRaw);
|
|
573
|
+
if (parsed.content) {
|
|
574
|
+
const content = Buffer.from(parsed.content, "base64").toString("utf8");
|
|
575
|
+
const manifest = JSON.parse(content);
|
|
576
|
+
result.fleetVersion = manifest.version;
|
|
577
|
+
result.preset = manifest.preset;
|
|
578
|
+
}
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
try {
|
|
583
|
+
const prsRaw = await executor([
|
|
584
|
+
"pr",
|
|
585
|
+
"list",
|
|
586
|
+
"--repo",
|
|
587
|
+
repoIdentifier,
|
|
588
|
+
"--state",
|
|
589
|
+
"open",
|
|
590
|
+
"--json",
|
|
591
|
+
"number,title,author,isDraft,createdAt,updatedAt,reviewDecision,url,headRefName,body"
|
|
592
|
+
]);
|
|
593
|
+
const prs = JSON.parse(prsRaw);
|
|
594
|
+
if (Array.isArray(prs)) {
|
|
595
|
+
result.openPRs = prs.map((pr) => ({
|
|
596
|
+
number: pr.number,
|
|
597
|
+
title: pr.title,
|
|
598
|
+
author: pr.author?.login || "unknown",
|
|
599
|
+
isDraft: Boolean(pr.isDraft),
|
|
600
|
+
reviewDecision: pr.reviewDecision,
|
|
601
|
+
createdAt: pr.createdAt,
|
|
602
|
+
updatedAt: pr.updatedAt,
|
|
603
|
+
url: pr.url,
|
|
604
|
+
headRefName: pr.headRefName,
|
|
605
|
+
body: pr.body
|
|
606
|
+
}));
|
|
607
|
+
}
|
|
608
|
+
} catch (err) {
|
|
609
|
+
result.error = `Failed to fetch PRs: ${err.message}`;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
const issuesRaw = await executor([
|
|
613
|
+
"issue",
|
|
614
|
+
"list",
|
|
615
|
+
"--repo",
|
|
616
|
+
repoIdentifier,
|
|
617
|
+
"--state",
|
|
618
|
+
"open",
|
|
619
|
+
"--json",
|
|
620
|
+
"number,title,assignees,updatedAt,comments,url"
|
|
621
|
+
]);
|
|
622
|
+
const issues = JSON.parse(issuesRaw);
|
|
623
|
+
if (Array.isArray(issues)) {
|
|
624
|
+
for (const issue of issues) {
|
|
625
|
+
const claim = parseClaimFromIssue(issue, result.openPRs, now);
|
|
626
|
+
if (claim) {
|
|
627
|
+
result.activeClaims.push(claim);
|
|
628
|
+
if (claim.isStale) {
|
|
629
|
+
result.staleWarnings.push(
|
|
630
|
+
`Issue #${claim.issueNumber} claimed by @${claim.assignee} ${claim.ageHours.toFixed(1)}h ago with no open PR`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
|
|
638
|
+
}
|
|
639
|
+
const logContents = [];
|
|
640
|
+
if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
|
|
641
|
+
const logsDir = path5.join(repoIdentifier, ".github/prompts/logs");
|
|
642
|
+
if (fs5.existsSync(logsDir)) {
|
|
643
|
+
const collectLogs = (dir) => {
|
|
644
|
+
const entries = fs5.readdirSync(dir, { withFileTypes: true });
|
|
645
|
+
for (const entry of entries) {
|
|
646
|
+
const fullPath = path5.join(dir, entry.name);
|
|
647
|
+
if (entry.isDirectory()) {
|
|
648
|
+
collectLogs(fullPath);
|
|
649
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
650
|
+
try {
|
|
651
|
+
logContents.push(fs5.readFileSync(fullPath, "utf8"));
|
|
652
|
+
} catch {
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
collectLogs(logsDir);
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
try {
|
|
661
|
+
const treeRaw = await executor([
|
|
662
|
+
"api",
|
|
663
|
+
`repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
|
|
664
|
+
]);
|
|
665
|
+
const tree = JSON.parse(treeRaw);
|
|
666
|
+
if (Array.isArray(tree.tree)) {
|
|
667
|
+
const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-15);
|
|
668
|
+
for (const file of logFiles) {
|
|
669
|
+
try {
|
|
670
|
+
const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
|
|
671
|
+
const parsed = JSON.parse(fileRaw);
|
|
672
|
+
if (parsed.content) {
|
|
673
|
+
logContents.push(Buffer.from(parsed.content, "base64").toString("utf8"));
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
} catch {
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (logContents.length > 0) {
|
|
683
|
+
result.tokenUsage = computeTokenSpendFromLogs(logContents, now);
|
|
684
|
+
}
|
|
685
|
+
} catch (err) {
|
|
686
|
+
result.error = err.message;
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
function summarizeFleet(statuses) {
|
|
691
|
+
const summary = {
|
|
692
|
+
totalRepos: statuses.length,
|
|
693
|
+
activeClaimsCount: 0,
|
|
694
|
+
staleClaimsCount: 0,
|
|
695
|
+
openPRsCount: 0,
|
|
696
|
+
draftPRsCount: 0,
|
|
697
|
+
readyPRsCount: 0,
|
|
698
|
+
totalInputTokens7d: 0,
|
|
699
|
+
totalOutputTokens7d: 0,
|
|
700
|
+
totalTokens7d: 0,
|
|
701
|
+
totalEstimatedCost7d: 0,
|
|
702
|
+
totalRuns7d: 0
|
|
703
|
+
};
|
|
704
|
+
for (const s of statuses) {
|
|
705
|
+
summary.activeClaimsCount += s.activeClaims.length;
|
|
706
|
+
summary.staleClaimsCount += s.activeClaims.filter((c) => c.isStale).length;
|
|
707
|
+
summary.openPRsCount += s.openPRs.length;
|
|
708
|
+
summary.draftPRsCount += s.openPRs.filter((p) => p.isDraft).length;
|
|
709
|
+
summary.readyPRsCount += s.openPRs.filter((p) => !p.isDraft).length;
|
|
710
|
+
summary.totalInputTokens7d += s.tokenUsage.sevenDayInputTokens;
|
|
711
|
+
summary.totalOutputTokens7d += s.tokenUsage.sevenDayOutputTokens;
|
|
712
|
+
summary.totalTokens7d += s.tokenUsage.sevenDayTotalTokens;
|
|
713
|
+
summary.totalEstimatedCost7d += s.tokenUsage.sevenDayEstimatedCost;
|
|
714
|
+
summary.totalRuns7d += s.tokenUsage.recentRunCount;
|
|
715
|
+
}
|
|
716
|
+
return summary;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// src/lib/dashboard.ts
|
|
366
720
|
import pc3 from "picocolors";
|
|
721
|
+
function formatTokens(num) {
|
|
722
|
+
if (num >= 1e6) {
|
|
723
|
+
return `${(num / 1e6).toFixed(2)}M`;
|
|
724
|
+
}
|
|
725
|
+
if (num >= 1e3) {
|
|
726
|
+
return `${(num / 1e3).toFixed(1)}k`;
|
|
727
|
+
}
|
|
728
|
+
return num.toString();
|
|
729
|
+
}
|
|
730
|
+
function formatCurrency(amount) {
|
|
731
|
+
return `$${amount.toFixed(2)}`;
|
|
732
|
+
}
|
|
733
|
+
function renderFleetDashboard(statuses, options = {}) {
|
|
734
|
+
const summary = summarizeFleet(statuses);
|
|
735
|
+
if (options.json) {
|
|
736
|
+
return JSON.stringify(
|
|
737
|
+
{
|
|
738
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
739
|
+
summary,
|
|
740
|
+
repositories: statuses
|
|
741
|
+
},
|
|
742
|
+
null,
|
|
743
|
+
2
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
const lines = [];
|
|
747
|
+
lines.push(pc3.bold(pc3.cyan("\n\u{1F4CA} Jonah Fleet Multi-Repo Monitor\n")));
|
|
748
|
+
if (statuses.length === 0) {
|
|
749
|
+
lines.push(pc3.yellow(" No repositories configured in fleet registry."));
|
|
750
|
+
lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
|
|
751
|
+
return lines.join("\n");
|
|
752
|
+
}
|
|
753
|
+
for (const s of statuses) {
|
|
754
|
+
const versionStr = s.fleetVersion ? `v${s.fleetVersion}` : "unmanaged";
|
|
755
|
+
const presetStr = s.preset ? `preset: ${s.preset}` : "";
|
|
756
|
+
const headerInfo = [versionStr, presetStr].filter(Boolean).join(", ");
|
|
757
|
+
lines.push(pc3.bold(`\u{1F4E6} ${pc3.cyan(s.repo)} ${pc3.gray(`(${headerInfo})`)}`));
|
|
758
|
+
if (s.error) {
|
|
759
|
+
lines.push(pc3.red(` \u274C Error: ${s.error}`));
|
|
760
|
+
}
|
|
761
|
+
lines.push(pc3.bold(" \u{1F512} Active Claims:"));
|
|
762
|
+
if (s.activeClaims.length === 0) {
|
|
763
|
+
lines.push(pc3.gray(" None (idle)"));
|
|
764
|
+
} else {
|
|
765
|
+
for (const claim of s.activeClaims) {
|
|
766
|
+
const staleTag = claim.isStale ? pc3.red(pc3.bold(" [\u26A0\uFE0F STALE CLAIM > 6h]")) : pc3.green(" [ACTIVE]");
|
|
767
|
+
const ageStr = `${claim.ageHours.toFixed(1)}h ago`;
|
|
768
|
+
lines.push(
|
|
769
|
+
` #${claim.issueNumber} ${claim.title}${staleTag}` + pc3.gray(` (claimed by @${claim.assignee}, ${ageStr})`)
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
lines.push(pc3.bold(" \u{1F500} Open PRs:"));
|
|
774
|
+
if (s.openPRs.length === 0) {
|
|
775
|
+
lines.push(pc3.gray(" None"));
|
|
776
|
+
} else {
|
|
777
|
+
for (const pr of s.openPRs) {
|
|
778
|
+
const stateTag = pr.isDraft ? pc3.yellow("[DRAFT]") : pc3.green("[READY]");
|
|
779
|
+
const reviewStr = pr.reviewDecision ? pc3.magenta(` (${pr.reviewDecision})`) : "";
|
|
780
|
+
lines.push(` #${pr.number} ${stateTag} ${pr.title}${reviewStr}` + pc3.gray(` by @${pr.author}`));
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
lines.push(pc3.bold(" \u{1F4C8} 7-Day Token Spend:"));
|
|
784
|
+
const t = s.tokenUsage;
|
|
785
|
+
lines.push(
|
|
786
|
+
` Runs: ${pc3.bold(t.recentRunCount.toString())} | Tokens: ${pc3.bold(formatTokens(t.sevenDayTotalTokens))} ` + pc3.gray(`(in: ${formatTokens(t.sevenDayInputTokens)}, out: ${formatTokens(t.sevenDayOutputTokens)})`) + ` | Cost: ${pc3.bold(pc3.green(formatCurrency(t.sevenDayEstimatedCost)))}`
|
|
787
|
+
);
|
|
788
|
+
if (s.staleWarnings.length > 0) {
|
|
789
|
+
lines.push(pc3.bold(pc3.red(" \u26A0\uFE0F Warnings:")));
|
|
790
|
+
for (const w of s.staleWarnings) {
|
|
791
|
+
lines.push(pc3.red(` - ${w}`));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
lines.push("");
|
|
795
|
+
}
|
|
796
|
+
lines.push(pc3.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"));
|
|
797
|
+
lines.push(pc3.bold("\u{1F310} Fleet Summary:"));
|
|
798
|
+
lines.push(
|
|
799
|
+
` Repositories: ${pc3.bold(summary.totalRepos.toString())} | Active Claims: ${pc3.bold(summary.activeClaimsCount.toString())} ` + (summary.staleClaimsCount > 0 ? pc3.red(`(${summary.staleClaimsCount} stale)`) : pc3.green("(0 stale)")) + ` | Open PRs: ${pc3.bold(summary.openPRsCount.toString())} ` + pc3.gray(`(${summary.draftPRsCount} draft, ${summary.readyPRsCount} ready)`)
|
|
800
|
+
);
|
|
801
|
+
lines.push(
|
|
802
|
+
` 7-Day Spend: ${pc3.bold(formatTokens(summary.totalTokens7d))} tokens ` + pc3.gray(`(in: ${formatTokens(summary.totalInputTokens7d)}, out: ${formatTokens(summary.totalOutputTokens7d)})`) + ` | Est. Cost: ${pc3.bold(pc3.green(formatCurrency(summary.totalEstimatedCost7d)))} across ${pc3.bold(summary.totalRuns7d.toString())} runs`
|
|
803
|
+
);
|
|
804
|
+
lines.push(pc3.bold("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n"));
|
|
805
|
+
return lines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/commands/monitor.ts
|
|
809
|
+
async function runMonitor(options = {}) {
|
|
810
|
+
const cwd = options.cwd || process.cwd();
|
|
811
|
+
const executor = options.executor || defaultGhExecutor;
|
|
812
|
+
if (options.add) {
|
|
813
|
+
const updated = addGlobalRepository(options.add);
|
|
814
|
+
console.log(pc4.green(`\u2713 Added ${pc4.bold(options.add)} to Jonah Fleet registry.`));
|
|
815
|
+
console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
|
|
816
|
+
`));
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
if (options.remove) {
|
|
820
|
+
const updated = removeGlobalRepository(options.remove);
|
|
821
|
+
console.log(pc4.yellow(`\u2713 Removed ${pc4.bold(options.remove)} from Jonah Fleet registry.`));
|
|
822
|
+
console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
|
|
823
|
+
`));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
let targetRepos = [];
|
|
827
|
+
if (options.repos && options.repos.length > 0) {
|
|
828
|
+
targetRepos = options.repos;
|
|
829
|
+
} else {
|
|
830
|
+
targetRepos = getFleetRepositories(cwd);
|
|
831
|
+
}
|
|
832
|
+
if (targetRepos.length === 0) {
|
|
833
|
+
try {
|
|
834
|
+
const remoteRaw = await executor(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
|
|
835
|
+
const currentRepo = remoteRaw.trim();
|
|
836
|
+
if (currentRepo) {
|
|
837
|
+
targetRepos = [currentRepo];
|
|
838
|
+
}
|
|
839
|
+
} catch {
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
if (targetRepos.length === 0) {
|
|
843
|
+
console.log(pc4.yellow("\n\u26A0\uFE0F No fleet repositories registered."));
|
|
844
|
+
console.log(pc4.cyan("Add repositories to monitor with:"));
|
|
845
|
+
console.log(pc4.gray(" jonah-fleet monitor --add owner/repo"));
|
|
846
|
+
console.log(pc4.gray("Or specify repositories directly:"));
|
|
847
|
+
console.log(pc4.gray(" jonah-fleet monitor owner/repo-1 owner/repo-2\n"));
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const pollAndRender = async () => {
|
|
851
|
+
const statuses = await Promise.all(
|
|
852
|
+
targetRepos.map((repo) => queryRepoFleetStatus(repo, executor))
|
|
853
|
+
);
|
|
854
|
+
if (options.watch && !options.json) {
|
|
855
|
+
console.clear();
|
|
856
|
+
}
|
|
857
|
+
const output = renderFleetDashboard(statuses, { json: options.json });
|
|
858
|
+
console.log(output);
|
|
859
|
+
};
|
|
860
|
+
await pollAndRender();
|
|
861
|
+
if (options.watch) {
|
|
862
|
+
const intervalSec = typeof options.interval === "number" ? options.interval : parseInt(options.interval || "10", 10) || 10;
|
|
863
|
+
const timer = setInterval(async () => {
|
|
864
|
+
await pollAndRender();
|
|
865
|
+
}, intervalSec * 1e3);
|
|
866
|
+
const handleSigint = () => {
|
|
867
|
+
clearInterval(timer);
|
|
868
|
+
process.removeListener("SIGINT", handleSigint);
|
|
869
|
+
console.log(pc4.gray("\nStopped live fleet monitoring.\n"));
|
|
870
|
+
process.exit(0);
|
|
871
|
+
};
|
|
872
|
+
process.on("SIGINT", handleSigint);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/commands/status.ts
|
|
367
877
|
async function runStatus(options = {}) {
|
|
368
878
|
const cwd = options.cwd || process.cwd();
|
|
879
|
+
if (options.fleet) {
|
|
880
|
+
await runMonitor({ cwd, json: options.json });
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
369
883
|
const manifest = loadManifest(cwd);
|
|
370
884
|
if (!manifest) {
|
|
371
|
-
|
|
885
|
+
if (options.json) {
|
|
886
|
+
console.log(JSON.stringify({ error: "No agents-manifest.json found", cwd }, null, 2));
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
console.log(pc5.yellow(`
|
|
372
890
|
\u26A0\uFE0F No agents-manifest.json found in ${cwd}. This project is not configured with Jonah Fleet.`));
|
|
373
|
-
console.log(
|
|
891
|
+
console.log(pc5.cyan(`Run 'npx jonah-fleet init' to set up autonomous agent routines.
|
|
374
892
|
`));
|
|
375
893
|
return;
|
|
376
894
|
}
|
|
377
|
-
|
|
895
|
+
const drift = checkDrift(cwd, manifest);
|
|
896
|
+
const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
|
|
897
|
+
if (options.json) {
|
|
898
|
+
console.log(
|
|
899
|
+
JSON.stringify(
|
|
900
|
+
{
|
|
901
|
+
cwd,
|
|
902
|
+
version: manifest.version,
|
|
903
|
+
fleetLatestVersion: FLEET_VERSION,
|
|
904
|
+
preset: manifest.preset,
|
|
905
|
+
autoUpdate: manifest.autoUpdate,
|
|
906
|
+
routines: manifest.routines,
|
|
907
|
+
skills: manifest.skills,
|
|
908
|
+
repositories: manifest.repositories || [],
|
|
909
|
+
drift: {
|
|
910
|
+
hasDrift,
|
|
911
|
+
...drift
|
|
912
|
+
}
|
|
913
|
+
},
|
|
914
|
+
null,
|
|
915
|
+
2
|
|
916
|
+
)
|
|
917
|
+
);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
console.log(pc5.bold(pc5.cyan(`
|
|
378
921
|
\u{1F4CA} Jonah Fleet Status for ${cwd}
|
|
379
922
|
`)));
|
|
380
|
-
console.log(` Version: ${manifest.version === FLEET_VERSION ?
|
|
381
|
-
console.log(` Preset: ${
|
|
382
|
-
console.log(` Auto-Update: ${manifest.autoUpdate?.enabled ?
|
|
383
|
-
console.log(
|
|
923
|
+
console.log(` Version: ${manifest.version === FLEET_VERSION ? pc5.green(manifest.version) : pc5.yellow(`${manifest.version} (fleet latest: ${FLEET_VERSION})`)}`);
|
|
924
|
+
console.log(` Preset: ${pc5.bold(manifest.preset)}`);
|
|
925
|
+
console.log(` Auto-Update: ${manifest.autoUpdate?.enabled ? pc5.green("Enabled (" + manifest.autoUpdate.channel + ")") : pc5.gray("Disabled")}`);
|
|
926
|
+
console.log(pc5.bold("\n Enabled Routines:"));
|
|
384
927
|
for (const [routine, enabled] of Object.entries(manifest.routines)) {
|
|
385
|
-
console.log(` - ${routine.padEnd(35)}: ${enabled ?
|
|
928
|
+
console.log(` - ${routine.padEnd(35)}: ${enabled ? pc5.green("ENABLED") : pc5.gray("DISABLED")}`);
|
|
386
929
|
}
|
|
387
|
-
console.log(
|
|
930
|
+
console.log(pc5.bold("\n Configured Skills:"));
|
|
388
931
|
for (const skill of manifest.skills) {
|
|
389
|
-
console.log(` - ${
|
|
932
|
+
console.log(` - ${pc5.cyan(skill)}`);
|
|
390
933
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
934
|
+
if (manifest.repositories && manifest.repositories.length > 0) {
|
|
935
|
+
console.log(pc5.bold("\n Fleet Repositories:"));
|
|
936
|
+
for (const repo of manifest.repositories) {
|
|
937
|
+
console.log(` - ${pc5.cyan(repo)}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
console.log(pc5.bold("\n Drift / Health:"));
|
|
394
941
|
if (!hasDrift) {
|
|
395
|
-
console.log(
|
|
942
|
+
console.log(pc5.green(" \u2713 All prompts, workflows, and skills are healthy and match fleet templates.\n"));
|
|
396
943
|
} else {
|
|
397
|
-
if (drift.missingPrompts.length > 0) console.log(
|
|
398
|
-
if (drift.modifiedPrompts.length > 0) console.log(
|
|
399
|
-
if (drift.missingWorkflows.length > 0) console.log(
|
|
400
|
-
if (drift.modifiedWorkflows.length > 0) console.log(
|
|
401
|
-
if (drift.missingSkills.length > 0) console.log(
|
|
402
|
-
console.log(
|
|
944
|
+
if (drift.missingPrompts.length > 0) console.log(pc5.red(` \u274C Missing prompts: ${drift.missingPrompts.join(", ")}`));
|
|
945
|
+
if (drift.modifiedPrompts.length > 0) console.log(pc5.yellow(` \u26A0\uFE0F Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
|
|
946
|
+
if (drift.missingWorkflows.length > 0) console.log(pc5.red(` \u274C Missing workflows: ${drift.missingWorkflows.join(", ")}`));
|
|
947
|
+
if (drift.modifiedWorkflows.length > 0) console.log(pc5.yellow(` \u26A0\uFE0F Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
|
|
948
|
+
if (drift.missingSkills.length > 0) console.log(pc5.red(` \u274C Missing skills: ${drift.missingSkills.join(", ")}`));
|
|
949
|
+
console.log(pc5.cyan("\n Run 'jonah-fleet sync' to synchronize files.\n"));
|
|
403
950
|
}
|
|
404
951
|
}
|
|
405
952
|
|
|
406
953
|
// src/commands/contribute.ts
|
|
407
954
|
import { execSync } from "child_process";
|
|
408
|
-
import
|
|
955
|
+
import pc6 from "picocolors";
|
|
409
956
|
async function runContribute(options = {}) {
|
|
410
|
-
console.log(
|
|
957
|
+
console.log(pc6.cyan(`
|
|
411
958
|
\u{1F680} Jonah Fleet Upstream Contribution Bridge
|
|
412
959
|
`));
|
|
413
960
|
const title = options.title || "fix(prompts): improve orchestrator routine handling";
|
|
414
961
|
const body = options.body || "Proposed prompt optimization discovered during autonomous execution runs.";
|
|
415
|
-
console.log(`Preparing upstream contribution PR against ${
|
|
416
|
-
console.log(`Title: ${
|
|
417
|
-
console.log(`Body: ${
|
|
962
|
+
console.log(`Preparing upstream contribution PR against ${pc6.bold("juliendurandeu/jonah-fleet")}...`);
|
|
963
|
+
console.log(`Title: ${pc6.green(title)}`);
|
|
964
|
+
console.log(`Body: ${pc6.gray(body)}
|
|
418
965
|
`);
|
|
419
966
|
try {
|
|
420
967
|
const branchName = `contrib/optimize-${Date.now()}`;
|
|
421
|
-
console.log(`Creating branch ${
|
|
968
|
+
console.log(`Creating branch ${pc6.cyan(branchName)}...`);
|
|
422
969
|
try {
|
|
423
970
|
execSync("gh auth status", { stdio: "pipe" });
|
|
424
971
|
} catch {
|
|
425
|
-
console.error(
|
|
972
|
+
console.error(pc6.red("\u274C GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first."));
|
|
426
973
|
process.exit(1);
|
|
427
974
|
}
|
|
428
|
-
console.log(
|
|
429
|
-
console.log(
|
|
975
|
+
console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to juliendurandeu/jonah-fleet.`));
|
|
976
|
+
console.log(pc6.cyan(`Command executed by optimizer routine or operator:
|
|
430
977
|
`));
|
|
431
978
|
console.log(` gh pr create --repo juliendurandeu/jonah-fleet --title "${title}" --body "${body}"
|
|
432
979
|
`);
|
|
433
980
|
} catch (err) {
|
|
434
|
-
console.error(
|
|
981
|
+
console.error(pc6.red(`\u274C Error during contribution preparation: ${err.message}`));
|
|
435
982
|
process.exit(1);
|
|
436
983
|
}
|
|
437
984
|
}
|
|
@@ -445,9 +992,12 @@ program.command("init").description("Initialize Jonah Fleet configuration, routi
|
|
|
445
992
|
program.command("sync").description("Synchronize local prompts, workflows, and skills with the installed fleet version").option("-c, --check", "Check for drift without writing changes", false).option("-f, --force", "Force update all files to match fleet version", false).action(async (options) => {
|
|
446
993
|
await runSync(options);
|
|
447
994
|
});
|
|
448
|
-
program.command("status").description("Check the status, health, and drift of installed agent routines and skills").action(async (options) => {
|
|
995
|
+
program.command("status").description("Check the status, health, and drift of installed agent routines and skills").option("-f, --fleet", "Display multi-repository fleet monitor overview", false).option("-j, --json", "Output status as JSON", false).action(async (options) => {
|
|
449
996
|
await runStatus(options);
|
|
450
997
|
});
|
|
998
|
+
program.command("monitor [repos...]").description("Monitor health, active claims, PR review loops, and token spend across fleet repositories").option("-j, --json", "Output telemetry as JSON", false).option("-w, --watch", "Live watch and refresh dashboard", false).option("-i, --interval <seconds>", "Refresh interval in seconds for watch mode", "10").option("-a, --all", "Query all registered repositories from config and manifest", false).option("--add <repo>", "Add a repository to the fleet registry").option("--remove <repo>", "Remove a repository from the fleet registry").action(async (repos, options) => {
|
|
999
|
+
await runMonitor({ ...options, repos });
|
|
1000
|
+
});
|
|
451
1001
|
program.command("contribute").description("Submit local prompt improvements back upstream to jonah-fleet").option("-t, --title <title>", "Contribution PR title").option("-b, --body <body>", "Contribution PR description").action(async (options) => {
|
|
452
1002
|
await runContribute(options);
|
|
453
1003
|
});
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"items": { "type": "string" },
|
|
36
36
|
"description": "List of core engineering skills to install/sync"
|
|
37
37
|
},
|
|
38
|
+
"repositories": {
|
|
39
|
+
"type": "array",
|
|
40
|
+
"items": { "type": "string" },
|
|
41
|
+
"description": "List of fleet repositories tracked for multi-repo monitoring"
|
|
42
|
+
},
|
|
38
43
|
"autoUpdate": {
|
|
39
44
|
"type": "object",
|
|
40
45
|
"properties": {
|