jonah-fleet 1.1.0 → 1.2.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 +26 -0
- package/README.md +40 -4
- package/dist/index.js +289 -101
- package/package.json +9 -1
- package/templates/prompts/ORCHESTRATION.md +33 -4
- package/templates/prompts/optimizer.md +44 -8
- package/templates/prompts/peer-review.md +21 -6
package/CHANGELOG.md
CHANGED
|
@@ -5,8 +5,34 @@ 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.2.0] - 2026-08-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Autonomous Issue Synthesis in `peer-review.md` and `ORCHESTRATION.md`: peer-review routine automatically synthesizes a tracking issue on GitHub (`gh issue create`) and links `Closes #N` (`gh pr edit`) before squash-merging unlinked contributor PRs, maintaining 100% issue auditability without human contributor friction.
|
|
12
|
+
- Documented required GitHub Actions workflow permissions and fork approval policies in `README.md` and post-init CLI output (`src/commands/init.ts`).
|
|
13
|
+
- Contributor PR lenience in `peer-review.md`: allow self-contained PR descriptions to serve as the spec for external contributions rather than blocking on missing `Closes #N` tracking issues.
|
|
14
|
+
- Per-agent token & cost aggregation protocol and scorecard schema in `optimizer.md` (`templates/prompts/optimizer.md` and `.github/prompts/optimizer.md`).
|
|
15
|
+
- Token Anomaly Heuristics in `optimizer.md`: defined concrete numerical thresholds for Token Surge (>50% week-over-week), Budget Hog (>75% fleet spend), Iteration Ceiling Exhaustion (>20% at `token_limit`), and Review Loop Burn (>= 3 bounce rounds).
|
|
16
|
+
- Automated preventative remediation actions and triggers in `optimizer.md` for instruction pruning, early exit/skip guards, iteration ceiling tuning, and ping-pong convergence.
|
|
17
|
+
- Documented fleet-wide Token Anomaly Triage & Remediation workflow in `ORCHESTRATION.md`.
|
|
18
|
+
- Prompt validation tests verifying token anomaly heuristics, automated remediation triggers, orchestration triage, and prompt template sync.
|
|
19
|
+
- Per-agent token and cost consumption breakdown in `src/lib/fleet-query.ts`, `src/lib/dashboard.ts`, `src/commands/status.ts`, and `src/commands/monitor.ts`.
|
|
20
|
+
- `--tokens` / `--detailed` CLI options for `jonah-fleet status` and `jonah-fleet monitor` to inspect granular per-routine token usage, iteration averages, and fleet spend share.
|
|
21
|
+
- Extended JSON telemetry with complete `byRoutine` metadata across repositories and fleet summaries.
|
|
22
|
+
- Per-routine token, cost, and iteration aggregation in `src/lib/fleet-query.ts` (`RoutineTokenSpend` and `TokenSpendInfo.byRoutine`).
|
|
23
|
+
- Support for extracting `iterationsUsed` and `duration` in `parseLogMetadata()`.
|
|
24
|
+
- Extended test coverage in `tests/fleet-query.test.ts` for per-routine token stats, fleet share calculation, and parsing edge cases.
|
|
25
|
+
|
|
26
|
+
## [1.1.1] - 2026-08-26
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- Explicit OpenAI Symphony specification lineage documentation and conceptual mapping table.
|
|
30
|
+
- Architectural comparison matrix contrasting Jonah Fleet's zero-daemon GitHub-native model against persistent multi-agent runtimes (SwarmClaw).
|
|
31
|
+
- Synchronized template orchestration docs under `templates/prompts/ORCHESTRATION.md`.
|
|
32
|
+
|
|
8
33
|
## [1.1.0] - 2026-08-24
|
|
9
34
|
|
|
35
|
+
|
|
10
36
|
### Added
|
|
11
37
|
- Multi-repository fleet monitoring command (`jonah-fleet monitor` / `jonah-fleet status --fleet`).
|
|
12
38
|
- Global repository registry manager (`~/.jonah-fleet/config.json`) with `--add` and `--remove` CLI flags.
|
package/README.md
CHANGED
|
@@ -13,12 +13,41 @@
|
|
|
13
13
|
|
|
14
14
|
`jonah-fleet` packages a complete suite of autonomous software engineering agents into a standalone repository and zero-install CLI (`npx jonah-fleet`). It turns any repository into an autonomous agent-driven development environment with:
|
|
15
15
|
|
|
16
|
-
- **Symphony-aligned Orchestration**:
|
|
16
|
+
- **Symphony-aligned Orchestration**: Built on the principles formalized by OpenAI's [Symphony spec](https://github.com/openai/symphony/blob/main/SPEC.md) — single-flight locking, dead-run claim recovery, reader/writer separation, and warm-session review loops.
|
|
17
17
|
- **Autonomous Autowork**: Issue claiming, test-driven implementation (`/tdd`), automated draft PR creation, and warm-session review synchronization.
|
|
18
18
|
- **Strict Peer Review**: Multi-angle subagent code reviews (`/code-review`), security scanning, and automated squash-merge.
|
|
19
19
|
- **Issues Housekeeping & Dependency Security**: Weekly automated sweeps for duplicate detection, triage label assignment, and vulnerability remediation.
|
|
20
20
|
- **Continuous Bi-Directional Improvement Bridge**: When local `optimizer.md` routines discover generic prompt optimizations, fixes can be submitted directly back upstream to `jonah-fleet` and distributed to all projects.
|
|
21
21
|
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 🏛️ Architecture: The Symphony Lineage
|
|
25
|
+
|
|
26
|
+
Jonah Fleet is a **GitHub-native implementation of OpenAI's [Symphony specification](https://github.com/openai/symphony/blob/main/SPEC.md)** for orchestrating autonomous coding agents against issue trackers.
|
|
27
|
+
|
|
28
|
+
Rather than requiring a persistent orchestrator daemon or complex server infrastructure, Jonah Fleet maps all Symphony primitives directly onto GitHub and ephemeral CLI agent sessions:
|
|
29
|
+
|
|
30
|
+
| Symphony Concept | Jonah Fleet Implementation |
|
|
31
|
+
|---|---|
|
|
32
|
+
| **`WORKFLOW.md`** (Repo config & prompt templates) | `AGENTS.md` (aliased as `GEMINI.md`/`CLAUDE.md`) + `.github/prompts/*.md` |
|
|
33
|
+
| **Orchestrator** (Poll, dispatch, reconcile) | GitHub Actions event triggers + scheduled cron routines (zero persistent daemons) |
|
|
34
|
+
| **Issue Tracker** | GitHub Issues with single-flight claim protocols (`🔒` claim comments) |
|
|
35
|
+
| **Agent Runner** | Ephemeral agent sessions (**Antigravity CLI `agy`** via Gemini 3.7 Flash) in fresh clones |
|
|
36
|
+
| **Reader/Writer Separation** | Autowork authors PRs; Peer Review routine is sole merge authority for product PRs |
|
|
37
|
+
| **Warm-Context Synchronization** | In-session polling & live fix loops between Autowork and Peer Review before merge |
|
|
38
|
+
| **Dead-Run Recovery** | Stale claim detection (>6h without live PR) via autowork & issues-housekeeping sweeps |
|
|
39
|
+
|
|
40
|
+
### Architectural Comparison: Jonah Fleet vs. SwarmClaw
|
|
41
|
+
|
|
42
|
+
| Dimension | ⚓ Jonah Fleet | 🦞 SwarmClaw (`@swarmclawai/swarmclaw`) |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| **Paradigm** | **Symphony-aligned, issue-driven workflow automation** | **Self-hosted multi-agent runtime & swarm platform** |
|
|
45
|
+
| **Runtime Model** | Ephemeral CLI sessions (`agy`) spun up per issue/PR | Persistent daemon / Electron desktop app / server |
|
|
46
|
+
| **State & Coordination** | GitHub Issues, PR labels, commit status checks, and run logs | Local SQLite / Postgres, live WebSockets, durable agent memory |
|
|
47
|
+
| **Agent Topology** | Specialized asynchronous routines (Autowork, Peer Review, Housekeeping, Optimizer) | Interactive agent teams, live org charts, and hierarchical delegation |
|
|
48
|
+
| **Best For** | Production software engineering pipelines & automated multi-repo maintenance | Interactive agent chat, local tool runtimes, multi-provider desktop UI |
|
|
49
|
+
|
|
50
|
+
|
|
22
51
|
---
|
|
23
52
|
|
|
24
53
|
## 🚀 Quickstart
|
|
@@ -36,7 +65,14 @@ Available presets:
|
|
|
36
65
|
- **`standard`** (default): minimal + `issues-housekeeping` + `dependency-update-security-check`
|
|
37
66
|
- **`full`**: standard + `product-planning`
|
|
38
67
|
|
|
39
|
-
### 2. Configure
|
|
68
|
+
### 2. Configure GitHub Actions Permissions
|
|
69
|
+
|
|
70
|
+
To ensure agent workflows can create and merge PRs and run without getting stuck awaiting approval:
|
|
71
|
+
|
|
72
|
+
1. **Workflow permissions**: Go to **Settings** → **Actions** → **General** → **Workflow permissions**, choose **"Read and write permissions"**, and check **"Allow GitHub Actions to create and approve pull requests"**.
|
|
73
|
+
2. **Fork pull request workflows**: Under **Actions** → **General** → **Fork pull request workflows**, configure the workflow approval policy (*e.g.* **"Require approval for first-time contributors"** or **"Run workflows without approval"** for private/internal repositories) to prevent automated runs from stalling awaiting manual approval.
|
|
74
|
+
|
|
75
|
+
### 3. Configure project context (`AGENTS.md`)
|
|
40
76
|
|
|
41
77
|
`jonah-fleet init` generates an `AGENTS.md` file (or uses your existing one). Specify your test commands, build scripts, and architecture patterns:
|
|
42
78
|
|
|
@@ -52,7 +88,7 @@ npm run type-check
|
|
|
52
88
|
```
|
|
53
89
|
```
|
|
54
90
|
|
|
55
|
-
###
|
|
91
|
+
### 4. Check health and drift
|
|
56
92
|
|
|
57
93
|
To inspect which routines and skills are active or verify alignment with latest fleet updates:
|
|
58
94
|
|
|
@@ -66,7 +102,7 @@ To synchronize with the latest fleet version:
|
|
|
66
102
|
npx jonah-fleet sync
|
|
67
103
|
```
|
|
68
104
|
|
|
69
|
-
###
|
|
105
|
+
### 5. Multi-repo Fleet Monitoring
|
|
70
106
|
|
|
71
107
|
Monitor health, in-flight autowork claims, open PR review loops, and token usage across your entire fleet:
|
|
72
108
|
|
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.2.0";
|
|
84
84
|
var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
|
|
85
85
|
|
|
86
86
|
// src/lib/manifest.ts
|
|
@@ -264,6 +264,12 @@ async function runInit(options = {}) {
|
|
|
264
264
|
result.docsInstalled.forEach((d) => console.log(` - ${d}`));
|
|
265
265
|
}
|
|
266
266
|
console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
|
|
267
|
+
console.log(pc.cyan("Next steps for GitHub repository configuration:"));
|
|
268
|
+
console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
|
|
269
|
+
console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
|
|
270
|
+
console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
|
|
271
|
+
console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
|
|
272
|
+
console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
|
|
267
273
|
}
|
|
268
274
|
|
|
269
275
|
// src/commands/sync.ts
|
|
@@ -363,79 +369,14 @@ Run 'jonah-fleet sync --force' to apply updates.
|
|
|
363
369
|
}
|
|
364
370
|
|
|
365
371
|
// src/commands/status.ts
|
|
372
|
+
import fs6 from "fs";
|
|
373
|
+
import path6 from "path";
|
|
366
374
|
import pc5 from "picocolors";
|
|
367
375
|
|
|
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
376
|
// src/lib/fleet-query.ts
|
|
436
377
|
import { execFile } from "child_process";
|
|
437
|
-
import
|
|
438
|
-
import
|
|
378
|
+
import fs4 from "fs";
|
|
379
|
+
import path4 from "path";
|
|
439
380
|
import { promisify } from "util";
|
|
440
381
|
var execFileAsync = promisify(execFile);
|
|
441
382
|
var defaultGhExecutor = async (args) => {
|
|
@@ -461,15 +402,22 @@ function parseLogMetadata(content) {
|
|
|
461
402
|
meta.timestamp = val;
|
|
462
403
|
} else if (key === "result") {
|
|
463
404
|
meta.result = val;
|
|
464
|
-
} else if (key === "input tokens") {
|
|
405
|
+
} else if (key === "input tokens" || key === "input_tokens") {
|
|
465
406
|
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
466
407
|
if (!isNaN(num)) meta.inputTokens = num;
|
|
467
|
-
} else if (key === "output tokens") {
|
|
408
|
+
} else if (key === "output tokens" || key === "output_tokens") {
|
|
468
409
|
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
469
410
|
if (!isNaN(num)) meta.outputTokens = num;
|
|
470
|
-
} else if (key === "estimated cost") {
|
|
411
|
+
} else if (key === "estimated cost" || key === "estimated_cost") {
|
|
471
412
|
const num = parseFloat(val.replace(/[^0-9.]/g, ""));
|
|
472
413
|
if (!isNaN(num)) meta.estimatedCost = num;
|
|
414
|
+
} else if (key === "iterations used" || key === "iterations" || key === "iterations_used") {
|
|
415
|
+
const raw = val.split("/")[0].trim();
|
|
416
|
+
const num = parseInt(raw.replace(/[^\d]/g, ""), 10);
|
|
417
|
+
if (!isNaN(num)) meta.iterationsUsed = num;
|
|
418
|
+
} else if (key === "duration") {
|
|
419
|
+
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
420
|
+
if (!isNaN(num)) meta.duration = num;
|
|
473
421
|
}
|
|
474
422
|
}
|
|
475
423
|
if (meta.timestamp || meta.routine) {
|
|
@@ -480,26 +428,79 @@ function parseLogMetadata(content) {
|
|
|
480
428
|
function computeTokenSpendFromLogs(logContents, now = Date.now()) {
|
|
481
429
|
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
482
430
|
const cutoff = now - SEVEN_DAYS_MS;
|
|
483
|
-
let
|
|
484
|
-
let
|
|
485
|
-
let
|
|
486
|
-
let
|
|
431
|
+
let totalInputTokens = 0;
|
|
432
|
+
let totalOutputTokens = 0;
|
|
433
|
+
let totalCost = 0;
|
|
434
|
+
let totalRuns = 0;
|
|
435
|
+
const routineMap = {};
|
|
487
436
|
for (const content of logContents) {
|
|
488
437
|
const meta = parseLogMetadata(content);
|
|
489
438
|
if (!meta || !meta.timestamp) continue;
|
|
490
439
|
const logTime = new Date(meta.timestamp).getTime();
|
|
491
440
|
if (isNaN(logTime) || logTime < cutoff) continue;
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
441
|
+
const routineName = meta.routine || "unknown";
|
|
442
|
+
const input = meta.inputTokens || 0;
|
|
443
|
+
const output = meta.outputTokens || 0;
|
|
444
|
+
const tokens = input + output;
|
|
445
|
+
const cost = meta.estimatedCost || 0;
|
|
446
|
+
totalRuns++;
|
|
447
|
+
totalInputTokens += input;
|
|
448
|
+
totalOutputTokens += output;
|
|
449
|
+
totalCost += cost;
|
|
450
|
+
if (!routineMap[routineName]) {
|
|
451
|
+
routineMap[routineName] = {
|
|
452
|
+
runCount: 0,
|
|
453
|
+
inputTokens: 0,
|
|
454
|
+
outputTokens: 0,
|
|
455
|
+
totalTokens: 0,
|
|
456
|
+
estimatedCost: 0,
|
|
457
|
+
maxTokensPerRun: 0,
|
|
458
|
+
iterationsSum: 0,
|
|
459
|
+
iterationsCount: 0
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
const acc = routineMap[routineName];
|
|
463
|
+
acc.runCount++;
|
|
464
|
+
acc.inputTokens += input;
|
|
465
|
+
acc.outputTokens += output;
|
|
466
|
+
acc.totalTokens += tokens;
|
|
467
|
+
acc.estimatedCost += cost;
|
|
468
|
+
if (tokens > acc.maxTokensPerRun) {
|
|
469
|
+
acc.maxTokensPerRun = tokens;
|
|
470
|
+
}
|
|
471
|
+
if (meta.iterationsUsed !== void 0) {
|
|
472
|
+
acc.iterationsSum += meta.iterationsUsed;
|
|
473
|
+
acc.iterationsCount++;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const sevenDayTotalTokens = totalInputTokens + totalOutputTokens;
|
|
477
|
+
const byRoutine = {};
|
|
478
|
+
for (const [routineName, acc] of Object.entries(routineMap)) {
|
|
479
|
+
const avgTokensPerRun = acc.runCount > 0 ? Math.round(acc.totalTokens / acc.runCount) : 0;
|
|
480
|
+
const fleetSharePercent = sevenDayTotalTokens > 0 ? Number((acc.totalTokens / sevenDayTotalTokens * 100).toFixed(2)) : 0;
|
|
481
|
+
const routineSpend = {
|
|
482
|
+
routine: routineName,
|
|
483
|
+
runCount: acc.runCount,
|
|
484
|
+
inputTokens: acc.inputTokens,
|
|
485
|
+
outputTokens: acc.outputTokens,
|
|
486
|
+
totalTokens: acc.totalTokens,
|
|
487
|
+
estimatedCost: Number(acc.estimatedCost.toFixed(2)),
|
|
488
|
+
avgTokensPerRun,
|
|
489
|
+
maxTokensPerRun: acc.maxTokensPerRun,
|
|
490
|
+
fleetSharePercent
|
|
491
|
+
};
|
|
492
|
+
if (acc.iterationsCount > 0) {
|
|
493
|
+
routineSpend.avgIterationsUsed = Number((acc.iterationsSum / acc.iterationsCount).toFixed(1));
|
|
494
|
+
}
|
|
495
|
+
byRoutine[routineName] = routineSpend;
|
|
496
496
|
}
|
|
497
497
|
return {
|
|
498
|
-
sevenDayInputTokens:
|
|
499
|
-
sevenDayOutputTokens:
|
|
500
|
-
sevenDayTotalTokens
|
|
501
|
-
sevenDayEstimatedCost:
|
|
502
|
-
recentRunCount:
|
|
498
|
+
sevenDayInputTokens: totalInputTokens,
|
|
499
|
+
sevenDayOutputTokens: totalOutputTokens,
|
|
500
|
+
sevenDayTotalTokens,
|
|
501
|
+
sevenDayEstimatedCost: totalCost,
|
|
502
|
+
recentRunCount: totalRuns,
|
|
503
|
+
byRoutine
|
|
503
504
|
};
|
|
504
505
|
}
|
|
505
506
|
function parseClaimFromIssue(issue, openPRs = [], now = Date.now()) {
|
|
@@ -548,16 +549,17 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
548
549
|
sevenDayOutputTokens: 0,
|
|
549
550
|
sevenDayTotalTokens: 0,
|
|
550
551
|
sevenDayEstimatedCost: 0,
|
|
551
|
-
recentRunCount: 0
|
|
552
|
+
recentRunCount: 0,
|
|
553
|
+
byRoutine: {}
|
|
552
554
|
},
|
|
553
555
|
staleWarnings: []
|
|
554
556
|
};
|
|
555
557
|
try {
|
|
556
|
-
if (
|
|
557
|
-
const manifestPath =
|
|
558
|
-
if (
|
|
558
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
559
|
+
const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
|
|
560
|
+
if (fs4.existsSync(manifestPath)) {
|
|
559
561
|
try {
|
|
560
|
-
const raw = JSON.parse(
|
|
562
|
+
const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
|
|
561
563
|
result.fleetVersion = raw.version;
|
|
562
564
|
result.preset = raw.preset;
|
|
563
565
|
} catch {
|
|
@@ -637,18 +639,18 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
|
|
|
637
639
|
if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
|
|
638
640
|
}
|
|
639
641
|
const logContents = [];
|
|
640
|
-
if (
|
|
641
|
-
const logsDir =
|
|
642
|
-
if (
|
|
642
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
643
|
+
const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
|
|
644
|
+
if (fs4.existsSync(logsDir)) {
|
|
643
645
|
const collectLogs = (dir) => {
|
|
644
|
-
const entries =
|
|
646
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
645
647
|
for (const entry of entries) {
|
|
646
|
-
const fullPath =
|
|
648
|
+
const fullPath = path4.join(dir, entry.name);
|
|
647
649
|
if (entry.isDirectory()) {
|
|
648
650
|
collectLogs(fullPath);
|
|
649
651
|
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
650
652
|
try {
|
|
651
|
-
logContents.push(
|
|
653
|
+
logContents.push(fs4.readFileSync(fullPath, "utf8"));
|
|
652
654
|
} catch {
|
|
653
655
|
}
|
|
654
656
|
}
|
|
@@ -701,6 +703,7 @@ function summarizeFleet(statuses) {
|
|
|
701
703
|
totalEstimatedCost7d: 0,
|
|
702
704
|
totalRuns7d: 0
|
|
703
705
|
};
|
|
706
|
+
const fleetByRoutine = {};
|
|
704
707
|
for (const s of statuses) {
|
|
705
708
|
summary.activeClaimsCount += s.activeClaims.length;
|
|
706
709
|
summary.staleClaimsCount += s.activeClaims.filter((c) => c.isStale).length;
|
|
@@ -712,6 +715,57 @@ function summarizeFleet(statuses) {
|
|
|
712
715
|
summary.totalTokens7d += s.tokenUsage.sevenDayTotalTokens;
|
|
713
716
|
summary.totalEstimatedCost7d += s.tokenUsage.sevenDayEstimatedCost;
|
|
714
717
|
summary.totalRuns7d += s.tokenUsage.recentRunCount;
|
|
718
|
+
if (s.tokenUsage.byRoutine) {
|
|
719
|
+
for (const [rName, rSpend] of Object.entries(s.tokenUsage.byRoutine)) {
|
|
720
|
+
if (!fleetByRoutine[rName]) {
|
|
721
|
+
fleetByRoutine[rName] = {
|
|
722
|
+
runCount: 0,
|
|
723
|
+
inputTokens: 0,
|
|
724
|
+
outputTokens: 0,
|
|
725
|
+
totalTokens: 0,
|
|
726
|
+
estimatedCost: 0,
|
|
727
|
+
maxTokensPerRun: 0,
|
|
728
|
+
iterationsSum: 0,
|
|
729
|
+
iterationsCount: 0
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const acc = fleetByRoutine[rName];
|
|
733
|
+
acc.runCount += rSpend.runCount;
|
|
734
|
+
acc.inputTokens += rSpend.inputTokens;
|
|
735
|
+
acc.outputTokens += rSpend.outputTokens;
|
|
736
|
+
acc.totalTokens += rSpend.totalTokens;
|
|
737
|
+
acc.estimatedCost += rSpend.estimatedCost;
|
|
738
|
+
if (rSpend.maxTokensPerRun > acc.maxTokensPerRun) {
|
|
739
|
+
acc.maxTokensPerRun = rSpend.maxTokensPerRun;
|
|
740
|
+
}
|
|
741
|
+
if (rSpend.avgIterationsUsed !== void 0) {
|
|
742
|
+
acc.iterationsSum += rSpend.avgIterationsUsed * rSpend.runCount;
|
|
743
|
+
acc.iterationsCount += rSpend.runCount;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
if (Object.keys(fleetByRoutine).length > 0) {
|
|
749
|
+
summary.byRoutine = {};
|
|
750
|
+
for (const [rName, acc] of Object.entries(fleetByRoutine)) {
|
|
751
|
+
const avgTokensPerRun = acc.runCount > 0 ? Math.round(acc.totalTokens / acc.runCount) : 0;
|
|
752
|
+
const fleetSharePercent = summary.totalTokens7d > 0 ? Number((acc.totalTokens / summary.totalTokens7d * 100).toFixed(2)) : 0;
|
|
753
|
+
const rSpend = {
|
|
754
|
+
routine: rName,
|
|
755
|
+
runCount: acc.runCount,
|
|
756
|
+
inputTokens: acc.inputTokens,
|
|
757
|
+
outputTokens: acc.outputTokens,
|
|
758
|
+
totalTokens: acc.totalTokens,
|
|
759
|
+
estimatedCost: Number(acc.estimatedCost.toFixed(2)),
|
|
760
|
+
avgTokensPerRun,
|
|
761
|
+
maxTokensPerRun: acc.maxTokensPerRun,
|
|
762
|
+
fleetSharePercent
|
|
763
|
+
};
|
|
764
|
+
if (acc.iterationsCount > 0) {
|
|
765
|
+
rSpend.avgIterationsUsed = Number((acc.iterationsSum / acc.iterationsCount).toFixed(1));
|
|
766
|
+
}
|
|
767
|
+
summary.byRoutine[rName] = rSpend;
|
|
768
|
+
}
|
|
715
769
|
}
|
|
716
770
|
return summary;
|
|
717
771
|
}
|
|
@@ -785,6 +839,16 @@ function renderFleetDashboard(statuses, options = {}) {
|
|
|
785
839
|
lines.push(
|
|
786
840
|
` 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
841
|
);
|
|
842
|
+
if (t.byRoutine && Object.keys(t.byRoutine).length > 0) {
|
|
843
|
+
const routines = Object.values(t.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
844
|
+
for (const r of routines) {
|
|
845
|
+
const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
|
|
846
|
+
const tokenDetails = options.tokens || options.detailed ? ` (in: ${formatTokens(r.inputTokens)}, out: ${formatTokens(r.outputTokens)})` : "";
|
|
847
|
+
lines.push(
|
|
848
|
+
` \u2022 ${pc3.bold(r.routine)}: ${pc3.cyan(formatTokens(r.totalTokens))} tokens${pc3.gray(tokenDetails)} ` + pc3.gray(`(${r.fleetSharePercent.toFixed(1)}%)`) + ` | Cost: ${pc3.green(formatCurrency(r.estimatedCost))} | ${r.runCount} run${r.runCount === 1 ? "" : "s"}${pc3.gray(iterStr)}`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
788
852
|
if (s.staleWarnings.length > 0) {
|
|
789
853
|
lines.push(pc3.bold(pc3.red(" \u26A0\uFE0F Warnings:")));
|
|
790
854
|
for (const w of s.staleWarnings) {
|
|
@@ -801,10 +865,87 @@ function renderFleetDashboard(statuses, options = {}) {
|
|
|
801
865
|
lines.push(
|
|
802
866
|
` 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
867
|
);
|
|
868
|
+
if (summary.byRoutine && Object.keys(summary.byRoutine).length > 0 && (options.tokens || options.detailed || statuses.length > 1)) {
|
|
869
|
+
lines.push(pc3.bold("\n Fleet Spend by Routine:"));
|
|
870
|
+
const fleetRoutines = Object.values(summary.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
871
|
+
for (const r of fleetRoutines) {
|
|
872
|
+
const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
|
|
873
|
+
lines.push(
|
|
874
|
+
` \u2022 ${pc3.bold(r.routine)}: ${pc3.cyan(formatTokens(r.totalTokens))} tokens ` + pc3.gray(`(${r.fleetSharePercent.toFixed(1)}%)`) + ` | Cost: ${pc3.green(formatCurrency(r.estimatedCost))} | ${r.runCount} run${r.runCount === 1 ? "" : "s"}${pc3.gray(iterStr)}`
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
804
878
|
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
879
|
return lines.join("\n");
|
|
806
880
|
}
|
|
807
881
|
|
|
882
|
+
// src/commands/monitor.ts
|
|
883
|
+
import pc4 from "picocolors";
|
|
884
|
+
|
|
885
|
+
// src/lib/global-config.ts
|
|
886
|
+
import fs5 from "fs";
|
|
887
|
+
import path5 from "path";
|
|
888
|
+
import os from "os";
|
|
889
|
+
function getDefaultGlobalConfigPath() {
|
|
890
|
+
const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path5.join(os.homedir(), ".jonah-fleet");
|
|
891
|
+
return path5.join(baseDir, "config.json");
|
|
892
|
+
}
|
|
893
|
+
function loadGlobalConfig(customPath) {
|
|
894
|
+
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
895
|
+
if (!fs5.existsSync(filePath)) {
|
|
896
|
+
return { repositories: [] };
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const raw = fs5.readFileSync(filePath, "utf8");
|
|
900
|
+
const parsed = JSON.parse(raw);
|
|
901
|
+
return {
|
|
902
|
+
repositories: Array.isArray(parsed.repositories) ? parsed.repositories : []
|
|
903
|
+
};
|
|
904
|
+
} catch {
|
|
905
|
+
return { repositories: [] };
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
function saveGlobalConfig(config, customPath) {
|
|
909
|
+
const filePath = customPath || getDefaultGlobalConfigPath();
|
|
910
|
+
const dir = path5.dirname(filePath);
|
|
911
|
+
if (!fs5.existsSync(dir)) {
|
|
912
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
913
|
+
}
|
|
914
|
+
fs5.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
915
|
+
}
|
|
916
|
+
function addGlobalRepository(repo, customPath) {
|
|
917
|
+
const config = loadGlobalConfig(customPath);
|
|
918
|
+
const normalized = repo.trim();
|
|
919
|
+
if (!normalized) return config;
|
|
920
|
+
if (!config.repositories.includes(normalized)) {
|
|
921
|
+
config.repositories.push(normalized);
|
|
922
|
+
saveGlobalConfig(config, customPath);
|
|
923
|
+
}
|
|
924
|
+
return config;
|
|
925
|
+
}
|
|
926
|
+
function removeGlobalRepository(repo, customPath) {
|
|
927
|
+
const config = loadGlobalConfig(customPath);
|
|
928
|
+
const normalized = repo.trim();
|
|
929
|
+
config.repositories = config.repositories.filter((r) => r !== normalized);
|
|
930
|
+
saveGlobalConfig(config, customPath);
|
|
931
|
+
return config;
|
|
932
|
+
}
|
|
933
|
+
function getFleetRepositories(cwd, customGlobalConfigPath) {
|
|
934
|
+
const targetDir = cwd || process.cwd();
|
|
935
|
+
const manifest = loadManifest(targetDir);
|
|
936
|
+
const manifestRepos = Array.isArray(manifest?.repositories) ? manifest.repositories : [];
|
|
937
|
+
const globalConfig = loadGlobalConfig(customGlobalConfigPath);
|
|
938
|
+
const globalRepos = globalConfig.repositories;
|
|
939
|
+
const set = /* @__PURE__ */ new Set();
|
|
940
|
+
for (const r of manifestRepos) {
|
|
941
|
+
if (r && typeof r === "string" && r.trim()) set.add(r.trim());
|
|
942
|
+
}
|
|
943
|
+
for (const r of globalRepos) {
|
|
944
|
+
if (r && typeof r === "string" && r.trim()) set.add(r.trim());
|
|
945
|
+
}
|
|
946
|
+
return Array.from(set);
|
|
947
|
+
}
|
|
948
|
+
|
|
808
949
|
// src/commands/monitor.ts
|
|
809
950
|
async function runMonitor(options = {}) {
|
|
810
951
|
const cwd = options.cwd || process.cwd();
|
|
@@ -854,7 +995,11 @@ async function runMonitor(options = {}) {
|
|
|
854
995
|
if (options.watch && !options.json) {
|
|
855
996
|
console.clear();
|
|
856
997
|
}
|
|
857
|
-
const output = renderFleetDashboard(statuses, {
|
|
998
|
+
const output = renderFleetDashboard(statuses, {
|
|
999
|
+
json: options.json,
|
|
1000
|
+
tokens: options.tokens,
|
|
1001
|
+
detailed: options.detailed
|
|
1002
|
+
});
|
|
858
1003
|
console.log(output);
|
|
859
1004
|
};
|
|
860
1005
|
await pollAndRender();
|
|
@@ -877,7 +1022,7 @@ async function runMonitor(options = {}) {
|
|
|
877
1022
|
async function runStatus(options = {}) {
|
|
878
1023
|
const cwd = options.cwd || process.cwd();
|
|
879
1024
|
if (options.fleet) {
|
|
880
|
-
await runMonitor({ cwd, json: options.json });
|
|
1025
|
+
await runMonitor({ cwd, json: options.json, tokens: options.tokens, detailed: options.detailed });
|
|
881
1026
|
return;
|
|
882
1027
|
}
|
|
883
1028
|
const manifest = loadManifest(cwd);
|
|
@@ -894,6 +1039,32 @@ async function runStatus(options = {}) {
|
|
|
894
1039
|
}
|
|
895
1040
|
const drift = checkDrift(cwd, manifest);
|
|
896
1041
|
const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
|
|
1042
|
+
const logsDir = path6.join(cwd, ".github/prompts/logs");
|
|
1043
|
+
let tokenUsage = void 0;
|
|
1044
|
+
if (fs6.existsSync(logsDir)) {
|
|
1045
|
+
const logContents = [];
|
|
1046
|
+
const collectLogs = (dir) => {
|
|
1047
|
+
const entries = fs6.readdirSync(dir, { withFileTypes: true });
|
|
1048
|
+
for (const entry of entries) {
|
|
1049
|
+
const fullPath = path6.join(dir, entry.name);
|
|
1050
|
+
if (entry.isDirectory()) {
|
|
1051
|
+
collectLogs(fullPath);
|
|
1052
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
1053
|
+
try {
|
|
1054
|
+
logContents.push(fs6.readFileSync(fullPath, "utf8"));
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
try {
|
|
1061
|
+
collectLogs(logsDir);
|
|
1062
|
+
} catch {
|
|
1063
|
+
}
|
|
1064
|
+
if (logContents.length > 0) {
|
|
1065
|
+
tokenUsage = computeTokenSpendFromLogs(logContents);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
897
1068
|
if (options.json) {
|
|
898
1069
|
console.log(
|
|
899
1070
|
JSON.stringify(
|
|
@@ -906,6 +1077,7 @@ async function runStatus(options = {}) {
|
|
|
906
1077
|
routines: manifest.routines,
|
|
907
1078
|
skills: manifest.skills,
|
|
908
1079
|
repositories: manifest.repositories || [],
|
|
1080
|
+
tokenUsage,
|
|
909
1081
|
drift: {
|
|
910
1082
|
hasDrift,
|
|
911
1083
|
...drift
|
|
@@ -937,6 +1109,22 @@ async function runStatus(options = {}) {
|
|
|
937
1109
|
console.log(` - ${pc5.cyan(repo)}`);
|
|
938
1110
|
}
|
|
939
1111
|
}
|
|
1112
|
+
if (tokenUsage && tokenUsage.recentRunCount > 0) {
|
|
1113
|
+
console.log(pc5.bold("\n \u{1F4C8} 7-Day Token Spend:"));
|
|
1114
|
+
console.log(
|
|
1115
|
+
` Runs: ${pc5.bold(tokenUsage.recentRunCount.toString())} | Tokens: ${pc5.bold(formatTokens(tokenUsage.sevenDayTotalTokens))} ` + pc5.gray(`(in: ${formatTokens(tokenUsage.sevenDayInputTokens)}, out: ${formatTokens(tokenUsage.sevenDayOutputTokens)})`) + ` | Cost: ${pc5.bold(pc5.green(formatCurrency(tokenUsage.sevenDayEstimatedCost)))}`
|
|
1116
|
+
);
|
|
1117
|
+
if (tokenUsage.byRoutine && Object.keys(tokenUsage.byRoutine).length > 0) {
|
|
1118
|
+
const routines = Object.values(tokenUsage.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
|
|
1119
|
+
for (const r of routines) {
|
|
1120
|
+
const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
|
|
1121
|
+
const tokenDetails = options.tokens || options.detailed ? ` (in: ${formatTokens(r.inputTokens)}, out: ${formatTokens(r.outputTokens)})` : "";
|
|
1122
|
+
console.log(
|
|
1123
|
+
` \u2022 ${pc5.bold(r.routine)}: ${pc5.cyan(formatTokens(r.totalTokens))} tokens${pc5.gray(tokenDetails)} ` + pc5.gray(`(${r.fleetSharePercent.toFixed(1)}%)`) + ` | Cost: ${pc5.green(formatCurrency(r.estimatedCost))} | ${r.runCount} run${r.runCount === 1 ? "" : "s"}${pc5.gray(iterStr)}`
|
|
1124
|
+
);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
940
1128
|
console.log(pc5.bold("\n Drift / Health:"));
|
|
941
1129
|
if (!hasDrift) {
|
|
942
1130
|
console.log(pc5.green(" \u2713 All prompts, workflows, and skills are healthy and match fleet templates.\n"));
|
|
@@ -992,10 +1180,10 @@ program.command("init").description("Initialize Jonah Fleet configuration, routi
|
|
|
992
1180
|
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) => {
|
|
993
1181
|
await runSync(options);
|
|
994
1182
|
});
|
|
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) => {
|
|
1183
|
+
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("-t, --tokens", "Display detailed per-agent token and cost breakdown", false).option("--detailed", "Display detailed metrics breakdown", false).option("-j, --json", "Output status as JSON", false).action(async (options) => {
|
|
996
1184
|
await runStatus(options);
|
|
997
1185
|
});
|
|
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) => {
|
|
1186
|
+
program.command("monitor [repos...]").description("Monitor health, active claims, PR review loops, and token spend across fleet repositories").option("-t, --tokens", "Display detailed per-agent token and cost breakdown", false).option("--detailed", "Display detailed metrics breakdown", false).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
1187
|
await runMonitor({ ...options, repos });
|
|
1000
1188
|
});
|
|
1001
1189
|
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) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jonah-fleet",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Standalone autonomous agent fleet with Symphony orchestration, claim protocols, and continuous improvement loops",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
],
|
|
28
28
|
"author": "Julien Durandeu",
|
|
29
29
|
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/juliendurandeu/jonah-fleet.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/juliendurandeu/jonah-fleet#readme",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/juliendurandeu/jonah-fleet/issues"
|
|
37
|
+
},
|
|
30
38
|
"dependencies": {
|
|
31
39
|
"commander": "^12.1.0",
|
|
32
40
|
"picocolors": "^1.1.1"
|
|
@@ -4,16 +4,17 @@ How agent routines in this repository are dispatched, claimed, and reconciled
|
|
|
4
4
|
|
|
5
5
|
**Read this when** you need the claim protocol, the stale-claim conditions, the log-push rules, or the measurement-issue protocol — i.e. most Autowork, Peer Review, Analytics Review, and Issues Housekeeping runs.
|
|
6
6
|
|
|
7
|
-
This project's automation is a GitHub-native
|
|
7
|
+
This project's automation is a GitHub-native implementation of the orchestration pattern formalized by OpenAI's [Symphony specification](https://github.com/openai/symphony/blob/main/SPEC.md) for orchestrating autonomous coding agents against an issue tracker. There is **no long-running orchestrator daemon**; the roles map onto GitHub primitives:
|
|
8
8
|
|
|
9
|
-
| Concept | Implementation in this repo |
|
|
9
|
+
| Symphony Concept | Implementation in this repo |
|
|
10
10
|
|---|---|
|
|
11
11
|
| `WORKFLOW.md` (repo-owned config + prompt templates) | `AGENTS.md` (aliased as `GEMINI.md`/`CLAUDE.md`) + `.github/prompts/*.md` |
|
|
12
12
|
| Orchestrator (poll, dispatch, reconcile) | GitHub Actions triggers + scheduled routine sessions |
|
|
13
|
-
| Issue tracker | GitHub Issues |
|
|
14
|
-
| Agent runner | An ephemeral agent session (Antigravity CLI `agy`) in an isolated fresh clone |
|
|
13
|
+
| Issue tracker (Linear in Symphony) | GitHub Issues |
|
|
14
|
+
| Agent runner (Codex app-server in per-issue workspace) | An ephemeral agent session (Antigravity CLI `agy`) in an isolated fresh clone |
|
|
15
15
|
| Tracker is reader/scheduler; mutations happen via agent tools | Routines only schedule; the agent session makes every GitHub write |
|
|
16
16
|
|
|
17
|
+
|
|
17
18
|
Dispatch is both **scheduled** and **event-driven**. All routines run as ephemeral agent sessions via **Antigravity CLI (`agy`)** powered by **Gemini 3.7 Flash (High reasoning)**. The routine suite is calibrated to operate within a **strict 70% weekly token ceiling across all routines combined**, supervised by `optimizer.md`:
|
|
18
19
|
- **Scheduled cron sweeps**: Autowork runs periodically (`autowork-cron.yml`), complemented by prompt optimization (`prompt-optimizer-cron.yml`), issues housekeeping (`issues-housekeeping-cron.yml`), and dependency security checks (`dependency-check-cron.yml`).
|
|
19
20
|
- **Event-driven triggers**: GitHub Actions workflows fire routines on events so work starts within seconds instead of waiting for scheduled ticks:
|
|
@@ -95,3 +96,31 @@ Single source of truth for every routine's Logging section:
|
|
|
95
96
|
1. **Direct commit to `main` is default**: For operational run logs under `.github/prompts/logs/**`, commit directly to `main` via GitHub API or git push.
|
|
96
97
|
2. **Draft PR fallback**: If direct push fails, commit the log to a dedicated, fresh branch and open a draft PR carrying only the log files.
|
|
97
98
|
3. **Automated landing**: `auto-merge-log-prs.yml` or `issues-housekeeping.md` lands accumulated log PRs. Draft log PRs are never reviewed by Peer Review and do not count toward Autowork's backpressure limits.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Token Anomaly Triage & Remediation
|
|
103
|
+
|
|
104
|
+
How token spend, runaway loops, and budget anomalies are detected, triaged, and remediated autonomously across the fleet:
|
|
105
|
+
|
|
106
|
+
1. **Supervised Token Ceiling**: The fleet operates under a global 70% weekly token ceiling (~8.75M tokens/week across all routines). `optimizer.md` evaluates pacing during each scheduled sweep.
|
|
107
|
+
2. **Anomaly Classification & Heuristics**:
|
|
108
|
+
- **Token Surge**: Average token spend per run for a specific routine increases >50% week-over-week. Trigger: prompt bloat or runaway context accumulation. Remediation: prompt instruction pruning, replacing verbose guidelines with concise leading words and progressive disclosure pointers.
|
|
109
|
+
- **Budget Hog**: A single agent routine consumes >75% of total fleet token allowance. Trigger: unbalanced dispatch frequency or unbounded candidate sweeps. Remediation: throttle cron frequency, introduce stricter candidate batching, or add early exit conditions.
|
|
110
|
+
- **Iteration Ceiling Exhaustion**: >20% of runs in a routine terminate at the `token_limit` / max iteration cap. Trigger: tasks too complex for single-flight execution or unbounded looping. Remediation: enforce vertical slicing / umbrella decomposition, tighten pre-ready self-audits, or refine termination bounds.
|
|
111
|
+
- **Review Loop Burn**: Pull requests experiencing $\ge 3$ bounce rounds between autowork and peer-review. Trigger: ambiguous reviewer feedback, pedantic non-blocking findings, or brittle test assertions. Remediation: tighten reviewer trust/noise rules, calibrate reviewer severity thresholds, and engage human escalation via ping-pong caps.
|
|
112
|
+
3. **Automated Remediation PRs**: The optimizer automatically drafts targeted PRs—locally for repo-specific rules/configs, or upstream via `npx jonah-fleet contribute` for fleet-wide prompt/workflow improvements.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Autonomous Issue Synthesis
|
|
117
|
+
|
|
118
|
+
How external and human contributor pull requests are reconciled into the issue tracker without manual friction or reviewer bounces:
|
|
119
|
+
|
|
120
|
+
1. **Zero-Friction Contribution**: External human contributors often submit PRs directly without opening an issue first. Forcing contributors to open tracking issues or bouncing clean PRs causes friction, review thrash, and abandonment.
|
|
121
|
+
2. **Autonomous Synthesis on Merge**: When `peer-review.md` approves a pull request lacking a `Closes #N` link, the review routine automatically synthesizes a tracking issue before merging:
|
|
122
|
+
- Creates a tracked issue via `gh issue create` capturing the PR title, body, and deliverables.
|
|
123
|
+
- Appends `Closes #<synthesized_issue_id>` to the PR description via `gh pr edit`.
|
|
124
|
+
3. **Audit & Single-Flight Lineage**: When the PR is squash-merged, GitHub's native issue closure kicks in and automatically closes the synthesized issue. This maintains 100% issue auditability, project board tracking, telemetry metrics, and release changelogs without imposing any friction on human contributors.
|
|
125
|
+
|
|
126
|
+
|
|
@@ -13,6 +13,7 @@ The run is SUCCESS if ALL of these are true:
|
|
|
13
13
|
- [ ] All log files from the incremental window in `.github/prompts/logs/` have been scanned
|
|
14
14
|
- [ ] Every FAILURE log has been categorized and analyzed
|
|
15
15
|
- [ ] Inefficiency and review loops per PR have been computed across SUCCESS logs
|
|
16
|
+
- [ ] Per-agent token and cost consumption metrics have been aggregated across in-window logs and evaluated against the 70% weekly budget ceiling in `ORCHESTRATION.md`
|
|
16
17
|
- [ ] Closed bug issues and merged bug-fix PRs in the window have been analyzed for systemic root causes
|
|
17
18
|
- [ ] For each fixable pattern:
|
|
18
19
|
- If project-specific: opened a local PR with a prompt, template, or test fix and marked ready for review
|
|
@@ -38,17 +39,35 @@ If any criterion cannot be met, stop immediately and log FAILURE with the reason
|
|
|
38
39
|
|
|
39
40
|
### 1. Collect signals & analyze logs
|
|
40
41
|
|
|
41
|
-
1. Scan in-window log files in `.github/prompts/logs
|
|
42
|
-
2. Extract failure categories
|
|
43
|
-
3. Compute efficiency metrics
|
|
44
|
-
4.
|
|
42
|
+
1. **Scan in-window log files**: Read all log files in `.github/prompts/logs/*/` within the incremental scan window.
|
|
43
|
+
2. **Extract failure categories**: Categorize runs logging `FAILURE` (`prompt_unclear`, `data_issue`, `token_limit`, `infeasible_task`).
|
|
44
|
+
3. **Compute efficiency metrics**: Identify PRs experiencing $\ge 3$ review bounce rounds and runs with high iteration usage relative to limits.
|
|
45
|
+
4. **Aggregate per-agent token & cost consumption**:
|
|
46
|
+
- Parse the metadata table from each in-window log: `Routine`, `Input tokens`, `Output tokens`, `Estimated cost`, `Iterations used` (e.g. `26 / 65`), and `Result` (`SUCCESS` or `FAILURE`).
|
|
47
|
+
- Group logs by `Routine` (`autowork`, `peer-review`, `issues-housekeeping`, `dependency-update-security-check`, `optimizer`, `product-planning`).
|
|
48
|
+
- For each routine, compute:
|
|
49
|
+
- **Run count**: total completed runs.
|
|
50
|
+
- **Token volume**: total input tokens, total output tokens, combined total tokens.
|
|
51
|
+
- **Cost volume**: sum of estimated costs ($).
|
|
52
|
+
- **Fleet spend share**: `(routine total cost / fleet total cost) * 100` (or token volume share if cost is unmetered).
|
|
53
|
+
- **Token averages & peaks**: average tokens per run and max tokens in a single run.
|
|
54
|
+
- **Iteration efficiency**: average iterations used per run and percentage of budget consumed.
|
|
55
|
+
- **Weekly token ceiling pacing**: Compare total fleet tokens and per-routine volume against the 70% weekly token budget ceiling specified in `ORCHESTRATION.md` (~8.75M tokens/week, and per-routine budget overrides in `agents-manifest.json` if present). Determine burn rate velocity (tokens/day) and projected 7-day total.
|
|
56
|
+
5. **Evaluate Token Anomaly Heuristics**: Detect actionable anomalies using concrete numerical thresholds:
|
|
57
|
+
- **Token Surge**: Routine average token consumption increases >50% week-over-week (or against baseline).
|
|
58
|
+
- **Budget Hog**: A single agent routine consumes >75% of total fleet token allowance.
|
|
59
|
+
- **Iteration Ceiling Exhaustion**: >20% of runs in a routine terminate at the `token_limit` / max iteration cap.
|
|
60
|
+
- **Review Loop Burn**: Pull requests experiencing >= 3 bounce rounds between autowork and peer-review over unresolved or recurring findings.
|
|
61
|
+
6. **Analyze resolved bugs & review comments**: Examine closed bug issues, merged bug-fix PRs, and review feedback for missing checks in authoring (`autowork.md`) or review (`peer-review.md`).
|
|
45
62
|
|
|
46
63
|
### 2. Formulate preventative improvements
|
|
47
64
|
|
|
48
|
-
Translate findings into concrete preventative improvements:
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
-
|
|
65
|
+
Translate findings into concrete preventative improvements and remediation triggers:
|
|
66
|
+
- **Instruction Pruning**: For Token Surge and prompt bloat, prune redundant instructions, anti-patterns, and no-ops in routine prompts following `/writing-for-agents` principles (replacing sprawling descriptions with crisp leading words and progressive disclosure pointers).
|
|
67
|
+
- **Early Exit & Candidate Skip**: For Budget Hog and runaway sweeps, add early termination guards, candidate pre-qualification filters, and infeasible evaluation caps.
|
|
68
|
+
- **Iteration Ceiling & Self-Audit Tuning**: For Iteration Ceiling Exhaustion, adjust max iteration bounds or tighten pre-ready self-audits in `autowork.md` to catch defects before review cycles start.
|
|
69
|
+
- **Ping-Pong Convergence**: For Review Loop Burn, tighten reviewer trust & noise filtering, enforce clean-merge gates, and apply ping-pong caps to prevent endless bounce cycles.
|
|
70
|
+
- **Verification & Invariant Tests**: Add automated test cases in `tests/` verifying prompt invariant preservation and schema conformity.
|
|
52
71
|
|
|
53
72
|
### 3. Open Fix PR (Local or Upstream Bridge)
|
|
54
73
|
|
|
@@ -67,6 +86,23 @@ Translate findings into concrete preventative improvements:
|
|
|
67
86
|
After completing (SUCCESS or FAILURE), write a log file to `.github/prompts/logs/optimizer/{timestamp}.md` following the schema in `.github/prompts/logs/_template.md`. Include:
|
|
68
87
|
- Prompt SHA
|
|
69
88
|
- Analyzed logs count and identified patterns
|
|
89
|
+
- **Token & Cost Consumption by Agent** scorecard table:
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
### Token & Cost Consumption by Agent
|
|
93
|
+
|
|
94
|
+
| Routine | Runs | Input Tokens | Output Tokens | Total Tokens | Cost | Fleet % | Avg Iterations | Max Iterations | Status / Anomaly |
|
|
95
|
+
|---|---|---|---|---|---|---|---|---|---|
|
|
96
|
+
| `autowork` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
97
|
+
| `peer-review` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
98
|
+
| `issues-housekeeping` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
99
|
+
| `dependency-update-security-check` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
100
|
+
| `optimizer` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
101
|
+
| `product-planning` | 0 | 0 | 0 | 0 | $0.00 | 0.0% | 0 | 0 | Nominal |
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- Weekly token budget pacing evaluation (pacing vs 70% ceiling in `ORCHESTRATION.md`)
|
|
70
105
|
- PRs opened (local or upstream)
|
|
71
106
|
|
|
72
107
|
**Important**: Commit the log file directly to `main` and push. Follow the Log delivery fallback in `ORCHESTRATION.md` if direct push fails.
|
|
108
|
+
|
|
@@ -19,7 +19,7 @@ The run is SUCCESS only if ALL of these are true:
|
|
|
19
19
|
|
|
20
20
|
- [ ] Identified the target PR: if one was named in the invocation, reviewed exactly that PR; otherwise listed open PRs and selected one by priority
|
|
21
21
|
- [ ] Ran the code-review pass (`/code-review` and security pass), and posted findings as inline review comments
|
|
22
|
-
- [ ] Took exactly one final action: squash-merged (if PR is good, CI green and present
|
|
22
|
+
- [ ] Took exactly one final action: squash-merged (if PR is good, CI green and present; executed Autonomous Issue Synthesis if unlinked) OR posted findings and **converted the PR back to draft** (`gh pr ready <N> --undo`) for author/autowork in-session fixes OR, if round cap reached at round 5 with blocking findings, converted to draft and escalated to human
|
|
23
23
|
- [ ] If merging: captured deferred non-blocking findings per materiality bar (filed follow-up issues for material ones, batched or dropped immaterial ones)
|
|
24
24
|
- [ ] If in Scan mode and no eligible PRs exist, logged SUCCESS with "No PRs to review"
|
|
25
25
|
|
|
@@ -37,8 +37,8 @@ If any criterion cannot be met, stop immediately and log FAILURE with the reason
|
|
|
37
37
|
## Final action: merge or bounce to draft
|
|
38
38
|
|
|
39
39
|
Every review ends in exactly one of two states:
|
|
40
|
-
- **Merge** — only if PR is good, CI is green and verified on the head commit
|
|
41
|
-
- Sequence: (1) squash-merge, (
|
|
40
|
+
- **Merge** — only if PR is good, CI is green and verified on the head commit. If the PR does not reference a tracked issue (`Closes #N`), execute Autonomous Issue Synthesis prior to merge.
|
|
41
|
+
- Sequence: (1) if unlinked, synthesize tracking issue (`gh issue create`) and link to PR (`gh pr edit`), (2) squash-merge, (3) submit held review comments, (4) file follow-up issues for deferred material findings.
|
|
42
42
|
- Immaterial findings (style/preference) default to dying in the review thread or getting batched.
|
|
43
43
|
- Mechanical doc fixes (missing changelog line, doc typo in diff) can be committed directly to `main` after squash-merge.
|
|
44
44
|
- **Bounce to draft** — if any **blocking** finding remains (correctness bug, security flaw, failing/missing CI, broken contract):
|
|
@@ -55,6 +55,7 @@ Every review ends in exactly one of two states:
|
|
|
55
55
|
- Do not attempt `REQUEST_CHANGES` or `APPROVE` on own PRs (GitHub rejects same-account review states). Always use `COMMENT` + draft toggle.
|
|
56
56
|
- On re-review, do not raise new findings in code that was unchanged since the prior review — only inspect the delta commits.
|
|
57
57
|
- Do not bounce a PR for non-blocking style/preference findings when all correctness checks pass.
|
|
58
|
+
- Do not bounce an external contributor PR purely for missing `Closes #N` when the PR description provides a clear specification.
|
|
58
59
|
|
|
59
60
|
## Instructions
|
|
60
61
|
|
|
@@ -80,7 +81,7 @@ Check if `$PR_NUMBER` is set:
|
|
|
80
81
|
|
|
81
82
|
1. Run `/code-review` over the diff (or delta commits if re-review) evaluating:
|
|
82
83
|
- **Standards**: Conformance to `AGENTS.md` (or `CLAUDE.md`/`GEMINI.md`), conventions, and architecture.
|
|
83
|
-
- **Spec Compliance**: Verification against the linked issue's deliverables (`## Tasks`).
|
|
84
|
+
- **Spec Compliance**: Verification against the linked issue's deliverables (`## Tasks`), or against the PR description's summary/changes if no tracking issue is linked.
|
|
84
85
|
2. Run Security Pass: auth gates, permission checks, injection risks, sensitive credentials.
|
|
85
86
|
3. If PR modifies rendered UI, verify screenshots or visual components if tooling/scripts are available.
|
|
86
87
|
4. Run repository verification commands (tests, type-check) if CI status is unconfirmed.
|
|
@@ -88,8 +89,21 @@ Check if `$PR_NUMBER` is set:
|
|
|
88
89
|
### Step 5: Classify Findings & Make Decision
|
|
89
90
|
|
|
90
91
|
Classify each finding:
|
|
91
|
-
- **Blocking**: Broken logic, security hole, data loss, regression, broken tests, missing
|
|
92
|
-
- **Non-blocking**: Minor refactor, style preference, performance micro-optimization.
|
|
92
|
+
- **Blocking**: Broken logic, security hole, data loss, regression, broken tests, missing deliverable from the issue/PR specification.
|
|
93
|
+
- **Non-blocking**: Minor refactor, style preference, performance micro-optimization, missing `Closes #N` on contributor PRs with self-contained descriptions.
|
|
94
|
+
|
|
95
|
+
### Step 5.5: Autonomous Issue Synthesis (for unlinked PRs)
|
|
96
|
+
|
|
97
|
+
If the PR is clean and approved for merge, but lacks a `Closes #N` tracking link:
|
|
98
|
+
1. Synthesize a retroactive tracking issue on GitHub:
|
|
99
|
+
```bash
|
|
100
|
+
gh issue create --title "<PR Title>" --body "Tracked retroactively from external pull request #<PR_NUMBER>.\n\n## Deliverables & Context\n<PR Description>\n\n_Synthesized autonomously by Jonah Fleet Peer Review_"
|
|
101
|
+
```
|
|
102
|
+
2. Capture the newly created issue number `$ISSUE_NUMBER`.
|
|
103
|
+
3. Edit the PR description to append `Closes #$ISSUE_NUMBER`:
|
|
104
|
+
```bash
|
|
105
|
+
gh pr edit <PR_NUMBER> --body "<PR Description>\n\nCloses #$ISSUE_NUMBER"
|
|
106
|
+
```
|
|
93
107
|
|
|
94
108
|
### Step 6: Execute Final Action
|
|
95
109
|
|
|
@@ -97,6 +111,7 @@ Classify each finding:
|
|
|
97
111
|
- If `N < 5`: Post inline comments, submit review as `COMMENT`, and convert PR to draft (`gh pr ready <N> --undo`).
|
|
98
112
|
- If `N >= 5`: Convert PR to draft, post summary comment escalating to repo maintainer, and apply `needs-human` label.
|
|
99
113
|
- **If Clean (or only Non-blocking findings)**:
|
|
114
|
+
- If PR lacks `Closes #N`, execute Autonomous Issue Synthesis (Step 5.5).
|
|
100
115
|
- Squash-merge the PR: `gh pr merge <N> --squash --delete-branch`.
|
|
101
116
|
- Submit held review comments.
|
|
102
117
|
- File follow-up issues for material non-blocking findings.
|