jonah-fleet 1.0.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 +35 -0
- package/README.md +59 -4
- package/dist/index.js +770 -32
- package/package.json +9 -1
- package/schema.json +5 -0
- 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,6 +5,41 @@ 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
|
+
|
|
33
|
+
## [1.1.0] - 2026-08-24
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Multi-repository fleet monitoring command (`jonah-fleet monitor` / `jonah-fleet status --fleet`).
|
|
38
|
+
- Global repository registry manager (`~/.jonah-fleet/config.json`) with `--add` and `--remove` CLI flags.
|
|
39
|
+
- Real-time active claim inspection and stale claim detection (> 6h with no open PR).
|
|
40
|
+
- 7-day rolling token spend tracking and cost estimation from routine run logs.
|
|
41
|
+
- Live watch mode (`--watch`, `--interval <sec>`) and JSON output (`--json`).
|
|
42
|
+
|
|
8
43
|
## [1.0.0] - 2026-08-24
|
|
9
44
|
|
|
10
45
|
### Added
|
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,6 +102,25 @@ To synchronize with the latest fleet version:
|
|
|
66
102
|
npx jonah-fleet sync
|
|
67
103
|
```
|
|
68
104
|
|
|
105
|
+
### 5. Multi-repo Fleet Monitoring
|
|
106
|
+
|
|
107
|
+
Monitor health, in-flight autowork claims, open PR review loops, and token usage across your entire fleet:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Register repositories to your fleet registry
|
|
111
|
+
npx jonah-fleet monitor --add juliendurandeu/Jonah-RuPaul
|
|
112
|
+
npx jonah-fleet monitor --add juliendurandeu/jonah-newsletter-gemini
|
|
113
|
+
|
|
114
|
+
# View terminal dashboard
|
|
115
|
+
npx jonah-fleet monitor
|
|
116
|
+
|
|
117
|
+
# Live watch mode with auto-refresh
|
|
118
|
+
npx jonah-fleet monitor --watch --interval 10
|
|
119
|
+
|
|
120
|
+
# Output as JSON
|
|
121
|
+
npx jonah-fleet monitor --json
|
|
122
|
+
```
|
|
123
|
+
|
|
69
124
|
---
|
|
70
125
|
|
|
71
126
|
## 🛠️ Repository Layout
|
|
@@ -130,4 +185,4 @@ Each target project contains an `agents-manifest.json` at its root:
|
|
|
130
185
|
|
|
131
186
|
## 📄 License
|
|
132
187
|
|
|
133
|
-
MIT © Julien
|
|
188
|
+
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.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,75 +369,804 @@ 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";
|
|
374
|
+
import pc5 from "picocolors";
|
|
375
|
+
|
|
376
|
+
// src/lib/fleet-query.ts
|
|
377
|
+
import { execFile } from "child_process";
|
|
378
|
+
import fs4 from "fs";
|
|
379
|
+
import path4 from "path";
|
|
380
|
+
import { promisify } from "util";
|
|
381
|
+
var execFileAsync = promisify(execFile);
|
|
382
|
+
var defaultGhExecutor = async (args) => {
|
|
383
|
+
try {
|
|
384
|
+
const { stdout } = await execFileAsync("gh", args, { maxBuffer: 10 * 1024 * 1024 });
|
|
385
|
+
return stdout;
|
|
386
|
+
} catch (err) {
|
|
387
|
+
if (err.stdout) return err.stdout;
|
|
388
|
+
throw err;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
function parseLogMetadata(content) {
|
|
392
|
+
const lines = content.split("\n");
|
|
393
|
+
const meta = {};
|
|
394
|
+
for (const line of lines) {
|
|
395
|
+
const match = line.match(/^\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
|
|
396
|
+
if (!match) continue;
|
|
397
|
+
const key = match[1].trim().toLowerCase();
|
|
398
|
+
let val = match[2].trim().replace(/`/g, "");
|
|
399
|
+
if (key === "routine") {
|
|
400
|
+
meta.routine = val;
|
|
401
|
+
} else if (key === "timestamp") {
|
|
402
|
+
meta.timestamp = val;
|
|
403
|
+
} else if (key === "result") {
|
|
404
|
+
meta.result = val;
|
|
405
|
+
} else if (key === "input tokens" || key === "input_tokens") {
|
|
406
|
+
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
407
|
+
if (!isNaN(num)) meta.inputTokens = num;
|
|
408
|
+
} else if (key === "output tokens" || key === "output_tokens") {
|
|
409
|
+
const num = parseInt(val.replace(/[^\d]/g, ""), 10);
|
|
410
|
+
if (!isNaN(num)) meta.outputTokens = num;
|
|
411
|
+
} else if (key === "estimated cost" || key === "estimated_cost") {
|
|
412
|
+
const num = parseFloat(val.replace(/[^0-9.]/g, ""));
|
|
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;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (meta.timestamp || meta.routine) {
|
|
424
|
+
return meta;
|
|
425
|
+
}
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
function computeTokenSpendFromLogs(logContents, now = Date.now()) {
|
|
429
|
+
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
430
|
+
const cutoff = now - SEVEN_DAYS_MS;
|
|
431
|
+
let totalInputTokens = 0;
|
|
432
|
+
let totalOutputTokens = 0;
|
|
433
|
+
let totalCost = 0;
|
|
434
|
+
let totalRuns = 0;
|
|
435
|
+
const routineMap = {};
|
|
436
|
+
for (const content of logContents) {
|
|
437
|
+
const meta = parseLogMetadata(content);
|
|
438
|
+
if (!meta || !meta.timestamp) continue;
|
|
439
|
+
const logTime = new Date(meta.timestamp).getTime();
|
|
440
|
+
if (isNaN(logTime) || logTime < cutoff) continue;
|
|
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
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
sevenDayInputTokens: totalInputTokens,
|
|
499
|
+
sevenDayOutputTokens: totalOutputTokens,
|
|
500
|
+
sevenDayTotalTokens,
|
|
501
|
+
sevenDayEstimatedCost: totalCost,
|
|
502
|
+
recentRunCount: totalRuns,
|
|
503
|
+
byRoutine
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
function parseClaimFromIssue(issue, openPRs = [], now = Date.now()) {
|
|
507
|
+
if (!issue.assignees || issue.assignees.length === 0) {
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
const comments = issue.comments || [];
|
|
511
|
+
let latestClaimComment = null;
|
|
512
|
+
for (let i = comments.length - 1; i >= 0; i--) {
|
|
513
|
+
if (comments[i].body && comments[i].body.includes("\u{1F512} Claimed by autowork run")) {
|
|
514
|
+
latestClaimComment = comments[i];
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (!latestClaimComment) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const claimTime = new Date(latestClaimComment.createdAt).getTime();
|
|
522
|
+
const ageMs = now - (isNaN(claimTime) ? now : claimTime);
|
|
523
|
+
const ageHours = Math.max(0, ageMs / (1e3 * 60 * 60));
|
|
524
|
+
const issueNumStr = `#${issue.number}`;
|
|
525
|
+
const hasOpenPR = openPRs.some((pr) => {
|
|
526
|
+
const bodyMatch = pr.body && pr.body.includes(issueNumStr);
|
|
527
|
+
const titleMatch = pr.title && pr.title.includes(issueNumStr);
|
|
528
|
+
const branchMatch = pr.headRefName && pr.headRefName.includes(`issue-${issue.number}`);
|
|
529
|
+
return bodyMatch || titleMatch || branchMatch;
|
|
530
|
+
});
|
|
531
|
+
const isStale = ageHours > 6 && !hasOpenPR;
|
|
532
|
+
return {
|
|
533
|
+
issueNumber: issue.number,
|
|
534
|
+
title: issue.title,
|
|
535
|
+
assignee: issue.assignees[0]?.login || latestClaimComment.author?.login || "unknown",
|
|
536
|
+
claimedAt: latestClaimComment.createdAt,
|
|
537
|
+
ageHours,
|
|
538
|
+
isStale,
|
|
539
|
+
url: issue.url
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor, now = Date.now()) {
|
|
543
|
+
const result = {
|
|
544
|
+
repo: repoIdentifier,
|
|
545
|
+
activeClaims: [],
|
|
546
|
+
openPRs: [],
|
|
547
|
+
tokenUsage: {
|
|
548
|
+
sevenDayInputTokens: 0,
|
|
549
|
+
sevenDayOutputTokens: 0,
|
|
550
|
+
sevenDayTotalTokens: 0,
|
|
551
|
+
sevenDayEstimatedCost: 0,
|
|
552
|
+
recentRunCount: 0,
|
|
553
|
+
byRoutine: {}
|
|
554
|
+
},
|
|
555
|
+
staleWarnings: []
|
|
556
|
+
};
|
|
557
|
+
try {
|
|
558
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
559
|
+
const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
|
|
560
|
+
if (fs4.existsSync(manifestPath)) {
|
|
561
|
+
try {
|
|
562
|
+
const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
|
|
563
|
+
result.fleetVersion = raw.version;
|
|
564
|
+
result.preset = raw.preset;
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
try {
|
|
570
|
+
const manifestRaw = await executor([
|
|
571
|
+
"api",
|
|
572
|
+
`repos/${repoIdentifier}/contents/agents-manifest.json`
|
|
573
|
+
]);
|
|
574
|
+
const parsed = JSON.parse(manifestRaw);
|
|
575
|
+
if (parsed.content) {
|
|
576
|
+
const content = Buffer.from(parsed.content, "base64").toString("utf8");
|
|
577
|
+
const manifest = JSON.parse(content);
|
|
578
|
+
result.fleetVersion = manifest.version;
|
|
579
|
+
result.preset = manifest.preset;
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
try {
|
|
585
|
+
const prsRaw = await executor([
|
|
586
|
+
"pr",
|
|
587
|
+
"list",
|
|
588
|
+
"--repo",
|
|
589
|
+
repoIdentifier,
|
|
590
|
+
"--state",
|
|
591
|
+
"open",
|
|
592
|
+
"--json",
|
|
593
|
+
"number,title,author,isDraft,createdAt,updatedAt,reviewDecision,url,headRefName,body"
|
|
594
|
+
]);
|
|
595
|
+
const prs = JSON.parse(prsRaw);
|
|
596
|
+
if (Array.isArray(prs)) {
|
|
597
|
+
result.openPRs = prs.map((pr) => ({
|
|
598
|
+
number: pr.number,
|
|
599
|
+
title: pr.title,
|
|
600
|
+
author: pr.author?.login || "unknown",
|
|
601
|
+
isDraft: Boolean(pr.isDraft),
|
|
602
|
+
reviewDecision: pr.reviewDecision,
|
|
603
|
+
createdAt: pr.createdAt,
|
|
604
|
+
updatedAt: pr.updatedAt,
|
|
605
|
+
url: pr.url,
|
|
606
|
+
headRefName: pr.headRefName,
|
|
607
|
+
body: pr.body
|
|
608
|
+
}));
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
result.error = `Failed to fetch PRs: ${err.message}`;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const issuesRaw = await executor([
|
|
615
|
+
"issue",
|
|
616
|
+
"list",
|
|
617
|
+
"--repo",
|
|
618
|
+
repoIdentifier,
|
|
619
|
+
"--state",
|
|
620
|
+
"open",
|
|
621
|
+
"--json",
|
|
622
|
+
"number,title,assignees,updatedAt,comments,url"
|
|
623
|
+
]);
|
|
624
|
+
const issues = JSON.parse(issuesRaw);
|
|
625
|
+
if (Array.isArray(issues)) {
|
|
626
|
+
for (const issue of issues) {
|
|
627
|
+
const claim = parseClaimFromIssue(issue, result.openPRs, now);
|
|
628
|
+
if (claim) {
|
|
629
|
+
result.activeClaims.push(claim);
|
|
630
|
+
if (claim.isStale) {
|
|
631
|
+
result.staleWarnings.push(
|
|
632
|
+
`Issue #${claim.issueNumber} claimed by @${claim.assignee} ${claim.ageHours.toFixed(1)}h ago with no open PR`
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch (err) {
|
|
639
|
+
if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
|
|
640
|
+
}
|
|
641
|
+
const logContents = [];
|
|
642
|
+
if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
|
|
643
|
+
const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
|
|
644
|
+
if (fs4.existsSync(logsDir)) {
|
|
645
|
+
const collectLogs = (dir) => {
|
|
646
|
+
const entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
647
|
+
for (const entry of entries) {
|
|
648
|
+
const fullPath = path4.join(dir, entry.name);
|
|
649
|
+
if (entry.isDirectory()) {
|
|
650
|
+
collectLogs(fullPath);
|
|
651
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
652
|
+
try {
|
|
653
|
+
logContents.push(fs4.readFileSync(fullPath, "utf8"));
|
|
654
|
+
} catch {
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
collectLogs(logsDir);
|
|
660
|
+
}
|
|
661
|
+
} else {
|
|
662
|
+
try {
|
|
663
|
+
const treeRaw = await executor([
|
|
664
|
+
"api",
|
|
665
|
+
`repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
|
|
666
|
+
]);
|
|
667
|
+
const tree = JSON.parse(treeRaw);
|
|
668
|
+
if (Array.isArray(tree.tree)) {
|
|
669
|
+
const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-15);
|
|
670
|
+
for (const file of logFiles) {
|
|
671
|
+
try {
|
|
672
|
+
const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
|
|
673
|
+
const parsed = JSON.parse(fileRaw);
|
|
674
|
+
if (parsed.content) {
|
|
675
|
+
logContents.push(Buffer.from(parsed.content, "base64").toString("utf8"));
|
|
676
|
+
}
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch {
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (logContents.length > 0) {
|
|
685
|
+
result.tokenUsage = computeTokenSpendFromLogs(logContents, now);
|
|
686
|
+
}
|
|
687
|
+
} catch (err) {
|
|
688
|
+
result.error = err.message;
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
function summarizeFleet(statuses) {
|
|
693
|
+
const summary = {
|
|
694
|
+
totalRepos: statuses.length,
|
|
695
|
+
activeClaimsCount: 0,
|
|
696
|
+
staleClaimsCount: 0,
|
|
697
|
+
openPRsCount: 0,
|
|
698
|
+
draftPRsCount: 0,
|
|
699
|
+
readyPRsCount: 0,
|
|
700
|
+
totalInputTokens7d: 0,
|
|
701
|
+
totalOutputTokens7d: 0,
|
|
702
|
+
totalTokens7d: 0,
|
|
703
|
+
totalEstimatedCost7d: 0,
|
|
704
|
+
totalRuns7d: 0
|
|
705
|
+
};
|
|
706
|
+
const fleetByRoutine = {};
|
|
707
|
+
for (const s of statuses) {
|
|
708
|
+
summary.activeClaimsCount += s.activeClaims.length;
|
|
709
|
+
summary.staleClaimsCount += s.activeClaims.filter((c) => c.isStale).length;
|
|
710
|
+
summary.openPRsCount += s.openPRs.length;
|
|
711
|
+
summary.draftPRsCount += s.openPRs.filter((p) => p.isDraft).length;
|
|
712
|
+
summary.readyPRsCount += s.openPRs.filter((p) => !p.isDraft).length;
|
|
713
|
+
summary.totalInputTokens7d += s.tokenUsage.sevenDayInputTokens;
|
|
714
|
+
summary.totalOutputTokens7d += s.tokenUsage.sevenDayOutputTokens;
|
|
715
|
+
summary.totalTokens7d += s.tokenUsage.sevenDayTotalTokens;
|
|
716
|
+
summary.totalEstimatedCost7d += s.tokenUsage.sevenDayEstimatedCost;
|
|
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
|
+
}
|
|
769
|
+
}
|
|
770
|
+
return summary;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// src/lib/dashboard.ts
|
|
366
774
|
import pc3 from "picocolors";
|
|
775
|
+
function formatTokens(num) {
|
|
776
|
+
if (num >= 1e6) {
|
|
777
|
+
return `${(num / 1e6).toFixed(2)}M`;
|
|
778
|
+
}
|
|
779
|
+
if (num >= 1e3) {
|
|
780
|
+
return `${(num / 1e3).toFixed(1)}k`;
|
|
781
|
+
}
|
|
782
|
+
return num.toString();
|
|
783
|
+
}
|
|
784
|
+
function formatCurrency(amount) {
|
|
785
|
+
return `$${amount.toFixed(2)}`;
|
|
786
|
+
}
|
|
787
|
+
function renderFleetDashboard(statuses, options = {}) {
|
|
788
|
+
const summary = summarizeFleet(statuses);
|
|
789
|
+
if (options.json) {
|
|
790
|
+
return JSON.stringify(
|
|
791
|
+
{
|
|
792
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
793
|
+
summary,
|
|
794
|
+
repositories: statuses
|
|
795
|
+
},
|
|
796
|
+
null,
|
|
797
|
+
2
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const lines = [];
|
|
801
|
+
lines.push(pc3.bold(pc3.cyan("\n\u{1F4CA} Jonah Fleet Multi-Repo Monitor\n")));
|
|
802
|
+
if (statuses.length === 0) {
|
|
803
|
+
lines.push(pc3.yellow(" No repositories configured in fleet registry."));
|
|
804
|
+
lines.push(pc3.gray(" Use `jonah-fleet monitor --add <owner/repo>` to register repositories.\n"));
|
|
805
|
+
return lines.join("\n");
|
|
806
|
+
}
|
|
807
|
+
for (const s of statuses) {
|
|
808
|
+
const versionStr = s.fleetVersion ? `v${s.fleetVersion}` : "unmanaged";
|
|
809
|
+
const presetStr = s.preset ? `preset: ${s.preset}` : "";
|
|
810
|
+
const headerInfo = [versionStr, presetStr].filter(Boolean).join(", ");
|
|
811
|
+
lines.push(pc3.bold(`\u{1F4E6} ${pc3.cyan(s.repo)} ${pc3.gray(`(${headerInfo})`)}`));
|
|
812
|
+
if (s.error) {
|
|
813
|
+
lines.push(pc3.red(` \u274C Error: ${s.error}`));
|
|
814
|
+
}
|
|
815
|
+
lines.push(pc3.bold(" \u{1F512} Active Claims:"));
|
|
816
|
+
if (s.activeClaims.length === 0) {
|
|
817
|
+
lines.push(pc3.gray(" None (idle)"));
|
|
818
|
+
} else {
|
|
819
|
+
for (const claim of s.activeClaims) {
|
|
820
|
+
const staleTag = claim.isStale ? pc3.red(pc3.bold(" [\u26A0\uFE0F STALE CLAIM > 6h]")) : pc3.green(" [ACTIVE]");
|
|
821
|
+
const ageStr = `${claim.ageHours.toFixed(1)}h ago`;
|
|
822
|
+
lines.push(
|
|
823
|
+
` #${claim.issueNumber} ${claim.title}${staleTag}` + pc3.gray(` (claimed by @${claim.assignee}, ${ageStr})`)
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
lines.push(pc3.bold(" \u{1F500} Open PRs:"));
|
|
828
|
+
if (s.openPRs.length === 0) {
|
|
829
|
+
lines.push(pc3.gray(" None"));
|
|
830
|
+
} else {
|
|
831
|
+
for (const pr of s.openPRs) {
|
|
832
|
+
const stateTag = pr.isDraft ? pc3.yellow("[DRAFT]") : pc3.green("[READY]");
|
|
833
|
+
const reviewStr = pr.reviewDecision ? pc3.magenta(` (${pr.reviewDecision})`) : "";
|
|
834
|
+
lines.push(` #${pr.number} ${stateTag} ${pr.title}${reviewStr}` + pc3.gray(` by @${pr.author}`));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
lines.push(pc3.bold(" \u{1F4C8} 7-Day Token Spend:"));
|
|
838
|
+
const t = s.tokenUsage;
|
|
839
|
+
lines.push(
|
|
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)))}`
|
|
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
|
+
}
|
|
852
|
+
if (s.staleWarnings.length > 0) {
|
|
853
|
+
lines.push(pc3.bold(pc3.red(" \u26A0\uFE0F Warnings:")));
|
|
854
|
+
for (const w of s.staleWarnings) {
|
|
855
|
+
lines.push(pc3.red(` - ${w}`));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
lines.push("");
|
|
859
|
+
}
|
|
860
|
+
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"));
|
|
861
|
+
lines.push(pc3.bold("\u{1F310} Fleet Summary:"));
|
|
862
|
+
lines.push(
|
|
863
|
+
` 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)`)
|
|
864
|
+
);
|
|
865
|
+
lines.push(
|
|
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`
|
|
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
|
+
}
|
|
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"));
|
|
879
|
+
return lines.join("\n");
|
|
880
|
+
}
|
|
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
|
+
|
|
949
|
+
// src/commands/monitor.ts
|
|
950
|
+
async function runMonitor(options = {}) {
|
|
951
|
+
const cwd = options.cwd || process.cwd();
|
|
952
|
+
const executor = options.executor || defaultGhExecutor;
|
|
953
|
+
if (options.add) {
|
|
954
|
+
const updated = addGlobalRepository(options.add);
|
|
955
|
+
console.log(pc4.green(`\u2713 Added ${pc4.bold(options.add)} to Jonah Fleet registry.`));
|
|
956
|
+
console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
|
|
957
|
+
`));
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (options.remove) {
|
|
961
|
+
const updated = removeGlobalRepository(options.remove);
|
|
962
|
+
console.log(pc4.yellow(`\u2713 Removed ${pc4.bold(options.remove)} from Jonah Fleet registry.`));
|
|
963
|
+
console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
|
|
964
|
+
`));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
let targetRepos = [];
|
|
968
|
+
if (options.repos && options.repos.length > 0) {
|
|
969
|
+
targetRepos = options.repos;
|
|
970
|
+
} else {
|
|
971
|
+
targetRepos = getFleetRepositories(cwd);
|
|
972
|
+
}
|
|
973
|
+
if (targetRepos.length === 0) {
|
|
974
|
+
try {
|
|
975
|
+
const remoteRaw = await executor(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
|
|
976
|
+
const currentRepo = remoteRaw.trim();
|
|
977
|
+
if (currentRepo) {
|
|
978
|
+
targetRepos = [currentRepo];
|
|
979
|
+
}
|
|
980
|
+
} catch {
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (targetRepos.length === 0) {
|
|
984
|
+
console.log(pc4.yellow("\n\u26A0\uFE0F No fleet repositories registered."));
|
|
985
|
+
console.log(pc4.cyan("Add repositories to monitor with:"));
|
|
986
|
+
console.log(pc4.gray(" jonah-fleet monitor --add owner/repo"));
|
|
987
|
+
console.log(pc4.gray("Or specify repositories directly:"));
|
|
988
|
+
console.log(pc4.gray(" jonah-fleet monitor owner/repo-1 owner/repo-2\n"));
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const pollAndRender = async () => {
|
|
992
|
+
const statuses = await Promise.all(
|
|
993
|
+
targetRepos.map((repo) => queryRepoFleetStatus(repo, executor))
|
|
994
|
+
);
|
|
995
|
+
if (options.watch && !options.json) {
|
|
996
|
+
console.clear();
|
|
997
|
+
}
|
|
998
|
+
const output = renderFleetDashboard(statuses, {
|
|
999
|
+
json: options.json,
|
|
1000
|
+
tokens: options.tokens,
|
|
1001
|
+
detailed: options.detailed
|
|
1002
|
+
});
|
|
1003
|
+
console.log(output);
|
|
1004
|
+
};
|
|
1005
|
+
await pollAndRender();
|
|
1006
|
+
if (options.watch) {
|
|
1007
|
+
const intervalSec = typeof options.interval === "number" ? options.interval : parseInt(options.interval || "10", 10) || 10;
|
|
1008
|
+
const timer = setInterval(async () => {
|
|
1009
|
+
await pollAndRender();
|
|
1010
|
+
}, intervalSec * 1e3);
|
|
1011
|
+
const handleSigint = () => {
|
|
1012
|
+
clearInterval(timer);
|
|
1013
|
+
process.removeListener("SIGINT", handleSigint);
|
|
1014
|
+
console.log(pc4.gray("\nStopped live fleet monitoring.\n"));
|
|
1015
|
+
process.exit(0);
|
|
1016
|
+
};
|
|
1017
|
+
process.on("SIGINT", handleSigint);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// src/commands/status.ts
|
|
367
1022
|
async function runStatus(options = {}) {
|
|
368
1023
|
const cwd = options.cwd || process.cwd();
|
|
1024
|
+
if (options.fleet) {
|
|
1025
|
+
await runMonitor({ cwd, json: options.json, tokens: options.tokens, detailed: options.detailed });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
369
1028
|
const manifest = loadManifest(cwd);
|
|
370
1029
|
if (!manifest) {
|
|
371
|
-
|
|
1030
|
+
if (options.json) {
|
|
1031
|
+
console.log(JSON.stringify({ error: "No agents-manifest.json found", cwd }, null, 2));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
console.log(pc5.yellow(`
|
|
372
1035
|
\u26A0\uFE0F No agents-manifest.json found in ${cwd}. This project is not configured with Jonah Fleet.`));
|
|
373
|
-
console.log(
|
|
1036
|
+
console.log(pc5.cyan(`Run 'npx jonah-fleet init' to set up autonomous agent routines.
|
|
374
1037
|
`));
|
|
375
1038
|
return;
|
|
376
1039
|
}
|
|
377
|
-
|
|
1040
|
+
const drift = checkDrift(cwd, manifest);
|
|
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
|
+
}
|
|
1068
|
+
if (options.json) {
|
|
1069
|
+
console.log(
|
|
1070
|
+
JSON.stringify(
|
|
1071
|
+
{
|
|
1072
|
+
cwd,
|
|
1073
|
+
version: manifest.version,
|
|
1074
|
+
fleetLatestVersion: FLEET_VERSION,
|
|
1075
|
+
preset: manifest.preset,
|
|
1076
|
+
autoUpdate: manifest.autoUpdate,
|
|
1077
|
+
routines: manifest.routines,
|
|
1078
|
+
skills: manifest.skills,
|
|
1079
|
+
repositories: manifest.repositories || [],
|
|
1080
|
+
tokenUsage,
|
|
1081
|
+
drift: {
|
|
1082
|
+
hasDrift,
|
|
1083
|
+
...drift
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
null,
|
|
1087
|
+
2
|
|
1088
|
+
)
|
|
1089
|
+
);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
console.log(pc5.bold(pc5.cyan(`
|
|
378
1093
|
\u{1F4CA} Jonah Fleet Status for ${cwd}
|
|
379
1094
|
`)));
|
|
380
|
-
console.log(` Version: ${manifest.version === FLEET_VERSION ?
|
|
381
|
-
console.log(` Preset: ${
|
|
382
|
-
console.log(` Auto-Update: ${manifest.autoUpdate?.enabled ?
|
|
383
|
-
console.log(
|
|
1095
|
+
console.log(` Version: ${manifest.version === FLEET_VERSION ? pc5.green(manifest.version) : pc5.yellow(`${manifest.version} (fleet latest: ${FLEET_VERSION})`)}`);
|
|
1096
|
+
console.log(` Preset: ${pc5.bold(manifest.preset)}`);
|
|
1097
|
+
console.log(` Auto-Update: ${manifest.autoUpdate?.enabled ? pc5.green("Enabled (" + manifest.autoUpdate.channel + ")") : pc5.gray("Disabled")}`);
|
|
1098
|
+
console.log(pc5.bold("\n Enabled Routines:"));
|
|
384
1099
|
for (const [routine, enabled] of Object.entries(manifest.routines)) {
|
|
385
|
-
console.log(` - ${routine.padEnd(35)}: ${enabled ?
|
|
1100
|
+
console.log(` - ${routine.padEnd(35)}: ${enabled ? pc5.green("ENABLED") : pc5.gray("DISABLED")}`);
|
|
386
1101
|
}
|
|
387
|
-
console.log(
|
|
1102
|
+
console.log(pc5.bold("\n Configured Skills:"));
|
|
388
1103
|
for (const skill of manifest.skills) {
|
|
389
|
-
console.log(` - ${
|
|
1104
|
+
console.log(` - ${pc5.cyan(skill)}`);
|
|
390
1105
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
1106
|
+
if (manifest.repositories && manifest.repositories.length > 0) {
|
|
1107
|
+
console.log(pc5.bold("\n Fleet Repositories:"));
|
|
1108
|
+
for (const repo of manifest.repositories) {
|
|
1109
|
+
console.log(` - ${pc5.cyan(repo)}`);
|
|
1110
|
+
}
|
|
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
|
+
}
|
|
1128
|
+
console.log(pc5.bold("\n Drift / Health:"));
|
|
394
1129
|
if (!hasDrift) {
|
|
395
|
-
console.log(
|
|
1130
|
+
console.log(pc5.green(" \u2713 All prompts, workflows, and skills are healthy and match fleet templates.\n"));
|
|
396
1131
|
} 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(
|
|
1132
|
+
if (drift.missingPrompts.length > 0) console.log(pc5.red(` \u274C Missing prompts: ${drift.missingPrompts.join(", ")}`));
|
|
1133
|
+
if (drift.modifiedPrompts.length > 0) console.log(pc5.yellow(` \u26A0\uFE0F Modified prompts: ${drift.modifiedPrompts.join(", ")}`));
|
|
1134
|
+
if (drift.missingWorkflows.length > 0) console.log(pc5.red(` \u274C Missing workflows: ${drift.missingWorkflows.join(", ")}`));
|
|
1135
|
+
if (drift.modifiedWorkflows.length > 0) console.log(pc5.yellow(` \u26A0\uFE0F Modified workflows: ${drift.modifiedWorkflows.join(", ")}`));
|
|
1136
|
+
if (drift.missingSkills.length > 0) console.log(pc5.red(` \u274C Missing skills: ${drift.missingSkills.join(", ")}`));
|
|
1137
|
+
console.log(pc5.cyan("\n Run 'jonah-fleet sync' to synchronize files.\n"));
|
|
403
1138
|
}
|
|
404
1139
|
}
|
|
405
1140
|
|
|
406
1141
|
// src/commands/contribute.ts
|
|
407
1142
|
import { execSync } from "child_process";
|
|
408
|
-
import
|
|
1143
|
+
import pc6 from "picocolors";
|
|
409
1144
|
async function runContribute(options = {}) {
|
|
410
|
-
console.log(
|
|
1145
|
+
console.log(pc6.cyan(`
|
|
411
1146
|
\u{1F680} Jonah Fleet Upstream Contribution Bridge
|
|
412
1147
|
`));
|
|
413
1148
|
const title = options.title || "fix(prompts): improve orchestrator routine handling";
|
|
414
1149
|
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: ${
|
|
1150
|
+
console.log(`Preparing upstream contribution PR against ${pc6.bold("juliendurandeu/jonah-fleet")}...`);
|
|
1151
|
+
console.log(`Title: ${pc6.green(title)}`);
|
|
1152
|
+
console.log(`Body: ${pc6.gray(body)}
|
|
418
1153
|
`);
|
|
419
1154
|
try {
|
|
420
1155
|
const branchName = `contrib/optimize-${Date.now()}`;
|
|
421
|
-
console.log(`Creating branch ${
|
|
1156
|
+
console.log(`Creating branch ${pc6.cyan(branchName)}...`);
|
|
422
1157
|
try {
|
|
423
1158
|
execSync("gh auth status", { stdio: "pipe" });
|
|
424
1159
|
} catch {
|
|
425
|
-
console.error(
|
|
1160
|
+
console.error(pc6.red("\u274C GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first."));
|
|
426
1161
|
process.exit(1);
|
|
427
1162
|
}
|
|
428
|
-
console.log(
|
|
429
|
-
console.log(
|
|
1163
|
+
console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to juliendurandeu/jonah-fleet.`));
|
|
1164
|
+
console.log(pc6.cyan(`Command executed by optimizer routine or operator:
|
|
430
1165
|
`));
|
|
431
1166
|
console.log(` gh pr create --repo juliendurandeu/jonah-fleet --title "${title}" --body "${body}"
|
|
432
1167
|
`);
|
|
433
1168
|
} catch (err) {
|
|
434
|
-
console.error(
|
|
1169
|
+
console.error(pc6.red(`\u274C Error during contribution preparation: ${err.message}`));
|
|
435
1170
|
process.exit(1);
|
|
436
1171
|
}
|
|
437
1172
|
}
|
|
@@ -445,9 +1180,12 @@ program.command("init").description("Initialize Jonah Fleet configuration, routi
|
|
|
445
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) => {
|
|
446
1181
|
await runSync(options);
|
|
447
1182
|
});
|
|
448
|
-
program.command("status").description("Check the status, health, and drift of installed agent routines and skills").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) => {
|
|
449
1184
|
await runStatus(options);
|
|
450
1185
|
});
|
|
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) => {
|
|
1187
|
+
await runMonitor({ ...options, repos });
|
|
1188
|
+
});
|
|
451
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) => {
|
|
452
1190
|
await runContribute(options);
|
|
453
1191
|
});
|
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"
|
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": {
|
|
@@ -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.
|