jonah-fleet 1.1.0 → 1.4.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +49 -6
  3. package/dist/commands/contribute.d.ts +29 -0
  4. package/dist/commands/contribute.d.ts.map +1 -0
  5. package/dist/commands/daemon.d.ts +8 -0
  6. package/dist/commands/daemon.d.ts.map +1 -0
  7. package/dist/commands/init.d.ts +13 -0
  8. package/dist/commands/init.d.ts.map +1 -0
  9. package/dist/commands/monitor.d.ts +16 -0
  10. package/dist/commands/monitor.d.ts.map +1 -0
  11. package/dist/commands/run.d.ts +11 -0
  12. package/dist/commands/run.d.ts.map +1 -0
  13. package/dist/commands/status.d.ts +9 -0
  14. package/dist/commands/status.d.ts.map +1 -0
  15. package/dist/commands/sync.d.ts +7 -0
  16. package/dist/commands/sync.d.ts.map +1 -0
  17. package/dist/commands/telemetry.d.ts +14 -0
  18. package/dist/commands/telemetry.d.ts.map +1 -0
  19. package/dist/index.d.ts +2 -2
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +1986 -211
  22. package/dist/lib/daemon.d.ts +34 -0
  23. package/dist/lib/daemon.d.ts.map +1 -0
  24. package/dist/lib/dashboard.d.ts +10 -0
  25. package/dist/lib/dashboard.d.ts.map +1 -0
  26. package/dist/lib/detector.d.ts +25 -0
  27. package/dist/lib/detector.d.ts.map +1 -0
  28. package/dist/lib/diff.d.ts +10 -0
  29. package/dist/lib/diff.d.ts.map +1 -0
  30. package/dist/lib/evals.d.ts +72 -0
  31. package/dist/lib/evals.d.ts.map +1 -0
  32. package/dist/lib/fleet-query.d.ts +102 -0
  33. package/dist/lib/fleet-query.d.ts.map +1 -0
  34. package/dist/lib/global-config.d.ts +10 -0
  35. package/dist/lib/global-config.d.ts.map +1 -0
  36. package/dist/lib/installer.d.ts +27 -0
  37. package/dist/lib/installer.d.ts.map +1 -0
  38. package/dist/lib/manifest.d.ts +6 -0
  39. package/dist/lib/manifest.d.ts.map +1 -0
  40. package/dist/lib/presets.d.ts +52 -0
  41. package/dist/lib/presets.d.ts.map +1 -0
  42. package/dist/lib/runner.d.ts +40 -0
  43. package/dist/lib/runner.d.ts.map +1 -0
  44. package/dist/lib/telemetry.d.ts +90 -0
  45. package/dist/lib/telemetry.d.ts.map +1 -0
  46. package/dist/lib/worktree.d.ts +34 -0
  47. package/dist/lib/worktree.d.ts.map +1 -0
  48. package/package.json +17 -5
  49. package/schema.json +44 -1
  50. package/templates/docs/AGENTS.template.md +15 -0
  51. package/templates/prompts/ORCHESTRATION.md +107 -7
  52. package/templates/prompts/analytics-review.md +91 -0
  53. package/templates/prompts/autowork.md +15 -4
  54. package/templates/prompts/issues-housekeeping.md +2 -1
  55. package/templates/prompts/optimizer.md +46 -8
  56. package/templates/prompts/peer-review.md +35 -11
  57. package/templates/prompts/product-planning.md +18 -7
  58. package/templates/skills/diagnosing-bugs/SKILL.md +9 -0
  59. package/templates/workflows/autowork-cron.yml +19 -1
  60. package/templates/workflows/prompt-optimizer-cron.yml +22 -0
  61. package/templates/workflows/trigger-autowork-manual.yml +169 -0
  62. package/templates/workflows/trigger-autowork-on-bug.yml +5 -5
  63. package/templates/workflows/trigger-autowork-on-merge.yml +3 -3
  64. package/templates/workflows/trigger-review-routine.yml +107 -11
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/commands/init.ts
7
+ import readline from "readline";
7
8
  import pc from "picocolors";
8
9
 
9
10
  // src/lib/manifest.ts
@@ -19,7 +20,8 @@ var PRESET_CONFIGS = {
19
20
  optimizer: true,
20
21
  "issues-housekeeping": false,
21
22
  "dependency-update-security-check": false,
22
- "product-planning": false
23
+ "product-planning": false,
24
+ "analytics-review": false
23
25
  },
24
26
  skills: [
25
27
  "tdd",
@@ -36,7 +38,8 @@ var PRESET_CONFIGS = {
36
38
  optimizer: true,
37
39
  "issues-housekeeping": true,
38
40
  "dependency-update-security-check": true,
39
- "product-planning": false
41
+ "product-planning": false,
42
+ "analytics-review": false
40
43
  },
41
44
  skills: [
42
45
  "tdd",
@@ -56,7 +59,8 @@ var PRESET_CONFIGS = {
56
59
  optimizer: true,
57
60
  "issues-housekeeping": true,
58
61
  "dependency-update-security-check": true,
59
- "product-planning": true
62
+ "product-planning": true,
63
+ "analytics-review": true
60
64
  },
61
65
  skills: [
62
66
  "tdd",
@@ -73,14 +77,20 @@ var PRESET_CONFIGS = {
73
77
  }
74
78
  };
75
79
  var ROUTINE_TO_WORKFLOW_MAP = {
76
- autowork: ["autowork-cron.yml", "trigger-autowork-on-merge.yml", "trigger-autowork-on-bug.yml"],
80
+ autowork: [
81
+ "autowork-cron.yml",
82
+ "trigger-autowork-on-merge.yml",
83
+ "trigger-autowork-on-bug.yml",
84
+ "trigger-autowork-manual.yml"
85
+ ],
77
86
  "peer-review": ["trigger-review-routine.yml"],
78
87
  optimizer: ["prompt-optimizer-cron.yml"],
79
88
  "issues-housekeeping": ["issues-housekeeping-cron.yml"],
80
89
  "dependency-update-security-check": ["dependency-check-cron.yml"],
81
- "product-planning": []
90
+ "product-planning": [],
91
+ "analytics-review": []
82
92
  };
83
- var FLEET_VERSION = "1.1.0";
93
+ var FLEET_VERSION = "1.4.0";
84
94
  var SCHEMA_URL = "https://raw.githubusercontent.com/juliendurandeu/jonah-fleet/main/schema.json";
85
95
 
86
96
  // src/lib/manifest.ts
@@ -113,7 +123,8 @@ function createDefaultManifest(preset = "standard") {
113
123
  optimizer: true,
114
124
  "issues-housekeeping": true,
115
125
  "dependency-update-security-check": true,
116
- "product-planning": false
126
+ "product-planning": false,
127
+ "analytics-review": false
117
128
  },
118
129
  skills: PRESET_CONFIGS.standard.skills,
119
130
  autoUpdate: {
@@ -137,18 +148,393 @@ function createDefaultManifest(preset = "standard") {
137
148
  }
138
149
 
139
150
  // src/lib/installer.ts
151
+ import fs3 from "fs";
152
+ import path3 from "path";
153
+ import { fileURLToPath } from "url";
154
+
155
+ // src/lib/detector.ts
140
156
  import fs2 from "fs";
141
157
  import path2 from "path";
142
- import { fileURLToPath } from "url";
158
+ function detectTechStack(projectDir) {
159
+ const detectedFiles = [];
160
+ const fileExists = (relPath) => {
161
+ const full = path2.join(projectDir, relPath);
162
+ if (fs2.existsSync(full)) {
163
+ detectedFiles.push(relPath);
164
+ return true;
165
+ }
166
+ return false;
167
+ };
168
+ const readFileSafe = (relPath) => {
169
+ try {
170
+ const full = path2.join(projectDir, relPath);
171
+ if (fs2.existsSync(full)) {
172
+ return fs2.readFileSync(full, "utf8");
173
+ }
174
+ } catch {
175
+ }
176
+ return "";
177
+ };
178
+ if (fileExists("Cargo.toml")) {
179
+ const cargoToml = readFileSafe("Cargo.toml");
180
+ let framework;
181
+ if (cargoToml.includes("axum")) framework = "Axum";
182
+ else if (cargoToml.includes("actix-web")) framework = "Actix Web";
183
+ else if (cargoToml.includes("rocket")) framework = "Rocket";
184
+ else if (cargoToml.includes("tokio")) framework = "Tokio";
185
+ const name = framework ? `Rust / ${framework}` : "Rust";
186
+ return {
187
+ name,
188
+ language: "Rust",
189
+ framework,
190
+ packageManager: "cargo",
191
+ testFramework: "cargo test",
192
+ linter: "cargo clippy",
193
+ typeChecker: "cargo check",
194
+ commands: {
195
+ dev: "cargo run",
196
+ build: "cargo build --release",
197
+ lint: "cargo clippy -- -D warnings",
198
+ typeCheck: "cargo check",
199
+ test: "cargo test"
200
+ },
201
+ detectedFiles
202
+ };
203
+ }
204
+ if (fileExists("go.mod") || fileExists("main.go")) {
205
+ const goMod = readFileSafe("go.mod");
206
+ let framework;
207
+ if (goMod.includes("github.com/gin-gonic/gin")) framework = "Gin";
208
+ else if (goMod.includes("github.com/labstack/echo")) framework = "Echo";
209
+ else if (goMod.includes("github.com/go-chi/chi")) framework = "Chi";
210
+ else if (goMod.includes("github.com/gofiber/fiber")) framework = "Fiber";
211
+ const hasGolangCi = fileExists(".golangci.yml") || fileExists(".golangci.yaml");
212
+ const name = framework ? `Go / ${framework}` : "Go";
213
+ return {
214
+ name,
215
+ language: "Go",
216
+ framework,
217
+ packageManager: "go",
218
+ testFramework: "go test",
219
+ linter: hasGolangCi ? "golangci-lint" : void 0,
220
+ commands: {
221
+ dev: "go run .",
222
+ build: "go build -v ./...",
223
+ lint: hasGolangCi ? "golangci-lint run" : void 0,
224
+ test: "go test ./..."
225
+ },
226
+ detectedFiles
227
+ };
228
+ }
229
+ const hasPyProject = fileExists("pyproject.toml");
230
+ const hasRequirements = fileExists("requirements.txt");
231
+ const hasPipfile = fileExists("Pipfile");
232
+ const hasManagePy = fileExists("manage.py");
233
+ if (hasPyProject || hasRequirements || hasPipfile || hasManagePy) {
234
+ const pyproject = readFileSafe("pyproject.toml");
235
+ const requirements = readFileSafe("requirements.txt");
236
+ const allPythonMeta = `${pyproject}
237
+ ${requirements}`.toLowerCase();
238
+ let packageManager = "pip";
239
+ if (fileExists("uv.lock") || pyproject.includes("[tool.uv]")) {
240
+ packageManager = "uv";
241
+ } else if (fileExists("poetry.lock") || pyproject.includes("[tool.poetry]")) {
242
+ packageManager = "poetry";
243
+ } else if (hasPipfile) {
244
+ packageManager = "pipenv";
245
+ }
246
+ let framework;
247
+ if (allPythonMeta.includes("fastapi") || pyproject.includes("fastapi")) {
248
+ framework = "FastAPI";
249
+ } else if (allPythonMeta.includes("django") || hasManagePy) {
250
+ framework = "Django";
251
+ } else if (allPythonMeta.includes("flask")) {
252
+ framework = "Flask";
253
+ }
254
+ let testFramework = "pytest";
255
+ if (allPythonMeta.includes("pytest") || fileExists("pytest.ini") || fileExists("conftest.py")) {
256
+ testFramework = "pytest";
257
+ }
258
+ let linter;
259
+ if (allPythonMeta.includes("ruff") || fileExists("ruff.toml")) {
260
+ linter = "ruff";
261
+ } else if (allPythonMeta.includes("flake8") || fileExists(".flake8")) {
262
+ linter = "flake8";
263
+ }
264
+ let typeChecker;
265
+ if (allPythonMeta.includes("mypy") || fileExists("mypy.ini")) {
266
+ typeChecker = "mypy";
267
+ } else if (allPythonMeta.includes("pyright") || fileExists("pyrightconfig.json")) {
268
+ typeChecker = "pyright";
269
+ }
270
+ const pmPrefix = packageManager === "uv" ? "uv run " : packageManager === "poetry" ? "poetry run " : "";
271
+ let devCmd = `${pmPrefix}python main.py`;
272
+ if (framework === "FastAPI") {
273
+ devCmd = `${pmPrefix}fastapi dev`;
274
+ } else if (framework === "Django") {
275
+ devCmd = `${pmPrefix}python manage.py runserver`;
276
+ } else if (framework === "Flask") {
277
+ devCmd = `${pmPrefix}flask run`;
278
+ }
279
+ const testCmd = `${pmPrefix}${testFramework}`;
280
+ const lintCmd = linter ? `${pmPrefix}${linter}${linter === "ruff" ? " check ." : ""}` : void 0;
281
+ const typeCheckCmd = typeChecker ? `${pmPrefix}${typeChecker} .` : void 0;
282
+ const name = framework ? `Python / ${framework}` : "Python";
283
+ return {
284
+ name,
285
+ language: "Python",
286
+ framework,
287
+ packageManager,
288
+ testFramework,
289
+ linter,
290
+ typeChecker,
291
+ commands: {
292
+ dev: devCmd,
293
+ test: testCmd,
294
+ lint: lintCmd,
295
+ typeCheck: typeCheckCmd
296
+ },
297
+ detectedFiles
298
+ };
299
+ }
300
+ if (fileExists("package.json")) {
301
+ let pkg = {};
302
+ try {
303
+ pkg = JSON.parse(readFileSafe("package.json"));
304
+ } catch {
305
+ pkg = {};
306
+ }
307
+ const allDeps = {
308
+ ...pkg.dependencies || {},
309
+ ...pkg.devDependencies || {}
310
+ };
311
+ const scripts = pkg.scripts || {};
312
+ let packageManager = "npm";
313
+ if (fileExists("pnpm-lock.yaml")) {
314
+ packageManager = "pnpm";
315
+ } else if (fileExists("yarn.lock")) {
316
+ packageManager = "yarn";
317
+ } else if (fileExists("bun.lockb") || fileExists("bun.lock")) {
318
+ packageManager = "bun";
319
+ } else if (fileExists("package-lock.json")) {
320
+ packageManager = "npm";
321
+ }
322
+ const runPrefix = (scriptName) => {
323
+ if (packageManager === "npm") {
324
+ return scriptName === "test" || scriptName === "start" ? `npm ${scriptName}` : `npm run ${scriptName}`;
325
+ }
326
+ return `${packageManager} ${scriptName === "test" || scriptName === "start" ? scriptName : `run ${scriptName}`}`;
327
+ };
328
+ const hasTypeScript = Boolean(allDeps.typescript || fileExists("tsconfig.json"));
329
+ const language = hasTypeScript ? "TypeScript" : "JavaScript";
330
+ let name = "Node.js";
331
+ let framework;
332
+ if (allDeps.next || fileExists("next.config.js") || fileExists("next.config.mjs") || fileExists("next.config.ts")) {
333
+ framework = "Next.js";
334
+ name = "Next.js / React";
335
+ } else if (allDeps.remix || allDeps["@remix-run/node"]) {
336
+ framework = "Remix";
337
+ name = "Remix / React";
338
+ } else if (allDeps.astro) {
339
+ framework = "Astro";
340
+ name = "Astro";
341
+ } else if (allDeps.nuxt || allDeps.vue) {
342
+ framework = allDeps.nuxt ? "Nuxt" : "Vue";
343
+ name = framework;
344
+ } else if (allDeps["@sveltejs/kit"] || allDeps.svelte) {
345
+ framework = allDeps["@sveltejs/kit"] ? "SvelteKit" : "Svelte";
346
+ name = framework;
347
+ } else if (allDeps.react) {
348
+ framework = "React";
349
+ name = "React";
350
+ } else if (allDeps.express) {
351
+ framework = "Express";
352
+ name = "Node.js / Express";
353
+ } else if (allDeps.fastify) {
354
+ framework = "Fastify";
355
+ name = "Node.js / Fastify";
356
+ } else if (allDeps["@nestjs/core"]) {
357
+ framework = "NestJS";
358
+ name = "NestJS";
359
+ } else if (allDeps.koa) {
360
+ framework = "Koa";
361
+ name = "Node.js / Koa";
362
+ } else if (allDeps.hono) {
363
+ framework = "Hono";
364
+ name = "Hono";
365
+ }
366
+ let styling;
367
+ if (allDeps.tailwindcss || fileExists("tailwind.config.js") || fileExists("tailwind.config.ts")) {
368
+ styling = "Tailwind CSS";
369
+ }
370
+ const testFrameworks = [];
371
+ if (allDeps.vitest) testFrameworks.push("Vitest");
372
+ if (allDeps.jest) testFrameworks.push("Jest");
373
+ if (allDeps["@playwright/test"]) testFrameworks.push("Playwright");
374
+ if (allDeps.cypress) testFrameworks.push("Cypress");
375
+ const testFramework = testFrameworks.length > 0 ? testFrameworks.join(", ") : scripts.test ? "npm test" : void 0;
376
+ let linter;
377
+ if (allDeps.eslint || fileExists(".eslintrc.json") || fileExists("eslint.config.js") || fileExists("eslint.config.mjs")) {
378
+ linter = "ESLint";
379
+ } else if (allDeps["@biomejs/biome"] || fileExists("biome.json")) {
380
+ linter = "Biome";
381
+ }
382
+ const typeChecker = hasTypeScript ? "tsc" : void 0;
383
+ const commands = {};
384
+ if (scripts.dev) {
385
+ commands.dev = runPrefix("dev");
386
+ } else if (scripts.start) {
387
+ commands.dev = runPrefix("start");
388
+ } else {
389
+ commands.dev = `${packageManager} start`;
390
+ }
391
+ if (scripts.build) {
392
+ commands.build = runPrefix("build");
393
+ }
394
+ if (scripts.lint) {
395
+ commands.lint = runPrefix("lint");
396
+ } else if (linter === "ESLint") {
397
+ commands.lint = `${packageManager === "npm" ? "npx" : packageManager} eslint .`;
398
+ }
399
+ if (scripts["type-check"]) {
400
+ commands.typeCheck = runPrefix("type-check");
401
+ } else if (scripts.typecheck) {
402
+ commands.typeCheck = runPrefix("typecheck");
403
+ } else if (hasTypeScript) {
404
+ commands.typeCheck = "npx tsc --noEmit";
405
+ }
406
+ if (scripts.test) {
407
+ commands.test = runPrefix("test");
408
+ }
409
+ if (scripts["test:watch"]) {
410
+ commands.testWatch = runPrefix("test:watch");
411
+ }
412
+ return {
413
+ name,
414
+ language,
415
+ framework,
416
+ packageManager,
417
+ testFramework,
418
+ linter,
419
+ typeChecker,
420
+ styling,
421
+ commands,
422
+ detectedFiles
423
+ };
424
+ }
425
+ return {
426
+ name: "Generic",
427
+ language: "Generic",
428
+ packageManager: "npm",
429
+ commands: {
430
+ dev: "npm run dev",
431
+ build: "npm run build",
432
+ lint: "npm run lint",
433
+ test: "npm test"
434
+ },
435
+ detectedFiles
436
+ };
437
+ }
438
+ function renderAgentsTemplate(templateContent, stack) {
439
+ let rendered = templateContent;
440
+ let frameworkLanguage = "{Framework / Language}";
441
+ if (stack.framework && stack.framework !== stack.language) {
442
+ frameworkLanguage = `${stack.language} (${stack.framework})`;
443
+ } else if (stack.name && stack.name !== "Generic") {
444
+ frameworkLanguage = stack.name;
445
+ } else if (stack.language && stack.language !== "Generic") {
446
+ frameworkLanguage = stack.language;
447
+ }
448
+ rendered = rendered.replace(
449
+ /- \*\*Framework \/ Language\*\*:.*/,
450
+ () => `- **Framework / Language**: ${frameworkLanguage}`
451
+ );
452
+ if (stack.packageManager) {
453
+ rendered = rendered.replace(
454
+ /- \*\*Package Manager\*\*:.*/,
455
+ () => `- **Package Manager**: ${stack.packageManager}`
456
+ );
457
+ }
458
+ if (stack.testFramework) {
459
+ rendered = rendered.replace(
460
+ /- \*\*Testing\*\*:.*/,
461
+ () => `- **Testing**: ${stack.testFramework}`
462
+ );
463
+ }
464
+ if (stack.styling) {
465
+ rendered = rendered.replace(
466
+ /- \*\*Styling \/ UI\*\*:.*/,
467
+ () => `- **Styling / UI**: ${stack.styling}`
468
+ );
469
+ }
470
+ const workflowLines = [];
471
+ if (stack.commands.dev) {
472
+ workflowLines.push(`${stack.commands.dev.padEnd(20)} # Local dev server / run`);
473
+ }
474
+ if (stack.commands.build) {
475
+ workflowLines.push(`${stack.commands.build.padEnd(20)} # Production build`);
476
+ }
477
+ if (stack.commands.lint) {
478
+ workflowLines.push(`${stack.commands.lint.padEnd(20)} # Lint / static analysis`);
479
+ }
480
+ if (stack.commands.typeCheck) {
481
+ workflowLines.push(`${stack.commands.typeCheck.padEnd(20)} # Type / compiler check`);
482
+ }
483
+ if (stack.commands.test) {
484
+ workflowLines.push(`${stack.commands.test.padEnd(20)} # Run test suite`);
485
+ }
486
+ if (stack.commands.testWatch) {
487
+ workflowLines.push(`${stack.commands.testWatch.padEnd(20)} # Run tests in watch mode`);
488
+ }
489
+ if (workflowLines.length > 0) {
490
+ const workflowsBlock = `\`\`\`bash
491
+ ${workflowLines.join("\n")}
492
+ \`\`\``;
493
+ rendered = rendered.replace(/```bash[\s\S]*?```/, () => workflowsBlock);
494
+ }
495
+ return rendered;
496
+ }
497
+
498
+ // src/lib/installer.ts
143
499
  var __filename2 = fileURLToPath(import.meta.url);
144
- var __dirname2 = path2.dirname(__filename2);
500
+ var __dirname2 = path3.dirname(__filename2);
145
501
  function getTemplatesDir() {
146
- const candidate1 = path2.resolve(__dirname2, "../templates");
147
- const candidate2 = path2.resolve(__dirname2, "../../templates");
148
- if (fs2.existsSync(candidate1)) return candidate1;
149
- if (fs2.existsSync(candidate2)) return candidate2;
502
+ const candidate1 = path3.resolve(__dirname2, "../templates");
503
+ const candidate2 = path3.resolve(__dirname2, "../../templates");
504
+ if (fs3.existsSync(candidate1)) return candidate1;
505
+ if (fs3.existsSync(candidate2)) return candidate2;
150
506
  throw new Error(`Templates directory not found at ${candidate1} or ${candidate2}`);
151
507
  }
508
+ function applyWorkflowSchedule(content, customCron) {
509
+ if (!customCron) return content;
510
+ return content.replace(
511
+ /(-\s*cron:\s*['"])([^'"]+)(['"])/,
512
+ `$1${customCron}$3`
513
+ );
514
+ }
515
+ function extractWorkflowSchedule(content) {
516
+ const match = content.match(/-\s*cron:\s*['"]([^'"]+)['"]/);
517
+ return match ? match[1] : null;
518
+ }
519
+ function resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent) {
520
+ if (routineName && manifest.schedules?.[routineName]) {
521
+ return manifest.schedules[routineName];
522
+ }
523
+ if (manifest.schedules?.[workflowFile]) {
524
+ return manifest.schedules[workflowFile];
525
+ }
526
+ const baseName = workflowFile.replace(/\.(yml|yaml)$/, "");
527
+ if (manifest.schedules?.[baseName]) {
528
+ return manifest.schedules[baseName];
529
+ }
530
+ if (destContent) {
531
+ const existingCron = extractWorkflowSchedule(destContent);
532
+ if (existingCron) {
533
+ return existingCron;
534
+ }
535
+ }
536
+ return void 0;
537
+ }
152
538
  function installFleet(targetDir, manifest, options = {}) {
153
539
  const templatesDir = getTemplatesDir();
154
540
  const result = {
@@ -157,19 +543,19 @@ function installFleet(targetDir, manifest, options = {}) {
157
543
  skillsInstalled: [],
158
544
  docsInstalled: []
159
545
  };
160
- const targetPromptsDir = path2.join(targetDir, ".github/prompts");
161
- const targetWorkflowsDir = path2.join(targetDir, ".github/workflows");
162
- const targetSkillsDir = path2.join(targetDir, ".agents/skills");
163
- fs2.mkdirSync(targetPromptsDir, { recursive: true });
164
- fs2.mkdirSync(targetWorkflowsDir, { recursive: true });
165
- fs2.mkdirSync(targetSkillsDir, { recursive: true });
546
+ const targetPromptsDir = path3.join(targetDir, ".github/prompts");
547
+ const targetWorkflowsDir = path3.join(targetDir, ".github/workflows");
548
+ const targetSkillsDir = path3.join(targetDir, ".agents/skills");
549
+ fs3.mkdirSync(targetPromptsDir, { recursive: true });
550
+ fs3.mkdirSync(targetWorkflowsDir, { recursive: true });
551
+ fs3.mkdirSync(targetSkillsDir, { recursive: true });
166
552
  const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
167
553
  for (const file of basePrompts) {
168
- const src = path2.join(templatesDir, "prompts", file);
169
- const dest = path2.join(targetPromptsDir, file);
170
- if (fs2.existsSync(src)) {
171
- if (!fs2.existsSync(dest) || options.force) {
172
- fs2.copyFileSync(src, dest);
554
+ const src = path3.join(templatesDir, "prompts", file);
555
+ const dest = path3.join(targetPromptsDir, file);
556
+ if (fs3.existsSync(src)) {
557
+ if (!fs3.existsSync(dest) || options.force) {
558
+ fs3.copyFileSync(src, dest);
173
559
  result.promptsInstalled.push(file);
174
560
  }
175
561
  }
@@ -177,53 +563,64 @@ function installFleet(targetDir, manifest, options = {}) {
177
563
  for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
178
564
  if (!isEnabled) continue;
179
565
  const promptFile = `${routineName}.md`;
180
- const promptSrc = path2.join(templatesDir, "prompts", promptFile);
181
- const promptDest = path2.join(targetPromptsDir, promptFile);
182
- if (fs2.existsSync(promptSrc)) {
183
- if (!fs2.existsSync(promptDest) || options.force) {
184
- fs2.copyFileSync(promptSrc, promptDest);
566
+ const promptSrc = path3.join(templatesDir, "prompts", promptFile);
567
+ const promptDest = path3.join(targetPromptsDir, promptFile);
568
+ if (fs3.existsSync(promptSrc)) {
569
+ if (!fs3.existsSync(promptDest) || options.force) {
570
+ fs3.copyFileSync(promptSrc, promptDest);
185
571
  result.promptsInstalled.push(promptFile);
186
572
  }
187
573
  }
188
574
  const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
189
575
  for (const workflowFile of workflows) {
190
- const wfSrc = path2.join(templatesDir, "workflows", workflowFile);
191
- const wfDest = path2.join(targetWorkflowsDir, workflowFile);
192
- if (fs2.existsSync(wfSrc)) {
193
- if (!fs2.existsSync(wfDest) || options.force) {
194
- fs2.copyFileSync(wfSrc, wfDest);
576
+ const wfSrc = path3.join(templatesDir, "workflows", workflowFile);
577
+ const wfDest = path3.join(targetWorkflowsDir, workflowFile);
578
+ if (fs3.existsSync(wfSrc)) {
579
+ if (!fs3.existsSync(wfDest) || options.force) {
580
+ const rawContent = fs3.readFileSync(wfSrc, "utf8");
581
+ const destContent = fs3.existsSync(wfDest) ? fs3.readFileSync(wfDest, "utf8") : void 0;
582
+ const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
583
+ const finalContent = applyWorkflowSchedule(rawContent, schedule);
584
+ fs3.writeFileSync(wfDest, finalContent, "utf8");
195
585
  result.workflowsInstalled.push(workflowFile);
196
586
  }
197
587
  }
198
588
  }
199
589
  }
200
590
  if (manifest.autoUpdate?.enabled) {
201
- const syncWfSrc = path2.join(templatesDir, "workflows/sync-fleet.yml");
202
- const syncWfDest = path2.join(targetWorkflowsDir, "sync-fleet.yml");
203
- if (fs2.existsSync(syncWfSrc)) {
204
- if (!fs2.existsSync(syncWfDest) || options.force) {
205
- fs2.copyFileSync(syncWfSrc, syncWfDest);
591
+ const syncWfSrc = path3.join(templatesDir, "workflows/sync-fleet.yml");
592
+ const syncWfDest = path3.join(targetWorkflowsDir, "sync-fleet.yml");
593
+ if (fs3.existsSync(syncWfSrc)) {
594
+ if (!fs3.existsSync(syncWfDest) || options.force) {
595
+ const rawContent = fs3.readFileSync(syncWfSrc, "utf8");
596
+ const destContent = fs3.existsSync(syncWfDest) ? fs3.readFileSync(syncWfDest, "utf8") : void 0;
597
+ const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
598
+ const finalContent = applyWorkflowSchedule(rawContent, schedule);
599
+ fs3.writeFileSync(syncWfDest, finalContent, "utf8");
206
600
  result.workflowsInstalled.push("sync-fleet.yml");
207
601
  }
208
602
  }
209
603
  }
210
604
  for (const skill of manifest.skills) {
211
- const skillSrcDir = path2.join(templatesDir, "skills", skill);
212
- const skillDestDir = path2.join(targetSkillsDir, skill);
213
- if (fs2.existsSync(skillSrcDir)) {
214
- if (!fs2.existsSync(skillDestDir) || options.force) {
215
- fs2.cpSync(skillSrcDir, skillDestDir, { recursive: true });
605
+ const skillSrcDir = path3.join(templatesDir, "skills", skill);
606
+ const skillDestDir = path3.join(targetSkillsDir, skill);
607
+ if (fs3.existsSync(skillSrcDir)) {
608
+ if (!fs3.existsSync(skillDestDir) || options.force) {
609
+ fs3.cpSync(skillSrcDir, skillDestDir, { recursive: true });
216
610
  result.skillsInstalled.push(skill);
217
611
  }
218
612
  }
219
613
  }
220
- const agentsPath = path2.join(targetDir, "AGENTS.md");
221
- const claudePath = path2.join(targetDir, "CLAUDE.md");
222
- const geminiPath = path2.join(targetDir, "GEMINI.md");
223
- if (!fs2.existsSync(agentsPath) && !fs2.existsSync(claudePath) && !fs2.existsSync(geminiPath)) {
224
- const docSrc = path2.join(templatesDir, "docs/AGENTS.template.md");
225
- if (fs2.existsSync(docSrc)) {
226
- fs2.copyFileSync(docSrc, agentsPath);
614
+ const agentsPath = path3.join(targetDir, "AGENTS.md");
615
+ const claudePath = path3.join(targetDir, "CLAUDE.md");
616
+ const geminiPath = path3.join(targetDir, "GEMINI.md");
617
+ if (!fs3.existsSync(agentsPath) && !fs3.existsSync(claudePath) && !fs3.existsSync(geminiPath) || options.force) {
618
+ const docSrc = path3.join(templatesDir, "docs/AGENTS.template.md");
619
+ if (fs3.existsSync(docSrc)) {
620
+ const templateContent = fs3.readFileSync(docSrc, "utf8");
621
+ const stack = options.detectedStack || detectTechStack(targetDir);
622
+ const renderedContent = renderAgentsTemplate(templateContent, stack);
623
+ fs3.writeFileSync(agentsPath, renderedContent, "utf8");
227
624
  result.docsInstalled.push("AGENTS.md");
228
625
  }
229
626
  }
@@ -231,21 +628,82 @@ function installFleet(targetDir, manifest, options = {}) {
231
628
  }
232
629
 
233
630
  // src/commands/init.ts
631
+ async function promptQuestion(query, defaultValue) {
632
+ const rl = readline.createInterface({
633
+ input: process.stdin,
634
+ output: process.stdout
635
+ });
636
+ return new Promise((resolve) => {
637
+ rl.question(`${query} [${defaultValue}]: `, (answer) => {
638
+ rl.close();
639
+ resolve(answer.trim() || defaultValue);
640
+ });
641
+ });
642
+ }
234
643
  async function runInit(options = {}) {
235
644
  const cwd = options.cwd || process.cwd();
236
645
  const preset = options.preset || "standard";
237
646
  console.log(pc.cyan(`
238
647
  \u2693 Initializing Jonah Fleet (preset: ${pc.bold(preset)}) in ${cwd}
239
648
  `));
649
+ let detected = detectTechStack(cwd);
650
+ console.log(pc.bold("\u{1F50D} Tech Stack Auto-Detection:"));
651
+ console.log(pc.cyan(` - Detected Stack: ${pc.bold(detected.name)}`));
652
+ console.log(pc.cyan(` - Language: ${detected.language}`));
653
+ if (detected.framework) {
654
+ console.log(pc.cyan(` - Framework: ${detected.framework}`));
655
+ }
656
+ console.log(pc.cyan(` - Package Manager: ${detected.packageManager}`));
657
+ if (detected.testFramework) {
658
+ console.log(pc.cyan(` - Testing: ${detected.testFramework}`));
659
+ }
660
+ if (detected.commands.test) {
661
+ console.log(pc.cyan(` - Test Command: ${detected.commands.test}`));
662
+ }
663
+ const isInteractive = options.interactive ?? (process.stdin.isTTY && !options.stack && !options.testCmd);
664
+ if (isInteractive && process.stdin.isTTY) {
665
+ console.log(pc.yellow("\n\u2699\uFE0F Configure project settings (press enter to accept defaults):"));
666
+ const stackName = await promptQuestion("Tech Stack Name", detected.name);
667
+ const pkgManager = await promptQuestion("Package Manager", detected.packageManager);
668
+ const testCmd = await promptQuestion("Test Command", detected.commands.test || "npm test");
669
+ const buildCmd = await promptQuestion("Build Command", detected.commands.build || "npm run build");
670
+ detected = {
671
+ ...detected,
672
+ name: stackName,
673
+ language: stackName,
674
+ framework: void 0,
675
+ packageManager: pkgManager,
676
+ commands: {
677
+ ...detected.commands,
678
+ test: testCmd,
679
+ build: buildCmd
680
+ }
681
+ };
682
+ }
683
+ if (options.stack) {
684
+ detected.name = options.stack;
685
+ detected.language = options.stack;
686
+ detected.framework = void 0;
687
+ }
688
+ if (options.packageManager) {
689
+ detected.packageManager = options.packageManager;
690
+ }
691
+ if (options.testCmd) {
692
+ detected.commands.test = options.testCmd;
693
+ }
694
+ if (options.buildCmd) {
695
+ detected.commands.build = options.buildCmd;
696
+ }
240
697
  let manifest = loadManifest(cwd);
241
698
  if (manifest && !options.force) {
242
- console.log(pc.yellow(`\u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
699
+ console.log(pc.yellow(`
700
+ \u26A0\uFE0F Found existing agents-manifest.json. Updating with preset '${preset}'...`));
243
701
  } else {
244
702
  manifest = createDefaultManifest(preset);
245
703
  }
246
704
  saveManifest(cwd, manifest);
247
705
  console.log(pc.green(`\u2713 Created/Updated agents-manifest.json`));
248
- const result = installFleet(cwd, manifest, { force: options.force });
706
+ const result = installFleet(cwd, manifest, { force: options.force, detectedStack: detected });
249
707
  console.log(pc.bold("\nInstalled components:"));
250
708
  if (result.promptsInstalled.length > 0) {
251
709
  console.log(pc.green(` \u{1F4C1} Prompts (.github/prompts/):`));
@@ -264,14 +722,20 @@ async function runInit(options = {}) {
264
722
  result.docsInstalled.forEach((d) => console.log(` - ${d}`));
265
723
  }
266
724
  console.log(pc.bold(pc.green("\n\u{1F389} Jonah Fleet initialization complete!\n")));
725
+ console.log(pc.cyan("Next steps for GitHub repository configuration:"));
726
+ console.log(" 1. In Settings \u2192 Actions \u2192 General \u2192 Workflow permissions:");
727
+ console.log(' Select "Read and write permissions" and check "Allow GitHub Actions to create and approve pull requests".');
728
+ console.log(" 2. In Settings \u2192 Actions \u2192 General \u2192 Fork pull request workflows:");
729
+ console.log(" Configure workflow approval settings to prevent automated runs from stalling awaiting approval.");
730
+ console.log(" 3. Customize project context, build, and test commands in AGENTS.md.\n");
267
731
  }
268
732
 
269
733
  // src/commands/sync.ts
270
734
  import pc2 from "picocolors";
271
735
 
272
736
  // src/lib/diff.ts
273
- import fs3 from "fs";
274
- import path3 from "path";
737
+ import fs4 from "fs";
738
+ import path4 from "path";
275
739
  function checkDrift(targetDir, manifest) {
276
740
  const templatesDir = getTemplatesDir();
277
741
  const report = {
@@ -281,43 +745,64 @@ function checkDrift(targetDir, manifest) {
281
745
  modifiedWorkflows: [],
282
746
  missingSkills: []
283
747
  };
284
- const targetPromptsDir = path3.join(targetDir, ".github/prompts");
285
- const targetWorkflowsDir = path3.join(targetDir, ".github/workflows");
286
- const targetSkillsDir = path3.join(targetDir, ".agents/skills");
748
+ const targetPromptsDir = path4.join(targetDir, ".github/prompts");
749
+ const targetWorkflowsDir = path4.join(targetDir, ".github/workflows");
750
+ const targetSkillsDir = path4.join(targetDir, ".agents/skills");
287
751
  const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
288
752
  for (const file of basePrompts) {
289
- const src = path3.join(templatesDir, "prompts", file);
290
- const dest = path3.join(targetPromptsDir, file);
291
- if (!fs3.existsSync(dest)) {
753
+ const src = path4.join(templatesDir, "prompts", file);
754
+ const dest = path4.join(targetPromptsDir, file);
755
+ if (!fs4.existsSync(dest)) {
292
756
  report.missingPrompts.push(file);
293
- } else if (fs3.readFileSync(src, "utf8") !== fs3.readFileSync(dest, "utf8")) {
757
+ } else if (fs4.readFileSync(src, "utf8") !== fs4.readFileSync(dest, "utf8")) {
294
758
  report.modifiedPrompts.push(file);
295
759
  }
296
760
  }
297
761
  for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
298
762
  if (!isEnabled) continue;
299
763
  const promptFile = `${routineName}.md`;
300
- const promptSrc = path3.join(templatesDir, "prompts", promptFile);
301
- const promptDest = path3.join(targetPromptsDir, promptFile);
302
- if (!fs3.existsSync(promptDest)) {
764
+ const promptSrc = path4.join(templatesDir, "prompts", promptFile);
765
+ const promptDest = path4.join(targetPromptsDir, promptFile);
766
+ if (!fs4.existsSync(promptDest)) {
303
767
  report.missingPrompts.push(promptFile);
304
- } else if (fs3.existsSync(promptSrc) && fs3.readFileSync(promptSrc, "utf8") !== fs3.readFileSync(promptDest, "utf8")) {
768
+ } else if (fs4.existsSync(promptSrc) && fs4.readFileSync(promptSrc, "utf8") !== fs4.readFileSync(promptDest, "utf8")) {
305
769
  report.modifiedPrompts.push(promptFile);
306
770
  }
307
771
  const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
308
772
  for (const workflowFile of workflows) {
309
- const wfSrc = path3.join(templatesDir, "workflows", workflowFile);
310
- const wfDest = path3.join(targetWorkflowsDir, workflowFile);
311
- if (!fs3.existsSync(wfDest)) {
773
+ const wfSrc = path4.join(templatesDir, "workflows", workflowFile);
774
+ const wfDest = path4.join(targetWorkflowsDir, workflowFile);
775
+ if (!fs4.existsSync(wfDest)) {
312
776
  report.missingWorkflows.push(workflowFile);
313
- } else if (fs3.existsSync(wfSrc) && fs3.readFileSync(wfSrc, "utf8") !== fs3.readFileSync(wfDest, "utf8")) {
314
- report.modifiedWorkflows.push(workflowFile);
777
+ } else if (fs4.existsSync(wfSrc)) {
778
+ const rawSrc = fs4.readFileSync(wfSrc, "utf8");
779
+ const destContent = fs4.readFileSync(wfDest, "utf8");
780
+ const schedule = resolveWorkflowSchedule(workflowFile, routineName, manifest, destContent);
781
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
782
+ if (expectedSrc !== destContent) {
783
+ report.modifiedWorkflows.push(workflowFile);
784
+ }
785
+ }
786
+ }
787
+ }
788
+ if (manifest.autoUpdate?.enabled) {
789
+ const syncWfSrc = path4.join(templatesDir, "workflows/sync-fleet.yml");
790
+ const syncWfDest = path4.join(targetWorkflowsDir, "sync-fleet.yml");
791
+ if (!fs4.existsSync(syncWfDest)) {
792
+ report.missingWorkflows.push("sync-fleet.yml");
793
+ } else if (fs4.existsSync(syncWfSrc)) {
794
+ const rawSrc = fs4.readFileSync(syncWfSrc, "utf8");
795
+ const destContent = fs4.readFileSync(syncWfDest, "utf8");
796
+ const schedule = resolveWorkflowSchedule("sync-fleet.yml", "sync-fleet", manifest, destContent);
797
+ const expectedSrc = applyWorkflowSchedule(rawSrc, schedule);
798
+ if (expectedSrc !== destContent) {
799
+ report.modifiedWorkflows.push("sync-fleet.yml");
315
800
  }
316
801
  }
317
802
  }
318
803
  for (const skill of manifest.skills) {
319
- const skillDestDir = path3.join(targetSkillsDir, skill);
320
- if (!fs3.existsSync(skillDestDir)) {
804
+ const skillDestDir = path4.join(targetSkillsDir, skill);
805
+ if (!fs4.existsSync(skillDestDir)) {
321
806
  report.missingSkills.push(skill);
322
807
  }
323
808
  }
@@ -363,75 +848,10 @@ Run 'jonah-fleet sync --force' to apply updates.
363
848
  }
364
849
 
365
850
  // src/commands/status.ts
851
+ import fs7 from "fs";
852
+ import path7 from "path";
366
853
  import pc5 from "picocolors";
367
854
 
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
855
  // src/lib/fleet-query.ts
436
856
  import { execFile } from "child_process";
437
857
  import fs5 from "fs";
@@ -461,15 +881,22 @@ function parseLogMetadata(content) {
461
881
  meta.timestamp = val;
462
882
  } else if (key === "result") {
463
883
  meta.result = val;
464
- } else if (key === "input tokens") {
884
+ } else if (key === "input tokens" || key === "input_tokens") {
465
885
  const num = parseInt(val.replace(/[^\d]/g, ""), 10);
466
886
  if (!isNaN(num)) meta.inputTokens = num;
467
- } else if (key === "output tokens") {
887
+ } else if (key === "output tokens" || key === "output_tokens") {
468
888
  const num = parseInt(val.replace(/[^\d]/g, ""), 10);
469
889
  if (!isNaN(num)) meta.outputTokens = num;
470
- } else if (key === "estimated cost") {
890
+ } else if (key === "estimated cost" || key === "estimated_cost") {
471
891
  const num = parseFloat(val.replace(/[^0-9.]/g, ""));
472
892
  if (!isNaN(num)) meta.estimatedCost = num;
893
+ } else if (key === "iterations used" || key === "iterations" || key === "iterations_used") {
894
+ const raw = val.split("/")[0].trim();
895
+ const num = parseInt(raw.replace(/[^\d]/g, ""), 10);
896
+ if (!isNaN(num)) meta.iterationsUsed = num;
897
+ } else if (key === "duration") {
898
+ const num = parseInt(val.replace(/[^\d]/g, ""), 10);
899
+ if (!isNaN(num)) meta.duration = num;
473
900
  }
474
901
  }
475
902
  if (meta.timestamp || meta.routine) {
@@ -480,26 +907,79 @@ function parseLogMetadata(content) {
480
907
  function computeTokenSpendFromLogs(logContents, now = Date.now()) {
481
908
  const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
482
909
  const cutoff = now - SEVEN_DAYS_MS;
483
- let inputTokens = 0;
484
- let outputTokens = 0;
485
- let cost = 0;
486
- let runs = 0;
910
+ let totalInputTokens = 0;
911
+ let totalOutputTokens = 0;
912
+ let totalCost = 0;
913
+ let totalRuns = 0;
914
+ const routineMap = {};
487
915
  for (const content of logContents) {
488
916
  const meta = parseLogMetadata(content);
489
917
  if (!meta || !meta.timestamp) continue;
490
918
  const logTime = new Date(meta.timestamp).getTime();
491
919
  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;
920
+ const routineName = meta.routine || "unknown";
921
+ const input = meta.inputTokens || 0;
922
+ const output = meta.outputTokens || 0;
923
+ const tokens = input + output;
924
+ const cost = meta.estimatedCost || 0;
925
+ totalRuns++;
926
+ totalInputTokens += input;
927
+ totalOutputTokens += output;
928
+ totalCost += cost;
929
+ if (!routineMap[routineName]) {
930
+ routineMap[routineName] = {
931
+ runCount: 0,
932
+ inputTokens: 0,
933
+ outputTokens: 0,
934
+ totalTokens: 0,
935
+ estimatedCost: 0,
936
+ maxTokensPerRun: 0,
937
+ iterationsSum: 0,
938
+ iterationsCount: 0
939
+ };
940
+ }
941
+ const acc = routineMap[routineName];
942
+ acc.runCount++;
943
+ acc.inputTokens += input;
944
+ acc.outputTokens += output;
945
+ acc.totalTokens += tokens;
946
+ acc.estimatedCost += cost;
947
+ if (tokens > acc.maxTokensPerRun) {
948
+ acc.maxTokensPerRun = tokens;
949
+ }
950
+ if (meta.iterationsUsed !== void 0) {
951
+ acc.iterationsSum += meta.iterationsUsed;
952
+ acc.iterationsCount++;
953
+ }
954
+ }
955
+ const sevenDayTotalTokens = totalInputTokens + totalOutputTokens;
956
+ const byRoutine = {};
957
+ for (const [routineName, acc] of Object.entries(routineMap)) {
958
+ const avgTokensPerRun = acc.runCount > 0 ? Math.round(acc.totalTokens / acc.runCount) : 0;
959
+ const fleetSharePercent = sevenDayTotalTokens > 0 ? Number((acc.totalTokens / sevenDayTotalTokens * 100).toFixed(2)) : 0;
960
+ const routineSpend = {
961
+ routine: routineName,
962
+ runCount: acc.runCount,
963
+ inputTokens: acc.inputTokens,
964
+ outputTokens: acc.outputTokens,
965
+ totalTokens: acc.totalTokens,
966
+ estimatedCost: Number(acc.estimatedCost.toFixed(2)),
967
+ avgTokensPerRun,
968
+ maxTokensPerRun: acc.maxTokensPerRun,
969
+ fleetSharePercent
970
+ };
971
+ if (acc.iterationsCount > 0) {
972
+ routineSpend.avgIterationsUsed = Number((acc.iterationsSum / acc.iterationsCount).toFixed(1));
973
+ }
974
+ byRoutine[routineName] = routineSpend;
496
975
  }
497
976
  return {
498
- sevenDayInputTokens: inputTokens,
499
- sevenDayOutputTokens: outputTokens,
500
- sevenDayTotalTokens: inputTokens + outputTokens,
501
- sevenDayEstimatedCost: cost,
502
- recentRunCount: runs
977
+ sevenDayInputTokens: totalInputTokens,
978
+ sevenDayOutputTokens: totalOutputTokens,
979
+ sevenDayTotalTokens,
980
+ sevenDayEstimatedCost: totalCost,
981
+ recentRunCount: totalRuns,
982
+ byRoutine
503
983
  };
504
984
  }
505
985
  function parseClaimFromIssue(issue, openPRs = [], now = Date.now()) {
@@ -548,7 +1028,8 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
548
1028
  sevenDayOutputTokens: 0,
549
1029
  sevenDayTotalTokens: 0,
550
1030
  sevenDayEstimatedCost: 0,
551
- recentRunCount: 0
1031
+ recentRunCount: 0,
1032
+ byRoutine: {}
552
1033
  },
553
1034
  staleWarnings: []
554
1035
  };
@@ -701,6 +1182,7 @@ function summarizeFleet(statuses) {
701
1182
  totalEstimatedCost7d: 0,
702
1183
  totalRuns7d: 0
703
1184
  };
1185
+ const fleetByRoutine = {};
704
1186
  for (const s of statuses) {
705
1187
  summary.activeClaimsCount += s.activeClaims.length;
706
1188
  summary.staleClaimsCount += s.activeClaims.filter((c) => c.isStale).length;
@@ -712,6 +1194,57 @@ function summarizeFleet(statuses) {
712
1194
  summary.totalTokens7d += s.tokenUsage.sevenDayTotalTokens;
713
1195
  summary.totalEstimatedCost7d += s.tokenUsage.sevenDayEstimatedCost;
714
1196
  summary.totalRuns7d += s.tokenUsage.recentRunCount;
1197
+ if (s.tokenUsage.byRoutine) {
1198
+ for (const [rName, rSpend] of Object.entries(s.tokenUsage.byRoutine)) {
1199
+ if (!fleetByRoutine[rName]) {
1200
+ fleetByRoutine[rName] = {
1201
+ runCount: 0,
1202
+ inputTokens: 0,
1203
+ outputTokens: 0,
1204
+ totalTokens: 0,
1205
+ estimatedCost: 0,
1206
+ maxTokensPerRun: 0,
1207
+ iterationsSum: 0,
1208
+ iterationsCount: 0
1209
+ };
1210
+ }
1211
+ const acc = fleetByRoutine[rName];
1212
+ acc.runCount += rSpend.runCount;
1213
+ acc.inputTokens += rSpend.inputTokens;
1214
+ acc.outputTokens += rSpend.outputTokens;
1215
+ acc.totalTokens += rSpend.totalTokens;
1216
+ acc.estimatedCost += rSpend.estimatedCost;
1217
+ if (rSpend.maxTokensPerRun > acc.maxTokensPerRun) {
1218
+ acc.maxTokensPerRun = rSpend.maxTokensPerRun;
1219
+ }
1220
+ if (rSpend.avgIterationsUsed !== void 0) {
1221
+ acc.iterationsSum += rSpend.avgIterationsUsed * rSpend.runCount;
1222
+ acc.iterationsCount += rSpend.runCount;
1223
+ }
1224
+ }
1225
+ }
1226
+ }
1227
+ if (Object.keys(fleetByRoutine).length > 0) {
1228
+ summary.byRoutine = {};
1229
+ for (const [rName, acc] of Object.entries(fleetByRoutine)) {
1230
+ const avgTokensPerRun = acc.runCount > 0 ? Math.round(acc.totalTokens / acc.runCount) : 0;
1231
+ const fleetSharePercent = summary.totalTokens7d > 0 ? Number((acc.totalTokens / summary.totalTokens7d * 100).toFixed(2)) : 0;
1232
+ const rSpend = {
1233
+ routine: rName,
1234
+ runCount: acc.runCount,
1235
+ inputTokens: acc.inputTokens,
1236
+ outputTokens: acc.outputTokens,
1237
+ totalTokens: acc.totalTokens,
1238
+ estimatedCost: Number(acc.estimatedCost.toFixed(2)),
1239
+ avgTokensPerRun,
1240
+ maxTokensPerRun: acc.maxTokensPerRun,
1241
+ fleetSharePercent
1242
+ };
1243
+ if (acc.iterationsCount > 0) {
1244
+ rSpend.avgIterationsUsed = Number((acc.iterationsSum / acc.iterationsCount).toFixed(1));
1245
+ }
1246
+ summary.byRoutine[rName] = rSpend;
1247
+ }
715
1248
  }
716
1249
  return summary;
717
1250
  }
@@ -785,6 +1318,16 @@ function renderFleetDashboard(statuses, options = {}) {
785
1318
  lines.push(
786
1319
  ` 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
1320
  );
1321
+ if (t.byRoutine && Object.keys(t.byRoutine).length > 0) {
1322
+ const routines = Object.values(t.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
1323
+ for (const r of routines) {
1324
+ const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
1325
+ const tokenDetails = options.tokens || options.detailed ? ` (in: ${formatTokens(r.inputTokens)}, out: ${formatTokens(r.outputTokens)})` : "";
1326
+ lines.push(
1327
+ ` \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)}`
1328
+ );
1329
+ }
1330
+ }
788
1331
  if (s.staleWarnings.length > 0) {
789
1332
  lines.push(pc3.bold(pc3.red(" \u26A0\uFE0F Warnings:")));
790
1333
  for (const w of s.staleWarnings) {
@@ -798,49 +1341,135 @@ function renderFleetDashboard(statuses, options = {}) {
798
1341
  lines.push(
799
1342
  ` Repositories: ${pc3.bold(summary.totalRepos.toString())} | Active Claims: ${pc3.bold(summary.activeClaimsCount.toString())} ` + (summary.staleClaimsCount > 0 ? pc3.red(`(${summary.staleClaimsCount} stale)`) : pc3.green("(0 stale)")) + ` | Open PRs: ${pc3.bold(summary.openPRsCount.toString())} ` + pc3.gray(`(${summary.draftPRsCount} draft, ${summary.readyPRsCount} ready)`)
800
1343
  );
1344
+ const CEILING_70_PERCENT = 875e4;
1345
+ const ceilingPct = summary.totalTokens7d / CEILING_70_PERCENT * 100;
1346
+ let budgetTag = pc3.green("[HEALTHY]");
1347
+ if (ceilingPct > 100) budgetTag = pc3.red(pc3.bold("[EXCEEDED]"));
1348
+ else if (ceilingPct >= 90) budgetTag = pc3.red(pc3.bold("[CRITICAL]"));
1349
+ else if (ceilingPct >= 70) budgetTag = pc3.yellow("[WARNING]");
801
1350
  lines.push(
802
1351
  ` 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
1352
  );
1353
+ lines.push(
1354
+ ` Weekly Budget: ${pc3.bold(formatTokens(summary.totalTokens7d))} / ${formatTokens(CEILING_70_PERCENT)} tokens (${ceilingPct.toFixed(1)}% of 70% ceiling) ${budgetTag}`
1355
+ );
1356
+ if (summary.byRoutine && Object.keys(summary.byRoutine).length > 0 && (options.tokens || options.detailed || statuses.length > 1)) {
1357
+ lines.push(pc3.bold("\n Fleet Spend by Routine:"));
1358
+ const fleetRoutines = Object.values(summary.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
1359
+ for (const r of fleetRoutines) {
1360
+ const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
1361
+ lines.push(
1362
+ ` \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)}`
1363
+ );
1364
+ }
1365
+ }
804
1366
  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
1367
  return lines.join("\n");
806
1368
  }
807
1369
 
808
1370
  // src/commands/monitor.ts
809
- async function runMonitor(options = {}) {
810
- const cwd = options.cwd || process.cwd();
811
- const executor = options.executor || defaultGhExecutor;
812
- if (options.add) {
813
- const updated = addGlobalRepository(options.add);
814
- console.log(pc4.green(`\u2713 Added ${pc4.bold(options.add)} to Jonah Fleet registry.`));
815
- console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
816
- `));
817
- return;
818
- }
819
- if (options.remove) {
820
- const updated = removeGlobalRepository(options.remove);
821
- console.log(pc4.yellow(`\u2713 Removed ${pc4.bold(options.remove)} from Jonah Fleet registry.`));
822
- console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
823
- `));
824
- return;
1371
+ import pc4 from "picocolors";
1372
+
1373
+ // src/lib/global-config.ts
1374
+ import fs6 from "fs";
1375
+ import path6 from "path";
1376
+ import os from "os";
1377
+ function getDefaultGlobalConfigPath() {
1378
+ const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path6.join(os.homedir(), ".jonah-fleet");
1379
+ return path6.join(baseDir, "config.json");
1380
+ }
1381
+ function loadGlobalConfig(customPath) {
1382
+ const filePath = customPath || getDefaultGlobalConfigPath();
1383
+ if (!fs6.existsSync(filePath)) {
1384
+ return { repositories: [] };
825
1385
  }
826
- let targetRepos = [];
827
- if (options.repos && options.repos.length > 0) {
828
- targetRepos = options.repos;
829
- } else {
830
- targetRepos = getFleetRepositories(cwd);
1386
+ try {
1387
+ const raw = fs6.readFileSync(filePath, "utf8");
1388
+ const parsed = JSON.parse(raw);
1389
+ return {
1390
+ repositories: Array.isArray(parsed.repositories) ? parsed.repositories : []
1391
+ };
1392
+ } catch {
1393
+ return { repositories: [] };
831
1394
  }
832
- if (targetRepos.length === 0) {
833
- try {
834
- const remoteRaw = await executor(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
835
- const currentRepo = remoteRaw.trim();
836
- if (currentRepo) {
837
- targetRepos = [currentRepo];
838
- }
839
- } catch {
840
- }
1395
+ }
1396
+ function saveGlobalConfig(config, customPath) {
1397
+ const filePath = customPath || getDefaultGlobalConfigPath();
1398
+ const dir = path6.dirname(filePath);
1399
+ if (!fs6.existsSync(dir)) {
1400
+ fs6.mkdirSync(dir, { recursive: true });
841
1401
  }
842
- if (targetRepos.length === 0) {
843
- console.log(pc4.yellow("\n\u26A0\uFE0F No fleet repositories registered."));
1402
+ fs6.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
1403
+ }
1404
+ function addGlobalRepository(repo, customPath) {
1405
+ const config = loadGlobalConfig(customPath);
1406
+ const normalized = repo.trim();
1407
+ if (!normalized) return config;
1408
+ if (!config.repositories.includes(normalized)) {
1409
+ config.repositories.push(normalized);
1410
+ saveGlobalConfig(config, customPath);
1411
+ }
1412
+ return config;
1413
+ }
1414
+ function removeGlobalRepository(repo, customPath) {
1415
+ const config = loadGlobalConfig(customPath);
1416
+ const normalized = repo.trim();
1417
+ config.repositories = config.repositories.filter((r) => r !== normalized);
1418
+ saveGlobalConfig(config, customPath);
1419
+ return config;
1420
+ }
1421
+ function getFleetRepositories(cwd, customGlobalConfigPath) {
1422
+ const targetDir = cwd || process.cwd();
1423
+ const manifest = loadManifest(targetDir);
1424
+ const manifestRepos = Array.isArray(manifest?.repositories) ? manifest.repositories : [];
1425
+ const globalConfig = loadGlobalConfig(customGlobalConfigPath);
1426
+ const globalRepos = globalConfig.repositories;
1427
+ const set = /* @__PURE__ */ new Set();
1428
+ for (const r of manifestRepos) {
1429
+ if (r && typeof r === "string" && r.trim()) set.add(r.trim());
1430
+ }
1431
+ for (const r of globalRepos) {
1432
+ if (r && typeof r === "string" && r.trim()) set.add(r.trim());
1433
+ }
1434
+ return Array.from(set);
1435
+ }
1436
+
1437
+ // src/commands/monitor.ts
1438
+ async function runMonitor(options = {}) {
1439
+ const cwd = options.cwd || process.cwd();
1440
+ const executor = options.executor || defaultGhExecutor;
1441
+ if (options.add) {
1442
+ const updated = addGlobalRepository(options.add);
1443
+ console.log(pc4.green(`\u2713 Added ${pc4.bold(options.add)} to Jonah Fleet registry.`));
1444
+ console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
1445
+ `));
1446
+ return;
1447
+ }
1448
+ if (options.remove) {
1449
+ const updated = removeGlobalRepository(options.remove);
1450
+ console.log(pc4.yellow(`\u2713 Removed ${pc4.bold(options.remove)} from Jonah Fleet registry.`));
1451
+ console.log(pc4.gray(` Current registered repositories: ${updated.repositories.join(", ") || "none"}
1452
+ `));
1453
+ return;
1454
+ }
1455
+ let targetRepos = [];
1456
+ if (options.repos && options.repos.length > 0) {
1457
+ targetRepos = options.repos;
1458
+ } else {
1459
+ targetRepos = getFleetRepositories(cwd);
1460
+ }
1461
+ if (targetRepos.length === 0) {
1462
+ try {
1463
+ const remoteRaw = await executor(["repo", "view", "--json", "nameWithOwner", "-q", ".nameWithOwner"]);
1464
+ const currentRepo = remoteRaw.trim();
1465
+ if (currentRepo) {
1466
+ targetRepos = [currentRepo];
1467
+ }
1468
+ } catch {
1469
+ }
1470
+ }
1471
+ if (targetRepos.length === 0) {
1472
+ console.log(pc4.yellow("\n\u26A0\uFE0F No fleet repositories registered."));
844
1473
  console.log(pc4.cyan("Add repositories to monitor with:"));
845
1474
  console.log(pc4.gray(" jonah-fleet monitor --add owner/repo"));
846
1475
  console.log(pc4.gray("Or specify repositories directly:"));
@@ -854,7 +1483,11 @@ async function runMonitor(options = {}) {
854
1483
  if (options.watch && !options.json) {
855
1484
  console.clear();
856
1485
  }
857
- const output = renderFleetDashboard(statuses, { json: options.json });
1486
+ const output = renderFleetDashboard(statuses, {
1487
+ json: options.json,
1488
+ tokens: options.tokens,
1489
+ detailed: options.detailed
1490
+ });
858
1491
  console.log(output);
859
1492
  };
860
1493
  await pollAndRender();
@@ -877,7 +1510,7 @@ async function runMonitor(options = {}) {
877
1510
  async function runStatus(options = {}) {
878
1511
  const cwd = options.cwd || process.cwd();
879
1512
  if (options.fleet) {
880
- await runMonitor({ cwd, json: options.json });
1513
+ await runMonitor({ cwd, json: options.json, tokens: options.tokens, detailed: options.detailed });
881
1514
  return;
882
1515
  }
883
1516
  const manifest = loadManifest(cwd);
@@ -894,6 +1527,32 @@ async function runStatus(options = {}) {
894
1527
  }
895
1528
  const drift = checkDrift(cwd, manifest);
896
1529
  const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1530
+ const logsDir = path7.join(cwd, ".github/prompts/logs");
1531
+ let tokenUsage = void 0;
1532
+ if (fs7.existsSync(logsDir)) {
1533
+ const logContents = [];
1534
+ const collectLogs = (dir) => {
1535
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1536
+ for (const entry of entries) {
1537
+ const fullPath = path7.join(dir, entry.name);
1538
+ if (entry.isDirectory()) {
1539
+ collectLogs(fullPath);
1540
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1541
+ try {
1542
+ logContents.push(fs7.readFileSync(fullPath, "utf8"));
1543
+ } catch {
1544
+ }
1545
+ }
1546
+ }
1547
+ };
1548
+ try {
1549
+ collectLogs(logsDir);
1550
+ } catch {
1551
+ }
1552
+ if (logContents.length > 0) {
1553
+ tokenUsage = computeTokenSpendFromLogs(logContents);
1554
+ }
1555
+ }
897
1556
  if (options.json) {
898
1557
  console.log(
899
1558
  JSON.stringify(
@@ -906,6 +1565,7 @@ async function runStatus(options = {}) {
906
1565
  routines: manifest.routines,
907
1566
  skills: manifest.skills,
908
1567
  repositories: manifest.repositories || [],
1568
+ tokenUsage,
909
1569
  drift: {
910
1570
  hasDrift,
911
1571
  ...drift
@@ -937,6 +1597,22 @@ async function runStatus(options = {}) {
937
1597
  console.log(` - ${pc5.cyan(repo)}`);
938
1598
  }
939
1599
  }
1600
+ if (tokenUsage && tokenUsage.recentRunCount > 0) {
1601
+ console.log(pc5.bold("\n \u{1F4C8} 7-Day Token Spend:"));
1602
+ console.log(
1603
+ ` 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)))}`
1604
+ );
1605
+ if (tokenUsage.byRoutine && Object.keys(tokenUsage.byRoutine).length > 0) {
1606
+ const routines = Object.values(tokenUsage.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
1607
+ for (const r of routines) {
1608
+ const iterStr = r.avgIterationsUsed !== void 0 ? `, avg ${r.avgIterationsUsed} iters` : "";
1609
+ const tokenDetails = options.tokens || options.detailed ? ` (in: ${formatTokens(r.inputTokens)}, out: ${formatTokens(r.outputTokens)})` : "";
1610
+ console.log(
1611
+ ` \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)}`
1612
+ );
1613
+ }
1614
+ }
1615
+ }
940
1616
  console.log(pc5.bold("\n Drift / Health:"));
941
1617
  if (!hasDrift) {
942
1618
  console.log(pc5.green(" \u2713 All prompts, workflows, and skills are healthy and match fleet templates.\n"));
@@ -953,52 +1629,1151 @@ async function runStatus(options = {}) {
953
1629
  // src/commands/contribute.ts
954
1630
  import { execSync } from "child_process";
955
1631
  import pc6 from "picocolors";
1632
+ function prepareContributionPayload(options = {}) {
1633
+ const repo = options.repo || "juliendurandeu/jonah-fleet";
1634
+ const title = options.title || "fix(prompts): improve orchestrator routine handling";
1635
+ const body = options.body || "Proposed prompt optimization discovered during autonomous execution runs.";
1636
+ const branchName = `contrib/optimize-${Date.now()}`;
1637
+ const prCommand = `gh pr create --repo ${repo} --title "${title}" --body "${body}"`;
1638
+ return {
1639
+ repo,
1640
+ branchName,
1641
+ title,
1642
+ body,
1643
+ prompt: options.prompt,
1644
+ prCommand
1645
+ };
1646
+ }
956
1647
  async function runContribute(options = {}) {
957
1648
  console.log(pc6.cyan(`
958
1649
  \u{1F680} Jonah Fleet Upstream Contribution Bridge
959
1650
  `));
960
- const title = options.title || "fix(prompts): improve orchestrator routine handling";
961
- const body = options.body || "Proposed prompt optimization discovered during autonomous execution runs.";
962
- console.log(`Preparing upstream contribution PR against ${pc6.bold("juliendurandeu/jonah-fleet")}...`);
963
- console.log(`Title: ${pc6.green(title)}`);
964
- console.log(`Body: ${pc6.gray(body)}
1651
+ const payload = prepareContributionPayload(options);
1652
+ console.log(`Preparing upstream contribution PR against ${pc6.bold(payload.repo)}...`);
1653
+ console.log(`Title: ${pc6.green(payload.title)}`);
1654
+ console.log(`Body: ${pc6.gray(payload.body)}
965
1655
  `);
1656
+ if (options.dryRun) {
1657
+ console.log(pc6.yellow(`[DRY-RUN] Would create branch: ${payload.branchName}`));
1658
+ console.log(pc6.yellow(`[DRY-RUN] Would execute: ${payload.prCommand}
1659
+ `));
1660
+ return {
1661
+ success: true,
1662
+ dryRun: true,
1663
+ branchName: payload.branchName,
1664
+ title: payload.title
1665
+ };
1666
+ }
966
1667
  try {
967
- const branchName = `contrib/optimize-${Date.now()}`;
968
- console.log(`Creating branch ${pc6.cyan(branchName)}...`);
969
- try {
970
- execSync("gh auth status", { stdio: "pipe" });
971
- } catch {
972
- console.error(pc6.red("\u274C GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first."));
973
- process.exit(1);
974
- }
975
- console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to juliendurandeu/jonah-fleet.`));
976
- console.log(pc6.cyan(`Command executed by optimizer routine or operator:
1668
+ console.log(`Creating branch ${pc6.cyan(payload.branchName)}...`);
1669
+ if (options.executor) {
1670
+ await options.executor(["auth", "status"]);
1671
+ const prUrl = await options.executor([
1672
+ "pr",
1673
+ "create",
1674
+ "--repo",
1675
+ payload.repo,
1676
+ "--title",
1677
+ payload.title,
1678
+ "--body",
1679
+ payload.body
1680
+ ]);
1681
+ console.log(pc6.green(`\u2713 Submitted upstream contribution: ${prUrl}`));
1682
+ return {
1683
+ success: true,
1684
+ branchName: payload.branchName,
1685
+ title: payload.title,
1686
+ prUrl: typeof prUrl === "string" && prUrl.trim() ? prUrl.trim() : void 0
1687
+ };
1688
+ } else {
1689
+ try {
1690
+ execSync("gh auth status", { stdio: "pipe" });
1691
+ } catch {
1692
+ const errorMsg = "GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first.";
1693
+ console.error(pc6.red(`\u274C ${errorMsg}`));
1694
+ if (options.throwOnError) throw new Error(errorMsg);
1695
+ return { success: false, error: errorMsg };
1696
+ }
1697
+ console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to ${payload.repo}.`));
1698
+ console.log(pc6.cyan(`Command executed by optimizer routine or operator:
977
1699
  `));
978
- console.log(` gh pr create --repo juliendurandeu/jonah-fleet --title "${title}" --body "${body}"
1700
+ console.log(` ${payload.prCommand}
979
1701
  `);
1702
+ return {
1703
+ success: true,
1704
+ branchName: payload.branchName,
1705
+ title: payload.title
1706
+ };
1707
+ }
980
1708
  } catch (err) {
981
1709
  console.error(pc6.red(`\u274C Error during contribution preparation: ${err.message}`));
1710
+ if (options.throwOnError) {
1711
+ throw err;
1712
+ }
1713
+ return {
1714
+ success: false,
1715
+ error: err.message
1716
+ };
1717
+ }
1718
+ }
1719
+
1720
+ // src/commands/telemetry.ts
1721
+ import fs9 from "fs";
1722
+ import path9 from "path";
1723
+ import pc8 from "picocolors";
1724
+
1725
+ // src/lib/telemetry.ts
1726
+ import fs8 from "fs";
1727
+ import path8 from "path";
1728
+ import pc7 from "picocolors";
1729
+ var GLOBAL_WEEKLY_TOKEN_BUDGET = 875e4;
1730
+ function parseLogToTelemetry(content, options = {}) {
1731
+ if (!content || typeof content !== "string") return null;
1732
+ const lines = content.split("\n");
1733
+ const metadata = {};
1734
+ for (const line of lines) {
1735
+ const match = line.match(/^\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|/);
1736
+ if (!match) continue;
1737
+ const key = match[1].trim().toLowerCase();
1738
+ const val = match[2].trim().replace(/`/g, "");
1739
+ metadata[key] = val;
1740
+ }
1741
+ if (!metadata["timestamp"] && !metadata["routine"]) {
1742
+ return null;
1743
+ }
1744
+ const routine = metadata["routine"] || "unknown";
1745
+ const timestamp = metadata["timestamp"] || (/* @__PURE__ */ new Date()).toISOString();
1746
+ const result = metadata["result"] || "UNKNOWN";
1747
+ const errorReasonRaw = metadata["error reason"];
1748
+ const errorReason = errorReasonRaw && errorReasonRaw.toUpperCase() !== "N/A" ? errorReasonRaw : void 0;
1749
+ let failureCategory = metadata["failure category"];
1750
+ if (!failureCategory && result === "FAILURE" && errorReason) {
1751
+ const lower = errorReason.toLowerCase();
1752
+ if (lower.includes("token") || lower.includes("ceiling")) {
1753
+ failureCategory = "token_limit";
1754
+ } else if (lower.includes("build") || lower.includes("type-check")) {
1755
+ failureCategory = "build_error";
1756
+ } else if (lower.includes("infeasible") || lower.includes("blocker")) {
1757
+ failureCategory = "infeasible";
1758
+ } else if (lower.includes("conflict")) {
1759
+ failureCategory = "merge_conflict";
1760
+ }
1761
+ }
1762
+ const inputTokens = parseInt((metadata["input tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
1763
+ const outputTokens = parseInt((metadata["output tokens"] || "0").replace(/[^\d]/g, ""), 10) || 0;
1764
+ const totalTokens = inputTokens + outputTokens;
1765
+ const estimatedCost = parseFloat((metadata["estimated cost"] || "0").replace(/[^0-9.]/g, "")) || 0;
1766
+ let durationSeconds;
1767
+ if (metadata["duration"]) {
1768
+ const durNum = parseInt(metadata["duration"].replace(/[^\d]/g, ""), 10);
1769
+ if (!isNaN(durNum)) durationSeconds = durNum;
1770
+ }
1771
+ let iterationsUsed;
1772
+ let maxIterations;
1773
+ if (metadata["iterations used"]) {
1774
+ const iterMatch = metadata["iterations used"].match(/(\d+)\s*\/\s*(\d+)/);
1775
+ if (iterMatch) {
1776
+ iterationsUsed = parseInt(iterMatch[1], 10);
1777
+ maxIterations = parseInt(iterMatch[2], 10);
1778
+ } else {
1779
+ const singleNum = parseInt(metadata["iterations used"].replace(/[^\d]/g, ""), 10);
1780
+ if (!isNaN(singleNum)) iterationsUsed = singleNum;
1781
+ }
1782
+ }
1783
+ const promptSha = metadata["prompt sha"];
1784
+ return {
1785
+ schemaVersion: "1.0.0",
1786
+ routine,
1787
+ timestamp,
1788
+ repository: options.repository || "local",
1789
+ runId: options.runId,
1790
+ runNumber: options.runNumber,
1791
+ result,
1792
+ errorReason,
1793
+ failureCategory,
1794
+ inputTokens,
1795
+ outputTokens,
1796
+ totalTokens,
1797
+ estimatedCost,
1798
+ durationSeconds,
1799
+ iterationsUsed,
1800
+ maxIterations,
1801
+ promptSha
1802
+ };
1803
+ }
1804
+ function checkWeeklyBudgetLimit(usedTokens, ceilingTokens = GLOBAL_WEEKLY_TOKEN_BUDGET) {
1805
+ const remainingTokens = Math.max(0, ceilingTokens - usedTokens);
1806
+ const utilizationPercentage = ceilingTokens > 0 ? usedTokens / ceilingTokens * 100 : 0;
1807
+ const dailyBurnRate = usedTokens / 7;
1808
+ const projectedExhaustionDays = dailyBurnRate > 0 ? remainingTokens / dailyBurnRate : 999;
1809
+ let status = "HEALTHY";
1810
+ if (usedTokens > ceilingTokens) {
1811
+ status = "EXCEEDED";
1812
+ } else if (utilizationPercentage >= 90) {
1813
+ status = "CRITICAL";
1814
+ } else if (utilizationPercentage >= 70) {
1815
+ status = "WARNING";
1816
+ }
1817
+ return {
1818
+ weeklyCeilingTokens: ceilingTokens,
1819
+ usedTokens,
1820
+ remainingTokens,
1821
+ utilizationPercentage,
1822
+ status,
1823
+ dailyBurnRate,
1824
+ projectedExhaustionDays
1825
+ };
1826
+ }
1827
+ function aggregateFleetTelemetry(summaries, options = {}) {
1828
+ const budgetCeiling = options.weeklyTokenBudget || GLOBAL_WEEKLY_TOKEN_BUDGET;
1829
+ let totalInputTokens = 0;
1830
+ let totalOutputTokens = 0;
1831
+ let totalCost = 0;
1832
+ let successCount = 0;
1833
+ let failureCount = 0;
1834
+ let bouncedCount = 0;
1835
+ const byRoutine = {};
1836
+ const byRepository = {};
1837
+ const failureCategories = {};
1838
+ for (const s of summaries) {
1839
+ totalInputTokens += s.inputTokens;
1840
+ totalOutputTokens += s.outputTokens;
1841
+ totalCost += s.estimatedCost;
1842
+ if (s.result === "SUCCESS") successCount++;
1843
+ else if (s.result === "FAILURE") failureCount++;
1844
+ else if (s.result === "BOUNCED_TO_DRAFT") bouncedCount++;
1845
+ if (s.failureCategory) {
1846
+ failureCategories[s.failureCategory] = (failureCategories[s.failureCategory] || 0) + 1;
1847
+ }
1848
+ if (!byRoutine[s.routine]) {
1849
+ byRoutine[s.routine] = {
1850
+ routine: s.routine,
1851
+ runCount: 0,
1852
+ totalInputTokens: 0,
1853
+ totalOutputTokens: 0,
1854
+ totalTokens: 0,
1855
+ totalEstimatedCost: 0,
1856
+ successCount: 0,
1857
+ failureCount: 0,
1858
+ bouncedCount: 0,
1859
+ avgDurationSeconds: 0,
1860
+ avgIterationsUsed: 0
1861
+ };
1862
+ }
1863
+ const r = byRoutine[s.routine];
1864
+ r.runCount++;
1865
+ r.totalInputTokens += s.inputTokens;
1866
+ r.totalOutputTokens += s.outputTokens;
1867
+ r.totalTokens += s.totalTokens;
1868
+ r.totalEstimatedCost += s.estimatedCost;
1869
+ if (s.result === "SUCCESS") r.successCount++;
1870
+ else if (s.result === "FAILURE") r.failureCount++;
1871
+ else if (s.result === "BOUNCED_TO_DRAFT") r.bouncedCount++;
1872
+ if (s.durationSeconds) {
1873
+ r.avgDurationSeconds = (r.avgDurationSeconds * (r.runCount - 1) + s.durationSeconds) / r.runCount;
1874
+ }
1875
+ if (s.iterationsUsed) {
1876
+ r.avgIterationsUsed = (r.avgIterationsUsed * (r.runCount - 1) + s.iterationsUsed) / r.runCount;
1877
+ }
1878
+ if (!byRepository[s.repository]) {
1879
+ byRepository[s.repository] = {
1880
+ repository: s.repository,
1881
+ runCount: 0,
1882
+ totalTokens: 0,
1883
+ totalEstimatedCost: 0,
1884
+ successCount: 0,
1885
+ failureCount: 0
1886
+ };
1887
+ }
1888
+ const repoObj = byRepository[s.repository];
1889
+ repoObj.runCount++;
1890
+ repoObj.totalTokens += s.totalTokens;
1891
+ repoObj.totalEstimatedCost += s.estimatedCost;
1892
+ if (s.result === "SUCCESS") repoObj.successCount++;
1893
+ else if (s.result === "FAILURE") repoObj.failureCount++;
1894
+ }
1895
+ const totalTokens = totalInputTokens + totalOutputTokens;
1896
+ const budget = checkWeeklyBudgetLimit(totalTokens, budgetCeiling);
1897
+ return {
1898
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1899
+ totalRuns: summaries.length,
1900
+ successCount,
1901
+ failureCount,
1902
+ bouncedCount,
1903
+ totalInputTokens,
1904
+ totalOutputTokens,
1905
+ totalTokens,
1906
+ totalEstimatedCost: totalCost,
1907
+ budget,
1908
+ byRoutine,
1909
+ byRepository,
1910
+ failureCategories,
1911
+ events: summaries
1912
+ };
1913
+ }
1914
+ async function emitTelemetry(summary, endpoint, customFetch = globalThis.fetch) {
1915
+ if (!endpoint || !endpoint.trim()) {
1916
+ return { success: true };
1917
+ }
1918
+ try {
1919
+ const res = await customFetch(endpoint.trim(), {
1920
+ method: "POST",
1921
+ headers: {
1922
+ "Content-Type": "application/json",
1923
+ "User-Agent": "jonah-fleet-telemetry/1.0"
1924
+ },
1925
+ body: JSON.stringify(summary)
1926
+ });
1927
+ if (!res.ok) {
1928
+ return { success: false, error: `HTTP ${res.status}: ${res.statusText}` };
1929
+ }
1930
+ return { success: true };
1931
+ } catch (err) {
1932
+ return { success: false, error: err.message || "Unknown network error" };
1933
+ }
1934
+ }
1935
+ function collectLocalTelemetryLogs(dir, repositoryName = "local") {
1936
+ const summaries = [];
1937
+ const logsDir = path8.join(dir, ".github/prompts/logs");
1938
+ if (!fs8.existsSync(logsDir)) return summaries;
1939
+ const traverse = (currentDir) => {
1940
+ const entries = fs8.readdirSync(currentDir, { withFileTypes: true });
1941
+ for (const entry of entries) {
1942
+ const fullPath = path8.join(currentDir, entry.name);
1943
+ if (entry.isDirectory()) {
1944
+ traverse(fullPath);
1945
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
1946
+ try {
1947
+ const content = fs8.readFileSync(fullPath, "utf8");
1948
+ const summary = parseLogToTelemetry(content, { repository: repositoryName });
1949
+ if (summary) summaries.push(summary);
1950
+ } catch {
1951
+ }
1952
+ }
1953
+ }
1954
+ };
1955
+ traverse(logsDir);
1956
+ return summaries;
1957
+ }
1958
+ async function collectRepoTelemetry(repoIdentifier, executor = defaultGhExecutor, options = {}) {
1959
+ const summaries = [];
1960
+ if (fs8.existsSync(repoIdentifier) && fs8.statSync(repoIdentifier).isDirectory()) {
1961
+ return collectLocalTelemetryLogs(repoIdentifier, repoIdentifier);
1962
+ }
1963
+ try {
1964
+ const treeRaw = await executor([
1965
+ "api",
1966
+ `repos/${repoIdentifier}/git/trees/HEAD?recursive=1`
1967
+ ]);
1968
+ const tree = JSON.parse(treeRaw);
1969
+ if (Array.isArray(tree.tree)) {
1970
+ const logFiles = tree.tree.filter((node) => node.path && node.path.startsWith(".github/prompts/logs/") && node.path.endsWith(".md")).slice(-(options.maxLogs || 25));
1971
+ for (const file of logFiles) {
1972
+ try {
1973
+ const fileRaw = await executor(["api", `repos/${repoIdentifier}/contents/${file.path}`]);
1974
+ const parsed = JSON.parse(fileRaw);
1975
+ if (parsed.content) {
1976
+ const content = Buffer.from(parsed.content, "base64").toString("utf8");
1977
+ const summary = parseLogToTelemetry(content, { repository: repoIdentifier });
1978
+ if (summary) summaries.push(summary);
1979
+ }
1980
+ } catch {
1981
+ }
1982
+ }
1983
+ }
1984
+ } catch {
1985
+ }
1986
+ return summaries;
1987
+ }
1988
+ function formatTokens2(num) {
1989
+ if (num >= 1e6) {
1990
+ return `${(num / 1e6).toFixed(2)}M`;
1991
+ }
1992
+ if (num >= 1e3) {
1993
+ return `${(num / 1e3).toFixed(1)}k`;
1994
+ }
1995
+ return num.toString();
1996
+ }
1997
+ function renderProgressBar(percentage, width = 25) {
1998
+ const clamped = Math.max(0, Math.min(100, percentage));
1999
+ const filledCount = Math.round(clamped / 100 * width);
2000
+ const emptyCount = width - filledCount;
2001
+ const filledBar = "\u2588".repeat(filledCount);
2002
+ const emptyBar = "\u2591".repeat(emptyCount);
2003
+ if (clamped >= 90) return pc7.red(filledBar) + pc7.gray(emptyBar);
2004
+ if (clamped >= 70) return pc7.yellow(filledBar) + pc7.gray(emptyBar);
2005
+ return pc7.green(filledBar) + pc7.gray(emptyBar);
2006
+ }
2007
+ function renderTelemetryDashboard(telemetry, options = {}) {
2008
+ if (options.json) {
2009
+ return JSON.stringify(telemetry, null, 2);
2010
+ }
2011
+ const lines = [];
2012
+ lines.push(pc7.bold(pc7.cyan("\n\u{1F6F0}\uFE0F Jonah Fleet Telemetry Hub & Token Economics\n")));
2013
+ const b = telemetry.budget;
2014
+ let statusBadge = pc7.green("[HEALTHY]");
2015
+ if (b.status === "WARNING") statusBadge = pc7.yellow("[WARNING]");
2016
+ else if (b.status === "CRITICAL") statusBadge = pc7.red(pc7.bold("[CRITICAL]"));
2017
+ else if (b.status === "EXCEEDED") statusBadge = pc7.red(pc7.bold("[BUDGET EXCEEDED]"));
2018
+ lines.push(pc7.bold("\u{1F4C8} Global Weekly Token Budget Ceiling (~70% Fleet Limit):"));
2019
+ lines.push(
2020
+ ` ${renderProgressBar(b.utilizationPercentage, 30)} ${pc7.bold(`${b.utilizationPercentage.toFixed(1)}%`)} ${statusBadge}`
2021
+ );
2022
+ lines.push(
2023
+ ` Used: ${pc7.bold(formatTokens2(b.usedTokens))} / ${formatTokens2(b.weeklyCeilingTokens)} tokens | Remaining: ${pc7.green(formatTokens2(b.remainingTokens))} | Daily Burn: ${formatTokens2(b.dailyBurnRate)}/day`
2024
+ );
2025
+ lines.push("\n" + pc7.bold("\u{1F310} Fleet Aggregate Spend:"));
2026
+ lines.push(
2027
+ ` Total Runs: ${pc7.bold(telemetry.totalRuns.toString())} (${pc7.green(telemetry.successCount + " success")}, ${pc7.red(telemetry.failureCount + " failed")}, ${pc7.yellow(telemetry.bouncedCount + " bounced")})`
2028
+ );
2029
+ lines.push(
2030
+ ` Total Tokens: ${pc7.bold(formatTokens2(telemetry.totalTokens))} ` + pc7.gray(`(in: ${formatTokens2(telemetry.totalInputTokens)}, out: ${formatTokens2(telemetry.totalOutputTokens)})`) + ` | Est. Cost: ${pc7.bold(pc7.green(`$${telemetry.totalEstimatedCost.toFixed(2)}`))}`
2031
+ );
2032
+ lines.push("\n" + pc7.bold("\u{1F916} Spend by Agent Routine:"));
2033
+ for (const [routineName, r] of Object.entries(telemetry.byRoutine)) {
2034
+ const costStr = pc7.green(`$${r.totalEstimatedCost.toFixed(2)}`);
2035
+ lines.push(
2036
+ ` \u2022 ${pc7.cyan(routineName.padEnd(30))} Runs: ${pc7.bold(r.runCount.toString().padStart(2))} | Tokens: ${pc7.bold(formatTokens2(r.totalTokens).padStart(7))} | Cost: ${costStr.padStart(6)} | Avg Iter: ${r.avgIterationsUsed.toFixed(1)}`
2037
+ );
2038
+ }
2039
+ if (Object.keys(telemetry.byRepository).length > 0) {
2040
+ lines.push("\n" + pc7.bold("\u{1F4E6} Spend by Repository:"));
2041
+ for (const [repoName, repoObj] of Object.entries(telemetry.byRepository)) {
2042
+ lines.push(
2043
+ ` \u2022 ${pc7.bold(repoName)}: ${formatTokens2(repoObj.totalTokens)} tokens across ${repoObj.runCount} runs ($${repoObj.totalEstimatedCost.toFixed(2)})`
2044
+ );
2045
+ }
2046
+ }
2047
+ const failKeys = Object.keys(telemetry.failureCategories);
2048
+ if (failKeys.length > 0) {
2049
+ lines.push("\n" + pc7.bold(pc7.red("\u26A0\uFE0F Failure Categories Breakdown:")));
2050
+ for (const cat of failKeys) {
2051
+ lines.push(` - ${cat}: ${telemetry.failureCategories[cat]} occurrences`);
2052
+ }
2053
+ }
2054
+ lines.push("\n" + pc7.gray("\u2500".repeat(65)) + "\n");
2055
+ return lines.join("\n");
2056
+ }
2057
+
2058
+ // src/commands/telemetry.ts
2059
+ async function runTelemetry(options = {}) {
2060
+ const cwd = options.cwd || process.cwd();
2061
+ const executor = options.executor || defaultGhExecutor;
2062
+ const manifest = loadManifest(cwd);
2063
+ const weeklyBudget = typeof options.budget === "number" ? options.budget : typeof options.budget === "string" ? parseInt(options.budget, 10) : manifest?.telemetry?.weeklyTokenBudget || GLOBAL_WEEKLY_TOKEN_BUDGET;
2064
+ if (options.action === "emit" || options.log) {
2065
+ let logPath = options.log;
2066
+ if (!logPath) {
2067
+ const logsDir = path9.join(cwd, ".github/prompts/logs");
2068
+ if (fs9.existsSync(logsDir)) {
2069
+ let latestFile = null;
2070
+ let latestMtime = 0;
2071
+ const findLogs = (dir) => {
2072
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
2073
+ for (const entry of entries) {
2074
+ const p = path9.join(dir, entry.name);
2075
+ if (entry.isDirectory()) findLogs(p);
2076
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
2077
+ const stat = fs9.statSync(p);
2078
+ if (stat.mtimeMs > latestMtime) {
2079
+ latestMtime = stat.mtimeMs;
2080
+ latestFile = p;
2081
+ }
2082
+ }
2083
+ }
2084
+ };
2085
+ findLogs(logsDir);
2086
+ logPath = latestFile || void 0;
2087
+ }
2088
+ }
2089
+ if (!logPath || !fs9.existsSync(logPath)) {
2090
+ if (options.json) {
2091
+ console.log(JSON.stringify({ error: "No log file found to emit", path: logPath }, null, 2));
2092
+ } else {
2093
+ console.log(pc8.yellow(`
2094
+ \u26A0\uFE0F No log file found to emit: ${logPath || "none specified"}
2095
+ `));
2096
+ }
2097
+ return;
2098
+ }
2099
+ const content = fs9.readFileSync(logPath, "utf8");
2100
+ const repoName = options.repo || process.env.GITHUB_REPOSITORY || (manifest ? "local" : "unknown");
2101
+ const runId = process.env.GITHUB_RUN_ID;
2102
+ const runNumber = process.env.GITHUB_RUN_NUMBER ? parseInt(process.env.GITHUB_RUN_NUMBER, 10) : void 0;
2103
+ const summary = parseLogToTelemetry(content, {
2104
+ repository: repoName,
2105
+ runId,
2106
+ runNumber
2107
+ });
2108
+ if (!summary) {
2109
+ if (options.json) {
2110
+ console.log(JSON.stringify({ error: "Failed to parse metadata from log file", path: logPath }, null, 2));
2111
+ } else {
2112
+ console.log(pc8.red(`
2113
+ \u274C Failed to parse valid telemetry metadata from: ${logPath}
2114
+ `));
2115
+ }
2116
+ return;
2117
+ }
2118
+ const endpoint = options.endpoint || process.env.JONAH_FLEET_TELEMETRY_ENDPOINT || process.env.TELEMETRY_ENDPOINT || manifest?.telemetry?.endpoint;
2119
+ const emitResult = await emitTelemetry(summary, endpoint);
2120
+ if (options.json) {
2121
+ console.log(JSON.stringify({ success: emitResult.success, error: emitResult.error, summary }, null, 2));
2122
+ } else {
2123
+ if (emitResult.success) {
2124
+ if (endpoint) {
2125
+ console.log(pc8.green(`\u2713 Successfully emitted telemetry summary for ${pc8.bold(summary.routine)} to ${endpoint}`));
2126
+ } else {
2127
+ console.log(pc8.gray(`\u2139 Telemetry summary parsed for ${summary.routine} (emission skipped: no endpoint configured)`));
2128
+ }
2129
+ } else {
2130
+ console.log(pc8.red(`\u274C Failed to emit telemetry to ${endpoint}: ${emitResult.error}`));
2131
+ }
2132
+ }
2133
+ return;
2134
+ }
2135
+ let targetRepos = [];
2136
+ if (options.repos && options.repos.length > 0) {
2137
+ targetRepos = options.repos;
2138
+ } else {
2139
+ targetRepos = getFleetRepositories(cwd);
2140
+ }
2141
+ const allSummaries = [];
2142
+ const localSummaries = collectLocalTelemetryLogs(cwd, process.env.GITHUB_REPOSITORY || "local");
2143
+ allSummaries.push(...localSummaries);
2144
+ for (const repo of targetRepos) {
2145
+ if (repo === "local" || repo === cwd) continue;
2146
+ try {
2147
+ const repoSummaries = await collectRepoTelemetry(repo, executor);
2148
+ allSummaries.push(...repoSummaries);
2149
+ } catch {
2150
+ }
2151
+ }
2152
+ const aggregated = aggregateFleetTelemetry(allSummaries, { weeklyTokenBudget: weeklyBudget });
2153
+ const output = renderTelemetryDashboard(aggregated, { json: options.json });
2154
+ console.log(output);
2155
+ }
2156
+
2157
+ // src/commands/run.ts
2158
+ import pc9 from "picocolors";
2159
+
2160
+ // src/lib/runner.ts
2161
+ import fs11 from "fs";
2162
+ import path11 from "path";
2163
+ import os2 from "os";
2164
+ import { spawn } from "child_process";
2165
+
2166
+ // src/lib/worktree.ts
2167
+ import fs10 from "fs";
2168
+ import path10 from "path";
2169
+ import { execFile as execFile2 } from "child_process";
2170
+ import { promisify as promisify2 } from "util";
2171
+ var execFileAsync2 = promisify2(execFile2);
2172
+ function getWorktreesBaseDir(repoRoot) {
2173
+ return path10.join(repoRoot, ".jonah-fleet", "worktrees");
2174
+ }
2175
+ async function createWorktree(repoRoot, options) {
2176
+ const baseDir = getWorktreesBaseDir(repoRoot);
2177
+ fs10.mkdirSync(baseDir, { recursive: true });
2178
+ const sanitizedDirName = options.worktreeDirName || options.branchName.replace(/[^a-zA-Z0-9._-]/g, "-");
2179
+ const worktreePath = path10.join(baseDir, sanitizedDirName);
2180
+ if (fs10.existsSync(worktreePath)) {
2181
+ try {
2182
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot });
2183
+ } catch {
2184
+ fs10.rmSync(worktreePath, { recursive: true, force: true });
2185
+ await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot }).catch(() => {
2186
+ });
2187
+ }
2188
+ }
2189
+ let baseRef = options.baseRef;
2190
+ if (!baseRef) {
2191
+ try {
2192
+ await execFileAsync2("git", ["rev-parse", "--verify", "origin/main"], { cwd: repoRoot });
2193
+ baseRef = "origin/main";
2194
+ } catch {
2195
+ try {
2196
+ await execFileAsync2("git", ["rev-parse", "--verify", "main"], { cwd: repoRoot });
2197
+ baseRef = "main";
2198
+ } catch {
2199
+ baseRef = "HEAD";
2200
+ }
2201
+ }
2202
+ }
2203
+ try {
2204
+ await execFileAsync2("git", ["branch", "-D", options.branchName], { cwd: repoRoot });
2205
+ } catch {
2206
+ }
2207
+ await execFileAsync2("git", ["worktree", "add", worktreePath, "-b", options.branchName, baseRef], {
2208
+ cwd: repoRoot
2209
+ });
2210
+ return { worktreePath, branchName: options.branchName };
2211
+ }
2212
+ async function removeWorktree(repoRoot, worktreePath, options = {}) {
2213
+ try {
2214
+ if (fs10.existsSync(worktreePath)) {
2215
+ await execFileAsync2("git", ["worktree", "remove", "--force", worktreePath], { cwd: repoRoot });
2216
+ }
2217
+ } catch {
2218
+ if (fs10.existsSync(worktreePath)) {
2219
+ fs10.rmSync(worktreePath, { recursive: true, force: true });
2220
+ }
2221
+ } finally {
2222
+ await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot }).catch(() => {
2223
+ });
2224
+ }
2225
+ if (options.deleteBranch && options.branchName) {
2226
+ await execFileAsync2("git", ["branch", "-D", options.branchName], { cwd: repoRoot }).catch(() => {
2227
+ });
2228
+ }
2229
+ }
2230
+ async function listActiveWorktrees(repoRoot) {
2231
+ try {
2232
+ const { stdout } = await execFileAsync2("git", ["worktree", "list", "--porcelain"], { cwd: repoRoot });
2233
+ const entries = stdout.trim().split("\n\n");
2234
+ const worktrees = [];
2235
+ const baseDir = path10.resolve(getWorktreesBaseDir(repoRoot));
2236
+ for (const entry of entries) {
2237
+ if (!entry.trim()) continue;
2238
+ const lines = entry.split("\n");
2239
+ let currentPath = "";
2240
+ let currentBranch = "";
2241
+ let currentCommit = "";
2242
+ for (const line of lines) {
2243
+ if (line.startsWith("worktree ")) {
2244
+ currentPath = line.substring(9).trim();
2245
+ } else if (line.startsWith("HEAD ")) {
2246
+ currentCommit = line.substring(5).trim();
2247
+ } else if (line.startsWith("branch ")) {
2248
+ currentBranch = line.substring(7).replace("refs/heads/", "").trim();
2249
+ }
2250
+ }
2251
+ const resolvedPath = path10.resolve(currentPath);
2252
+ if (resolvedPath.startsWith(baseDir)) {
2253
+ worktrees.push({
2254
+ path: resolvedPath,
2255
+ branch: currentBranch || "detached",
2256
+ commit: currentCommit
2257
+ });
2258
+ }
2259
+ }
2260
+ return worktrees;
2261
+ } catch {
2262
+ return [];
2263
+ }
2264
+ }
2265
+ async function cleanupStaleWorktrees(repoRoot) {
2266
+ let cleaned = 0;
2267
+ try {
2268
+ await execFileAsync2("git", ["worktree", "prune"], { cwd: repoRoot });
2269
+ const baseDir = getWorktreesBaseDir(repoRoot);
2270
+ if (fs10.existsSync(baseDir)) {
2271
+ const active = await listActiveWorktrees(repoRoot);
2272
+ const activePaths = new Set(active.map((w) => w.path));
2273
+ const dirs = fs10.readdirSync(baseDir);
2274
+ for (const dir of dirs) {
2275
+ const fullPath = path10.resolve(path10.join(baseDir, dir));
2276
+ if (!activePaths.has(fullPath)) {
2277
+ fs10.rmSync(fullPath, { recursive: true, force: true });
2278
+ cleaned++;
2279
+ }
2280
+ }
2281
+ }
2282
+ } catch {
2283
+ }
2284
+ return cleaned;
2285
+ }
2286
+
2287
+ // src/lib/runner.ts
2288
+ function discoverSkillsPrompt(targetDir) {
2289
+ const skillsDir = path11.join(targetDir, ".agents", "skills");
2290
+ if (!fs11.existsSync(skillsDir)) return "";
2291
+ let skillsPrompt = "";
2292
+ try {
2293
+ const entries = fs11.readdirSync(skillsDir, { withFileTypes: true });
2294
+ for (const entry of entries) {
2295
+ if (entry.isDirectory()) {
2296
+ const skillPath = path11.join(".agents", "skills", entry.name, "SKILL.md");
2297
+ const fullPath = path11.join(targetDir, skillPath);
2298
+ if (fs11.existsSync(fullPath)) {
2299
+ skillsPrompt += `Read and follow ${skillPath}. `;
2300
+ }
2301
+ }
2302
+ }
2303
+ } catch {
2304
+ }
2305
+ return skillsPrompt;
2306
+ }
2307
+ function buildRoutinePrompt(targetDir, routine, options = {}) {
2308
+ const skillsPrompt = discoverSkillsPrompt(targetDir);
2309
+ const promptFile = `.github/prompts/${routine}.md`;
2310
+ if (routine === "autowork") {
2311
+ if (options.issue) {
2312
+ return `You are the Autowork routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}Your target is issue #${options.issue}. You are in Targeted mode: work issue #${options.issue} directly, ahead of Phase 1 convergence and priority scan.`;
2313
+ }
2314
+ return `You are the Autowork routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}You are in Scan mode: check open PRs for review comments to fix, close merged issues, then pick the highest-priority unclaimed issue.`;
2315
+ }
2316
+ if (routine === "peer-review") {
2317
+ if (options.pr) {
2318
+ return `You are the Peer Review routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}Your target is pull request #${options.pr}. You are in Targeted mode: review PR #${options.pr} directly.`;
2319
+ }
2320
+ return `You are the Peer Review routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}You are in Scan mode: check open PRs and select the highest-priority PR to review.`;
2321
+ }
2322
+ return `You are the ${routine} routine for this repository. Read and follow the instructions in ${promptFile} exactly. ${skillsPrompt}`;
2323
+ }
2324
+ async function runLocalRoutine(options) {
2325
+ const targetDir = path11.resolve(options.targetDir);
2326
+ const routine = options.routine;
2327
+ const model = options.model || "gemini-3.7-flash-high";
2328
+ const printTimeout = options.printTimeout || "30m";
2329
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2330
+ const hostname = os2.hostname();
2331
+ const promptFile = path11.join(targetDir, ".github", "prompts", `${routine}.md`);
2332
+ if (!fs11.existsSync(promptFile)) {
2333
+ throw new Error(`Routine prompt file not found: ${promptFile}`);
2334
+ }
2335
+ let branchName = `agent/${routine}-${timestamp}`;
2336
+ if (options.issue) {
2337
+ branchName = `agent/${routine}-issue-${options.issue}-${timestamp}`;
2338
+ } else if (options.pr) {
2339
+ branchName = `agent/${routine}-pr-${options.pr}-${timestamp}`;
2340
+ }
2341
+ let worktreePath;
2342
+ let executionDir = targetDir;
2343
+ if (!options.noWorktree && !options.dryRun) {
2344
+ const worktreeResult = await createWorktree(targetDir, { branchName });
2345
+ worktreePath = worktreeResult.worktreePath;
2346
+ executionDir = worktreePath;
2347
+ }
2348
+ const prompt = buildRoutinePrompt(executionDir, routine, {
2349
+ issue: options.issue,
2350
+ pr: options.pr
2351
+ });
2352
+ if (options.dryRun) {
2353
+ return {
2354
+ success: true,
2355
+ exitCode: 0,
2356
+ output: `[DRY RUN] Would execute routine '${routine}' in ${options.noWorktree ? "current directory" : "worktree"}:
2357
+ Prompt: ${prompt}
2358
+ Model: ${model}
2359
+ Timeout: ${printTimeout}`,
2360
+ worktreePath,
2361
+ branchName
2362
+ };
2363
+ }
2364
+ const childEnv = {
2365
+ ...process.env,
2366
+ ...options.env,
2367
+ LOCAL_AGENT: "true",
2368
+ LOCAL_HOST: hostname,
2369
+ TARGET_ISSUE: options.issue ? String(options.issue) : "",
2370
+ PR_NUMBER: options.pr ? String(options.pr) : ""
2371
+ };
2372
+ const args = [
2373
+ "-p",
2374
+ prompt,
2375
+ "--model",
2376
+ model,
2377
+ "--output-format",
2378
+ "text",
2379
+ "--print-timeout",
2380
+ printTimeout,
2381
+ "--dangerously-skip-permissions"
2382
+ ];
2383
+ let output = "";
2384
+ let exitCode = 0;
2385
+ const cleanup = async () => {
2386
+ if (worktreePath && !options.keepWorktree) {
2387
+ await removeWorktree(targetDir, worktreePath, { deleteBranch: false }).catch(() => {
2388
+ });
2389
+ }
2390
+ };
2391
+ const sigintHandler = async () => {
2392
+ await cleanup();
2393
+ process.exit(130);
2394
+ };
2395
+ process.once("SIGINT", sigintHandler);
2396
+ process.once("SIGTERM", sigintHandler);
2397
+ try {
2398
+ exitCode = await new Promise((resolve, reject) => {
2399
+ const child = spawn("agy", args, {
2400
+ cwd: executionDir,
2401
+ env: childEnv,
2402
+ stdio: ["inherit", "pipe", "pipe"]
2403
+ });
2404
+ child.stdout?.on("data", (data) => {
2405
+ const chunk = data.toString();
2406
+ output += chunk;
2407
+ if (options.onLog) {
2408
+ options.onLog(chunk);
2409
+ } else {
2410
+ process.stdout.write(chunk);
2411
+ }
2412
+ });
2413
+ child.stderr?.on("data", (data) => {
2414
+ const chunk = data.toString();
2415
+ output += chunk;
2416
+ if (options.onLog) {
2417
+ options.onLog(chunk);
2418
+ } else {
2419
+ process.stderr.write(chunk);
2420
+ }
2421
+ });
2422
+ child.on("error", (err) => {
2423
+ reject(err);
2424
+ });
2425
+ child.on("close", (code) => {
2426
+ resolve(code ?? 0);
2427
+ });
2428
+ });
2429
+ } finally {
2430
+ process.removeListener("SIGINT", sigintHandler);
2431
+ process.removeListener("SIGTERM", sigintHandler);
2432
+ if (!options.keepWorktree) {
2433
+ await cleanup();
2434
+ }
2435
+ }
2436
+ return {
2437
+ success: exitCode === 0,
2438
+ exitCode,
2439
+ output,
2440
+ worktreePath,
2441
+ branchName
2442
+ };
2443
+ }
2444
+
2445
+ // src/commands/run.ts
2446
+ async function runRoutineCommand(routine, options = {}) {
2447
+ const cwd = process.cwd();
2448
+ const manifest = loadManifest(cwd);
2449
+ if (!manifest) {
2450
+ console.warn(
2451
+ pc9.yellow(`\u26A0\uFE0F No agents-manifest.json found in ${cwd}. Running in unmanaged repository mode.`)
2452
+ );
2453
+ } else if (manifest.routines && manifest.routines[routine] === false) {
2454
+ console.warn(
2455
+ pc9.yellow(`\u26A0\uFE0F Routine '${routine}' is disabled in agents-manifest.json. Running anyway via explicit command.`)
2456
+ );
2457
+ }
2458
+ console.log(pc9.cyan(`
2459
+ \u{1F680} Launching local agent session for routine: ${pc9.bold(routine)}`));
2460
+ if (options.issue) {
2461
+ console.log(pc9.dim(` Target issue: #${options.issue}`));
2462
+ }
2463
+ if (options.pr) {
2464
+ console.log(pc9.dim(` Target pull request: #${options.pr}`));
2465
+ }
2466
+ if (options.model) {
2467
+ console.log(pc9.dim(` Model override: ${options.model}`));
2468
+ }
2469
+ if (options.worktree !== false) {
2470
+ console.log(pc9.dim(` Workspace isolation: Git Worktree (.jonah-fleet/worktrees/)`));
2471
+ } else {
2472
+ console.log(pc9.yellow(` Workspace isolation: Disabled (running in current directory)`));
2473
+ }
2474
+ console.log("");
2475
+ try {
2476
+ const result = await runLocalRoutine({
2477
+ targetDir: cwd,
2478
+ routine,
2479
+ issue: options.issue,
2480
+ pr: options.pr,
2481
+ model: options.model,
2482
+ printTimeout: options.timeout,
2483
+ noWorktree: options.worktree === false,
2484
+ keepWorktree: options.keepWorktree,
2485
+ dryRun: options.dryRun
2486
+ });
2487
+ if (options.dryRun) {
2488
+ console.log(pc9.green(result.output));
2489
+ return;
2490
+ }
2491
+ if (result.success) {
2492
+ console.log(pc9.green(`
2493
+ \u2713 Local agent session for '${routine}' completed successfully.`));
2494
+ } else {
2495
+ console.error(pc9.red(`
2496
+ \u2717 Local agent session for '${routine}' failed with exit code ${result.exitCode}.`));
2497
+ process.exit(result.exitCode);
2498
+ }
2499
+ } catch (error) {
2500
+ console.error(pc9.red(`
2501
+ \u2717 Failed to execute routine '${routine}': ${error.message}`));
982
2502
  process.exit(1);
983
2503
  }
984
2504
  }
985
2505
 
2506
+ // src/commands/daemon.ts
2507
+ import pc11 from "picocolors";
2508
+
2509
+ // src/lib/daemon.ts
2510
+ import fs12 from "fs";
2511
+ import path12 from "path";
2512
+ import { spawn as spawn2 } from "child_process";
2513
+ import pc10 from "picocolors";
2514
+ function getDaemonStatePath(repoRoot) {
2515
+ return path12.join(repoRoot, ".jonah-fleet", "daemon.json");
2516
+ }
2517
+ function readDaemonState(repoRoot) {
2518
+ const statePath = getDaemonStatePath(repoRoot);
2519
+ if (!fs12.existsSync(statePath)) return null;
2520
+ try {
2521
+ return JSON.parse(fs12.readFileSync(statePath, "utf8"));
2522
+ } catch {
2523
+ return null;
2524
+ }
2525
+ }
2526
+ function writeDaemonState(repoRoot, state) {
2527
+ const statePath = getDaemonStatePath(repoRoot);
2528
+ fs12.mkdirSync(path12.dirname(statePath), { recursive: true });
2529
+ fs12.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
2530
+ }
2531
+ function clearDaemonState(repoRoot) {
2532
+ const statePath = getDaemonStatePath(repoRoot);
2533
+ if (fs12.existsSync(statePath)) {
2534
+ try {
2535
+ fs12.unlinkSync(statePath);
2536
+ } catch {
2537
+ }
2538
+ }
2539
+ }
2540
+ function isDaemonRunning(repoRoot) {
2541
+ const state = readDaemonState(repoRoot);
2542
+ if (!state || !state.pid) return false;
2543
+ try {
2544
+ process.kill(state.pid, 0);
2545
+ return true;
2546
+ } catch {
2547
+ clearDaemonState(repoRoot);
2548
+ return false;
2549
+ }
2550
+ }
2551
+ async function startBackgroundDaemon(repoRoot, options = {}) {
2552
+ if (isDaemonRunning(repoRoot)) {
2553
+ const existing = readDaemonState(repoRoot);
2554
+ throw new Error(`Daemon is already running with PID ${existing?.pid}`);
2555
+ }
2556
+ const interval = options.interval || 30;
2557
+ const routines = options.routines || ["autowork", "peer-review"];
2558
+ const logFilePath = path12.join(repoRoot, ".jonah-fleet", "daemon.log");
2559
+ fs12.mkdirSync(path12.dirname(logFilePath), { recursive: true });
2560
+ const logFd = fs12.openSync(logFilePath, "a");
2561
+ const cliPath = process.argv[1];
2562
+ const args = ["daemon", "--foreground", "--interval", String(interval), "--routines", routines.join(",")];
2563
+ if (options.model) {
2564
+ args.push("--model", options.model);
2565
+ }
2566
+ const child = spawn2(process.execPath, [cliPath, ...args], {
2567
+ cwd: repoRoot,
2568
+ detached: true,
2569
+ stdio: ["ignore", logFd, logFd],
2570
+ env: { ...process.env, JONAH_FLEET_DAEMON: "true" }
2571
+ });
2572
+ child.unref();
2573
+ const state = {
2574
+ pid: child.pid,
2575
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2576
+ intervalMinutes: interval,
2577
+ routines,
2578
+ status: "idle"
2579
+ };
2580
+ writeDaemonState(repoRoot, state);
2581
+ return state;
2582
+ }
2583
+ async function stopDaemon(repoRoot) {
2584
+ const state = readDaemonState(repoRoot);
2585
+ if (!state || !state.pid) return false;
2586
+ try {
2587
+ process.kill(state.pid, "SIGTERM");
2588
+ clearDaemonState(repoRoot);
2589
+ await cleanupStaleWorktrees(repoRoot);
2590
+ return true;
2591
+ } catch {
2592
+ clearDaemonState(repoRoot);
2593
+ return false;
2594
+ }
2595
+ }
2596
+ async function runDaemonLoop(repoRoot, options = {}) {
2597
+ const intervalMinutes = options.interval || 30;
2598
+ const intervalMs = intervalMinutes * 60 * 1e3;
2599
+ const routines = options.routines || ["autowork", "peer-review"];
2600
+ const state = {
2601
+ pid: process.pid,
2602
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2603
+ intervalMinutes,
2604
+ routines,
2605
+ status: "idle"
2606
+ };
2607
+ writeDaemonState(repoRoot, state);
2608
+ console.log(pc10.cyan(`
2609
+ \u{1F916} Jonah Fleet Local Agent Daemon Started`));
2610
+ console.log(pc10.dim(` PID: ${process.pid}`));
2611
+ console.log(pc10.dim(` Poll Interval: Every ${intervalMinutes} minutes`));
2612
+ console.log(pc10.dim(` Routines: ${routines.join(", ")}`));
2613
+ console.log(pc10.dim(` Working Directory: ${repoRoot}
2614
+ `));
2615
+ let isStopping = false;
2616
+ const handleStop = async () => {
2617
+ if (isStopping) return;
2618
+ isStopping = true;
2619
+ console.log(pc10.yellow(`
2620
+ Stopping local agent daemon...`));
2621
+ clearDaemonState(repoRoot);
2622
+ await cleanupStaleWorktrees(repoRoot);
2623
+ process.exit(0);
2624
+ };
2625
+ process.once("SIGINT", handleStop);
2626
+ process.once("SIGTERM", handleStop);
2627
+ const runTick = async () => {
2628
+ if (isStopping) return;
2629
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2630
+ state.lastCheckAt = now;
2631
+ writeDaemonState(repoRoot, state);
2632
+ console.log(pc10.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Running routine polling sweep...`));
2633
+ await cleanupStaleWorktrees(repoRoot);
2634
+ for (const routine of routines) {
2635
+ if (isStopping) break;
2636
+ try {
2637
+ state.status = "working";
2638
+ state.activeRoutine = routine;
2639
+ writeDaemonState(repoRoot, state);
2640
+ console.log(pc10.cyan(`
2641
+ \u25B6 Starting local scan for ${routine}...`));
2642
+ const result = await runLocalRoutine({
2643
+ targetDir: repoRoot,
2644
+ routine,
2645
+ model: options.model,
2646
+ noWorktree: false
2647
+ });
2648
+ if (result.success) {
2649
+ console.log(pc10.green(`\u2713 Local routine '${routine}' finished successfully.`));
2650
+ } else {
2651
+ console.warn(pc10.yellow(`\u26A0\uFE0F Local routine '${routine}' completed with code ${result.exitCode}.`));
2652
+ }
2653
+ } catch (err) {
2654
+ console.error(pc10.red(`\u2717 Error running routine '${routine}': ${err.message}`));
2655
+ } finally {
2656
+ state.status = "idle";
2657
+ state.activeRoutine = void 0;
2658
+ state.activeWorktree = void 0;
2659
+ writeDaemonState(repoRoot, state);
2660
+ }
2661
+ }
2662
+ console.log(pc10.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Sweep completed. Next run in ${intervalMinutes}m.`));
2663
+ };
2664
+ await runTick();
2665
+ const intervalId = setInterval(runTick, intervalMs);
2666
+ await new Promise(() => {
2667
+ });
2668
+ }
2669
+
2670
+ // src/commands/daemon.ts
2671
+ async function runDaemonCommand(action, options = {}) {
2672
+ const cwd = process.cwd();
2673
+ const act = action?.toLowerCase() || (options.foreground ? "foreground" : "status");
2674
+ const daemonOpts = {
2675
+ interval: options.interval ? parseInt(options.interval, 10) : void 0,
2676
+ routines: options.routines ? options.routines.split(",").map((r) => r.trim()) : void 0,
2677
+ model: options.model,
2678
+ foreground: options.foreground
2679
+ };
2680
+ if (act === "start") {
2681
+ if (options.foreground) {
2682
+ await runDaemonLoop(cwd, daemonOpts);
2683
+ return;
2684
+ }
2685
+ try {
2686
+ const state2 = await startBackgroundDaemon(cwd, daemonOpts);
2687
+ console.log(pc11.green(`
2688
+ \u2713 Background agent daemon started successfully.`));
2689
+ console.log(pc11.dim(` PID: ${state2.pid}`));
2690
+ console.log(pc11.dim(` Poll Interval: Every ${state2.intervalMinutes} minutes`));
2691
+ console.log(pc11.dim(` Routines: ${state2.routines.join(", ")}`));
2692
+ console.log(pc11.dim(` Log file: .jonah-fleet/daemon.log`));
2693
+ console.log(pc11.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
2694
+ } catch (err) {
2695
+ console.error(pc11.red(`
2696
+ \u2717 Failed to start daemon: ${err.message}`));
2697
+ process.exit(1);
2698
+ }
2699
+ return;
2700
+ }
2701
+ if (act === "stop") {
2702
+ if (!isDaemonRunning(cwd)) {
2703
+ console.log(pc11.yellow(`
2704
+ \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
2705
+ return;
2706
+ }
2707
+ const state2 = readDaemonState(cwd);
2708
+ console.log(pc11.cyan(`
2709
+ Stopping background agent daemon (PID ${state2?.pid})...`));
2710
+ const stopped = await stopDaemon(cwd);
2711
+ if (stopped) {
2712
+ console.log(pc11.green(`\u2713 Local agent daemon stopped successfully.`));
2713
+ } else {
2714
+ console.error(pc11.red(`\u2717 Could not terminate daemon process.`));
2715
+ process.exit(1);
2716
+ }
2717
+ return;
2718
+ }
2719
+ if (act === "foreground") {
2720
+ await runDaemonLoop(cwd, daemonOpts);
2721
+ return;
2722
+ }
2723
+ const running = isDaemonRunning(cwd);
2724
+ const state = readDaemonState(cwd);
2725
+ const activeWorktrees = await listActiveWorktrees(cwd);
2726
+ console.log(pc11.cyan(`
2727
+ \u{1F916} Jonah Fleet Local Daemon Status
2728
+ `));
2729
+ if (running && state) {
2730
+ console.log(` Status: ${pc11.green(pc11.bold("RUNNING"))}`);
2731
+ console.log(` PID: ${state.pid}`);
2732
+ console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
2733
+ console.log(` Interval: Every ${state.intervalMinutes} minutes`);
2734
+ console.log(` Routines: ${state.routines.join(", ")}`);
2735
+ console.log(` Current State: ${state.status === "working" ? pc11.yellow("WORKING on " + state.activeRoutine) : pc11.green("IDLE")}`);
2736
+ if (state.lastCheckAt) {
2737
+ console.log(` Last Check: ${new Date(state.lastCheckAt).toLocaleTimeString()}`);
2738
+ }
2739
+ } else {
2740
+ console.log(` Status: ${pc11.gray("STOPPED")}`);
2741
+ console.log(pc11.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
2742
+ }
2743
+ console.log(`
2744
+ Active Worktrees: ${activeWorktrees.length}`);
2745
+ for (const wt of activeWorktrees) {
2746
+ console.log(pc11.dim(` - [${wt.branch}] ${wt.path}`));
2747
+ }
2748
+ console.log("");
2749
+ }
2750
+
986
2751
  // src/index.ts
987
2752
  var program = new Command();
988
2753
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
989
- program.command("init").description("Initialize Jonah Fleet configuration, routines, workflows, and skills in the current repo").option("-p, --preset <preset>", "Preset profile to install (minimal | standard | full)", "standard").option("-f, --force", "Force overwrite existing files", false).action(async (options) => {
2754
+ program.command("run <routine>").description("Run a specific prompt routine locally in an isolated git worktree").option("-i, --issue <number>", "Targeted issue number for autowork").option("-p, --pr <number>", "Targeted pull request number for peer-review").option("-m, --model <model>", "LLM model override (defaults to gemini-3.7-flash-high)").option("--timeout <duration>", "CLI execution print timeout (default: 30m)").option("--no-worktree", "Execute directly in current directory without creating a git worktree").option("--keep-worktree", "Preserve the git worktree after routine execution completes").option("-d, --dry-run", "Preview prompt and execution parameters without launching agent").action(async (routine, options) => {
2755
+ await runRoutineCommand(routine, options);
2756
+ });
2757
+ program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Polling interval in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: autowork,peer-review)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").action(async (action, options) => {
2758
+ await runDaemonCommand(action, options);
2759
+ });
2760
+ program.command("init").description("Initialize Jonah Fleet configuration, routines, workflows, and skills in the current repo").option("-p, --preset <preset>", "Preset profile to install (minimal | standard | full)", "standard").option("-f, --force", "Force overwrite existing files", false).option("--stack <stack>", "Override detected tech stack name").option("--package-manager <pm>", "Override package manager (npm, pnpm, yarn, bun, uv, poetry, cargo, go)").option("--test-cmd <cmd>", "Override test execution command").option("--build-cmd <cmd>", "Override build execution command").option("--interactive", "Force interactive prompts for stack configuration").option("--no-interactive", "Disable interactive prompts").action(async (options) => {
990
2761
  await runInit(options);
991
2762
  });
992
2763
  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
2764
  await runSync(options);
994
2765
  });
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) => {
2766
+ 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
2767
  await runStatus(options);
997
2768
  });
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) => {
2769
+ 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
2770
  await runMonitor({ ...options, repos });
1000
2771
  });
1001
- program.command("contribute").description("Submit local prompt improvements back upstream to jonah-fleet").option("-t, --title <title>", "Contribution PR title").option("-b, --body <body>", "Contribution PR description").action(async (options) => {
2772
+ 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").option("-p, --prompt <prompt>", "Target prompt template being refined").option("-r, --repo <repo>", "Upstream target repository (defaults to juliendurandeu/jonah-fleet)").option("-d, --dry-run", "Preview contribution branch and PR command without executing", false).action(async (options) => {
1002
2773
  await runContribute(options);
1003
2774
  });
2775
+ program.command("telemetry [repos...]").description("Aggregate and report fleet-wide telemetry, review loop metrics, failure categories, and weekly token spend").option("-l, --log <path>", "Path to markdown log file to emit").option("-e, --endpoint <url>", "Collector endpoint URL for telemetry emission").option("-b, --budget <tokens>", "Weekly token budget ceiling (~8.75M default)").option("-j, --json", "Output telemetry summary as JSON", false).option("--emit", "Emit telemetry event for specified or latest run log").action(async (repos, options) => {
2776
+ const action = options.emit ? "emit" : "aggregate";
2777
+ await runTelemetry({ ...options, repos, action });
2778
+ });
1004
2779
  program.parse(process.argv);