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 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**: Single-flight locking, stale-claim recovery, and reader/writer separation.
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 project context (`AGENTS.md`)
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
- ### 3. Check health and drift
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
- ### 4. Multi-repo Fleet Monitoring
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.1.0";
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 fs5 from "fs";
438
- import path5 from "path";
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 inputTokens = 0;
484
- let outputTokens = 0;
485
- let cost = 0;
486
- let runs = 0;
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
- runs++;
493
- if (meta.inputTokens) inputTokens += meta.inputTokens;
494
- if (meta.outputTokens) outputTokens += meta.outputTokens;
495
- if (meta.estimatedCost) cost += meta.estimatedCost;
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: inputTokens,
499
- sevenDayOutputTokens: outputTokens,
500
- sevenDayTotalTokens: inputTokens + outputTokens,
501
- sevenDayEstimatedCost: cost,
502
- recentRunCount: runs
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 (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
557
- const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
558
- if (fs5.existsSync(manifestPath)) {
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(fs5.readFileSync(manifestPath, "utf8"));
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 (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
641
- const logsDir = path5.join(repoIdentifier, ".github/prompts/logs");
642
- if (fs5.existsSync(logsDir)) {
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 = fs5.readdirSync(dir, { withFileTypes: true });
646
+ const entries = fs4.readdirSync(dir, { withFileTypes: true });
645
647
  for (const entry of entries) {
646
- const fullPath = path5.join(dir, entry.name);
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(fs5.readFileSync(fullPath, "utf8"));
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, { json: options.json });
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.1.0",
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 instance of the pattern formalized for orchestrating coding agents against an issue tracker. There is **no long-running orchestrator daemon**; the roles map onto GitHub primitives:
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: `prompt_unclear`, `data_issue`, `token_limit`, `infeasible_task`.
43
- 3. Compute efficiency metrics: PRs with 3+ review rounds, high iteration usage.
44
- 4. Analyze resolved bugs for missing checks in authoring (`autowork.md`) or review (`peer-review.md`).
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
- - Enhance pre-ready checklist in `autowork.md`
50
- - Add automated verification tests or invariant checks
51
- - Clarify ambiguous prompt instructions
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, linked issue exists) 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
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, and has a linked issue (`Closes #N`).
41
- - Sequence: (1) squash-merge, (2) submit held review comments, (3) file follow-up issues for deferred material findings.
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 linked issue.
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.