jonah-fleet 1.2.0 → 1.4.1

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 +96 -0
  2. package/README.md +9 -2
  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 +10 -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 +1781 -120
  22. package/dist/lib/daemon.d.ts +42 -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 +9 -5
  49. package/schema.json +44 -1
  50. package/templates/docs/AGENTS.template.md +15 -0
  51. package/templates/prompts/ORCHESTRATION.md +74 -3
  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 +2 -0
  56. package/templates/prompts/peer-review.md +14 -5
  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.2.0";
93
+ var FLEET_VERSION = "1.4.1";
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/):`));
@@ -276,8 +734,8 @@ async function runInit(options = {}) {
276
734
  import pc2 from "picocolors";
277
735
 
278
736
  // src/lib/diff.ts
279
- import fs3 from "fs";
280
- import path3 from "path";
737
+ import fs4 from "fs";
738
+ import path4 from "path";
281
739
  function checkDrift(targetDir, manifest) {
282
740
  const templatesDir = getTemplatesDir();
283
741
  const report = {
@@ -287,43 +745,64 @@ function checkDrift(targetDir, manifest) {
287
745
  modifiedWorkflows: [],
288
746
  missingSkills: []
289
747
  };
290
- const targetPromptsDir = path3.join(targetDir, ".github/prompts");
291
- const targetWorkflowsDir = path3.join(targetDir, ".github/workflows");
292
- 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");
293
751
  const basePrompts = ["ORCHESTRATION.md", "_prompt-template.md"];
294
752
  for (const file of basePrompts) {
295
- const src = path3.join(templatesDir, "prompts", file);
296
- const dest = path3.join(targetPromptsDir, file);
297
- if (!fs3.existsSync(dest)) {
753
+ const src = path4.join(templatesDir, "prompts", file);
754
+ const dest = path4.join(targetPromptsDir, file);
755
+ if (!fs4.existsSync(dest)) {
298
756
  report.missingPrompts.push(file);
299
- } else if (fs3.readFileSync(src, "utf8") !== fs3.readFileSync(dest, "utf8")) {
757
+ } else if (fs4.readFileSync(src, "utf8") !== fs4.readFileSync(dest, "utf8")) {
300
758
  report.modifiedPrompts.push(file);
301
759
  }
302
760
  }
303
761
  for (const [routineName, isEnabled] of Object.entries(manifest.routines)) {
304
762
  if (!isEnabled) continue;
305
763
  const promptFile = `${routineName}.md`;
306
- const promptSrc = path3.join(templatesDir, "prompts", promptFile);
307
- const promptDest = path3.join(targetPromptsDir, promptFile);
308
- if (!fs3.existsSync(promptDest)) {
764
+ const promptSrc = path4.join(templatesDir, "prompts", promptFile);
765
+ const promptDest = path4.join(targetPromptsDir, promptFile);
766
+ if (!fs4.existsSync(promptDest)) {
309
767
  report.missingPrompts.push(promptFile);
310
- } 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")) {
311
769
  report.modifiedPrompts.push(promptFile);
312
770
  }
313
771
  const workflows = ROUTINE_TO_WORKFLOW_MAP[routineName] || [];
314
772
  for (const workflowFile of workflows) {
315
- const wfSrc = path3.join(templatesDir, "workflows", workflowFile);
316
- const wfDest = path3.join(targetWorkflowsDir, workflowFile);
317
- if (!fs3.existsSync(wfDest)) {
773
+ const wfSrc = path4.join(templatesDir, "workflows", workflowFile);
774
+ const wfDest = path4.join(targetWorkflowsDir, workflowFile);
775
+ if (!fs4.existsSync(wfDest)) {
318
776
  report.missingWorkflows.push(workflowFile);
319
- } else if (fs3.existsSync(wfSrc) && fs3.readFileSync(wfSrc, "utf8") !== fs3.readFileSync(wfDest, "utf8")) {
320
- 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");
321
800
  }
322
801
  }
323
802
  }
324
803
  for (const skill of manifest.skills) {
325
- const skillDestDir = path3.join(targetSkillsDir, skill);
326
- if (!fs3.existsSync(skillDestDir)) {
804
+ const skillDestDir = path4.join(targetSkillsDir, skill);
805
+ if (!fs4.existsSync(skillDestDir)) {
327
806
  report.missingSkills.push(skill);
328
807
  }
329
808
  }
@@ -369,14 +848,14 @@ Run 'jonah-fleet sync --force' to apply updates.
369
848
  }
370
849
 
371
850
  // src/commands/status.ts
372
- import fs6 from "fs";
373
- import path6 from "path";
851
+ import fs7 from "fs";
852
+ import path7 from "path";
374
853
  import pc5 from "picocolors";
375
854
 
376
855
  // src/lib/fleet-query.ts
377
856
  import { execFile } from "child_process";
378
- import fs4 from "fs";
379
- import path4 from "path";
857
+ import fs5 from "fs";
858
+ import path5 from "path";
380
859
  import { promisify } from "util";
381
860
  var execFileAsync = promisify(execFile);
382
861
  var defaultGhExecutor = async (args) => {
@@ -555,11 +1034,11 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
555
1034
  staleWarnings: []
556
1035
  };
557
1036
  try {
558
- if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
559
- const manifestPath = path4.join(repoIdentifier, "agents-manifest.json");
560
- if (fs4.existsSync(manifestPath)) {
1037
+ if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
1038
+ const manifestPath = path5.join(repoIdentifier, "agents-manifest.json");
1039
+ if (fs5.existsSync(manifestPath)) {
561
1040
  try {
562
- const raw = JSON.parse(fs4.readFileSync(manifestPath, "utf8"));
1041
+ const raw = JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
563
1042
  result.fleetVersion = raw.version;
564
1043
  result.preset = raw.preset;
565
1044
  } catch {
@@ -639,18 +1118,18 @@ async function queryRepoFleetStatus(repoIdentifier, executor = defaultGhExecutor
639
1118
  if (!result.error) result.error = `Failed to fetch issues: ${err.message}`;
640
1119
  }
641
1120
  const logContents = [];
642
- if (fs4.existsSync(repoIdentifier) && fs4.statSync(repoIdentifier).isDirectory()) {
643
- const logsDir = path4.join(repoIdentifier, ".github/prompts/logs");
644
- if (fs4.existsSync(logsDir)) {
1121
+ if (fs5.existsSync(repoIdentifier) && fs5.statSync(repoIdentifier).isDirectory()) {
1122
+ const logsDir = path5.join(repoIdentifier, ".github/prompts/logs");
1123
+ if (fs5.existsSync(logsDir)) {
645
1124
  const collectLogs = (dir) => {
646
- const entries = fs4.readdirSync(dir, { withFileTypes: true });
1125
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
647
1126
  for (const entry of entries) {
648
- const fullPath = path4.join(dir, entry.name);
1127
+ const fullPath = path5.join(dir, entry.name);
649
1128
  if (entry.isDirectory()) {
650
1129
  collectLogs(fullPath);
651
1130
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
652
1131
  try {
653
- logContents.push(fs4.readFileSync(fullPath, "utf8"));
1132
+ logContents.push(fs5.readFileSync(fullPath, "utf8"));
654
1133
  } catch {
655
1134
  }
656
1135
  }
@@ -862,9 +1341,18 @@ function renderFleetDashboard(statuses, options = {}) {
862
1341
  lines.push(
863
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)`)
864
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]");
865
1350
  lines.push(
866
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`
867
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
+ );
868
1356
  if (summary.byRoutine && Object.keys(summary.byRoutine).length > 0 && (options.tokens || options.detailed || statuses.length > 1)) {
869
1357
  lines.push(pc3.bold("\n Fleet Spend by Routine:"));
870
1358
  const fleetRoutines = Object.values(summary.byRoutine).sort((a, b) => b.totalTokens - a.totalTokens);
@@ -883,20 +1371,20 @@ function renderFleetDashboard(statuses, options = {}) {
883
1371
  import pc4 from "picocolors";
884
1372
 
885
1373
  // src/lib/global-config.ts
886
- import fs5 from "fs";
887
- import path5 from "path";
1374
+ import fs6 from "fs";
1375
+ import path6 from "path";
888
1376
  import os from "os";
889
1377
  function getDefaultGlobalConfigPath() {
890
- const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path5.join(os.homedir(), ".jonah-fleet");
891
- return path5.join(baseDir, "config.json");
1378
+ const baseDir = process.env.JONAH_FLEET_CONFIG_DIR || path6.join(os.homedir(), ".jonah-fleet");
1379
+ return path6.join(baseDir, "config.json");
892
1380
  }
893
1381
  function loadGlobalConfig(customPath) {
894
1382
  const filePath = customPath || getDefaultGlobalConfigPath();
895
- if (!fs5.existsSync(filePath)) {
1383
+ if (!fs6.existsSync(filePath)) {
896
1384
  return { repositories: [] };
897
1385
  }
898
1386
  try {
899
- const raw = fs5.readFileSync(filePath, "utf8");
1387
+ const raw = fs6.readFileSync(filePath, "utf8");
900
1388
  const parsed = JSON.parse(raw);
901
1389
  return {
902
1390
  repositories: Array.isArray(parsed.repositories) ? parsed.repositories : []
@@ -907,11 +1395,11 @@ function loadGlobalConfig(customPath) {
907
1395
  }
908
1396
  function saveGlobalConfig(config, customPath) {
909
1397
  const filePath = customPath || getDefaultGlobalConfigPath();
910
- const dir = path5.dirname(filePath);
911
- if (!fs5.existsSync(dir)) {
912
- fs5.mkdirSync(dir, { recursive: true });
1398
+ const dir = path6.dirname(filePath);
1399
+ if (!fs6.existsSync(dir)) {
1400
+ fs6.mkdirSync(dir, { recursive: true });
913
1401
  }
914
- fs5.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
1402
+ fs6.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n", "utf8");
915
1403
  }
916
1404
  function addGlobalRepository(repo, customPath) {
917
1405
  const config = loadGlobalConfig(customPath);
@@ -1039,19 +1527,19 @@ async function runStatus(options = {}) {
1039
1527
  }
1040
1528
  const drift = checkDrift(cwd, manifest);
1041
1529
  const hasDrift = drift.missingPrompts.length > 0 || drift.modifiedPrompts.length > 0 || drift.missingWorkflows.length > 0 || drift.modifiedWorkflows.length > 0 || drift.missingSkills.length > 0;
1042
- const logsDir = path6.join(cwd, ".github/prompts/logs");
1530
+ const logsDir = path7.join(cwd, ".github/prompts/logs");
1043
1531
  let tokenUsage = void 0;
1044
- if (fs6.existsSync(logsDir)) {
1532
+ if (fs7.existsSync(logsDir)) {
1045
1533
  const logContents = [];
1046
1534
  const collectLogs = (dir) => {
1047
- const entries = fs6.readdirSync(dir, { withFileTypes: true });
1535
+ const entries = fs7.readdirSync(dir, { withFileTypes: true });
1048
1536
  for (const entry of entries) {
1049
- const fullPath = path6.join(dir, entry.name);
1537
+ const fullPath = path7.join(dir, entry.name);
1050
1538
  if (entry.isDirectory()) {
1051
1539
  collectLogs(fullPath);
1052
1540
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
1053
1541
  try {
1054
- logContents.push(fs6.readFileSync(fullPath, "utf8"));
1542
+ logContents.push(fs7.readFileSync(fullPath, "utf8"));
1055
1543
  } catch {
1056
1544
  }
1057
1545
  }
@@ -1141,40 +1629,1209 @@ async function runStatus(options = {}) {
1141
1629
  // src/commands/contribute.ts
1142
1630
  import { execSync } from "child_process";
1143
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
+ }
1144
1647
  async function runContribute(options = {}) {
1145
1648
  console.log(pc6.cyan(`
1146
1649
  \u{1F680} Jonah Fleet Upstream Contribution Bridge
1147
1650
  `));
1148
- const title = options.title || "fix(prompts): improve orchestrator routine handling";
1149
- const body = options.body || "Proposed prompt optimization discovered during autonomous execution runs.";
1150
- console.log(`Preparing upstream contribution PR against ${pc6.bold("juliendurandeu/jonah-fleet")}...`);
1151
- console.log(`Title: ${pc6.green(title)}`);
1152
- console.log(`Body: ${pc6.gray(body)}
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)}
1153
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
+ }
1154
1667
  try {
1155
- const branchName = `contrib/optimize-${Date.now()}`;
1156
- console.log(`Creating branch ${pc6.cyan(branchName)}...`);
1157
- try {
1158
- execSync("gh auth status", { stdio: "pipe" });
1159
- } catch {
1160
- console.error(pc6.red("\u274C GitHub CLI (`gh`) is not authenticated. Run `gh auth login` first."));
1161
- process.exit(1);
1162
- }
1163
- console.log(pc6.green(`\u2713 Ready to package and submit upstream contribution to juliendurandeu/jonah-fleet.`));
1164
- console.log(pc6.cyan(`Command executed by optimizer routine or operator:
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:
1165
1699
  `));
1166
- console.log(` gh pr create --repo juliendurandeu/jonah-fleet --title "${title}" --body "${body}"
1700
+ console.log(` ${payload.prCommand}
1167
1701
  `);
1702
+ return {
1703
+ success: true,
1704
+ branchName: payload.branchName,
1705
+ title: payload.title
1706
+ };
1707
+ }
1168
1708
  } catch (err) {
1169
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}`));
1170
2502
  process.exit(1);
1171
2503
  }
1172
2504
  }
1173
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, execFile as execFile3 } from "child_process";
2513
+ import { promisify as promisify3 } from "util";
2514
+ import pc10 from "picocolors";
2515
+ var execFileAsync3 = promisify3(execFile3);
2516
+ function getDaemonStatePath(repoRoot) {
2517
+ return path12.join(repoRoot, ".jonah-fleet", "daemon.json");
2518
+ }
2519
+ function readDaemonState(repoRoot) {
2520
+ const statePath = getDaemonStatePath(repoRoot);
2521
+ if (!fs12.existsSync(statePath)) return null;
2522
+ try {
2523
+ return JSON.parse(fs12.readFileSync(statePath, "utf8"));
2524
+ } catch {
2525
+ return null;
2526
+ }
2527
+ }
2528
+ function writeDaemonState(repoRoot, state) {
2529
+ const statePath = getDaemonStatePath(repoRoot);
2530
+ fs12.mkdirSync(path12.dirname(statePath), { recursive: true });
2531
+ fs12.writeFileSync(statePath, JSON.stringify(state, null, 2) + "\n", "utf8");
2532
+ }
2533
+ function clearDaemonState(repoRoot) {
2534
+ const statePath = getDaemonStatePath(repoRoot);
2535
+ if (fs12.existsSync(statePath)) {
2536
+ try {
2537
+ fs12.unlinkSync(statePath);
2538
+ } catch {
2539
+ }
2540
+ }
2541
+ }
2542
+ function isDaemonRunning(repoRoot) {
2543
+ const state = readDaemonState(repoRoot);
2544
+ if (!state || !state.pid) return false;
2545
+ try {
2546
+ process.kill(state.pid, 0);
2547
+ return true;
2548
+ } catch {
2549
+ clearDaemonState(repoRoot);
2550
+ return false;
2551
+ }
2552
+ }
2553
+ async function countOpenReadyPRs(repoRoot) {
2554
+ try {
2555
+ const { stdout } = await execFileAsync3(
2556
+ "gh",
2557
+ ["pr", "list", "--state", "open", "--draft=false", "--json", "number", "--jq", "length"],
2558
+ { cwd: repoRoot }
2559
+ );
2560
+ return parseInt(stdout.trim(), 10) || 0;
2561
+ } catch {
2562
+ return 0;
2563
+ }
2564
+ }
2565
+ async function startBackgroundDaemon(repoRoot, options = {}) {
2566
+ if (isDaemonRunning(repoRoot)) {
2567
+ const existing = readDaemonState(repoRoot);
2568
+ throw new Error(`Daemon is already running with PID ${existing?.pid}`);
2569
+ }
2570
+ const reviewInterval = options.reviewInterval || 3;
2571
+ const autoworkInterval = options.autoworkInterval || options.interval || 30;
2572
+ const routines = options.routines || ["peer-review", "autowork"];
2573
+ const logFilePath = path12.join(repoRoot, ".jonah-fleet", "daemon.log");
2574
+ fs12.mkdirSync(path12.dirname(logFilePath), { recursive: true });
2575
+ const logFd = fs12.openSync(logFilePath, "a");
2576
+ const cliPath = process.argv[1];
2577
+ const args = [
2578
+ "daemon",
2579
+ "--foreground",
2580
+ "--review-interval",
2581
+ String(reviewInterval),
2582
+ "--autowork-interval",
2583
+ String(autoworkInterval),
2584
+ "--routines",
2585
+ routines.join(",")
2586
+ ];
2587
+ if (options.model) {
2588
+ args.push("--model", options.model);
2589
+ }
2590
+ const child = spawn2(process.execPath, [cliPath, ...args], {
2591
+ cwd: repoRoot,
2592
+ detached: true,
2593
+ stdio: ["ignore", logFd, logFd],
2594
+ env: { ...process.env, JONAH_FLEET_DAEMON: "true" }
2595
+ });
2596
+ child.unref();
2597
+ const state = {
2598
+ pid: child.pid,
2599
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2600
+ reviewIntervalMinutes: reviewInterval,
2601
+ autoworkIntervalMinutes: autoworkInterval,
2602
+ routines,
2603
+ status: "idle"
2604
+ };
2605
+ writeDaemonState(repoRoot, state);
2606
+ return state;
2607
+ }
2608
+ async function stopDaemon(repoRoot) {
2609
+ const state = readDaemonState(repoRoot);
2610
+ if (!state || !state.pid) return false;
2611
+ try {
2612
+ process.kill(state.pid, "SIGTERM");
2613
+ clearDaemonState(repoRoot);
2614
+ await cleanupStaleWorktrees(repoRoot);
2615
+ return true;
2616
+ } catch {
2617
+ clearDaemonState(repoRoot);
2618
+ return false;
2619
+ }
2620
+ }
2621
+ async function runDaemonLoop(repoRoot, options = {}) {
2622
+ const reviewInterval = options.reviewInterval || 3;
2623
+ const autoworkInterval = options.autoworkInterval || options.interval || 30;
2624
+ const routines = options.routines || ["peer-review", "autowork"];
2625
+ const state = {
2626
+ pid: process.pid,
2627
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
2628
+ reviewIntervalMinutes: reviewInterval,
2629
+ autoworkIntervalMinutes: autoworkInterval,
2630
+ routines,
2631
+ status: "idle"
2632
+ };
2633
+ writeDaemonState(repoRoot, state);
2634
+ console.log(pc10.cyan(`
2635
+ \u{1F916} Jonah Fleet Multi-Cadence Local Agent Daemon Started`));
2636
+ console.log(pc10.dim(` PID: ${process.pid}`));
2637
+ console.log(pc10.dim(` Peer Review Watchdog: Every ${reviewInterval} minutes (with zero-cost PR preflight)`));
2638
+ console.log(pc10.dim(` Autowork Backlog Scan: Every ${autoworkInterval} minutes`));
2639
+ console.log(pc10.dim(` Working Directory: ${repoRoot}
2640
+ `));
2641
+ let isStopping = false;
2642
+ let isWorking = false;
2643
+ const handleStop = async () => {
2644
+ if (isStopping) return;
2645
+ isStopping = true;
2646
+ console.log(pc10.yellow(`
2647
+ Stopping local agent daemon...`));
2648
+ clearDaemonState(repoRoot);
2649
+ await cleanupStaleWorktrees(repoRoot);
2650
+ process.exit(0);
2651
+ };
2652
+ process.once("SIGINT", handleStop);
2653
+ process.once("SIGTERM", handleStop);
2654
+ const runReviewCheck = async () => {
2655
+ if (isStopping || isWorking) return;
2656
+ state.lastReviewCheckAt = (/* @__PURE__ */ new Date()).toISOString();
2657
+ writeDaemonState(repoRoot, state);
2658
+ const openPRCount = await countOpenReadyPRs(repoRoot);
2659
+ if (openPRCount === 0) {
2660
+ console.log(pc10.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Peer Review Watchdog: 0 ready PRs found (0 tokens used).`));
2661
+ return;
2662
+ }
2663
+ try {
2664
+ isWorking = true;
2665
+ state.status = "working";
2666
+ state.activeRoutine = "peer-review";
2667
+ writeDaemonState(repoRoot, state);
2668
+ console.log(pc10.cyan(`
2669
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F50D} Peer Review Watchdog: Found ${openPRCount} ready PR(s). Starting review session...`));
2670
+ await cleanupStaleWorktrees(repoRoot);
2671
+ const result = await runLocalRoutine({
2672
+ targetDir: repoRoot,
2673
+ routine: "peer-review",
2674
+ model: options.model,
2675
+ noWorktree: false
2676
+ });
2677
+ if (result.success) {
2678
+ console.log(pc10.green(`\u2713 Local peer-review completed successfully.`));
2679
+ } else {
2680
+ console.warn(pc10.yellow(`\u26A0\uFE0F Local peer-review completed with code ${result.exitCode}.`));
2681
+ }
2682
+ } catch (err) {
2683
+ console.error(pc10.red(`\u2717 Error in peer-review: ${err.message}`));
2684
+ } finally {
2685
+ isWorking = false;
2686
+ state.status = "idle";
2687
+ state.activeRoutine = void 0;
2688
+ writeDaemonState(repoRoot, state);
2689
+ }
2690
+ };
2691
+ const runAutoworkCheck = async () => {
2692
+ if (isStopping || isWorking || !routines.includes("autowork")) return;
2693
+ state.lastAutoworkCheckAt = (/* @__PURE__ */ new Date()).toISOString();
2694
+ writeDaemonState(repoRoot, state);
2695
+ try {
2696
+ isWorking = true;
2697
+ state.status = "working";
2698
+ state.activeRoutine = "autowork";
2699
+ writeDaemonState(repoRoot, state);
2700
+ console.log(pc10.cyan(`
2701
+ [${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] \u{1F680} Autowork Backlog Scan: Starting session...`));
2702
+ await cleanupStaleWorktrees(repoRoot);
2703
+ const result = await runLocalRoutine({
2704
+ targetDir: repoRoot,
2705
+ routine: "autowork",
2706
+ model: options.model,
2707
+ noWorktree: false
2708
+ });
2709
+ if (result.success) {
2710
+ console.log(pc10.green(`\u2713 Local autowork completed successfully.`));
2711
+ } else {
2712
+ console.warn(pc10.yellow(`\u26A0\uFE0F Local autowork completed with code ${result.exitCode}.`));
2713
+ }
2714
+ } catch (err) {
2715
+ console.error(pc10.red(`\u2717 Error in autowork: ${err.message}`));
2716
+ } finally {
2717
+ isWorking = false;
2718
+ state.status = "idle";
2719
+ state.activeRoutine = void 0;
2720
+ writeDaemonState(repoRoot, state);
2721
+ }
2722
+ };
2723
+ if (routines.includes("peer-review")) {
2724
+ await runReviewCheck();
2725
+ }
2726
+ if (routines.includes("autowork")) {
2727
+ await runAutoworkCheck();
2728
+ }
2729
+ const reviewIntervalMs = reviewInterval * 60 * 1e3;
2730
+ const autoworkIntervalMs = autoworkInterval * 60 * 1e3;
2731
+ const reviewTimer = setInterval(runReviewCheck, reviewIntervalMs);
2732
+ const autoworkTimer = setInterval(runAutoworkCheck, autoworkIntervalMs);
2733
+ await new Promise(() => {
2734
+ });
2735
+ }
2736
+
2737
+ // src/commands/daemon.ts
2738
+ async function runDaemonCommand(action, options = {}) {
2739
+ const cwd = process.cwd();
2740
+ const act = action?.toLowerCase() || (options.foreground ? "foreground" : "status");
2741
+ const daemonOpts = {
2742
+ interval: options.interval ? parseInt(options.interval, 10) : void 0,
2743
+ reviewInterval: options.reviewInterval ? parseInt(options.reviewInterval, 10) : void 0,
2744
+ autoworkInterval: options.autoworkInterval ? parseInt(options.autoworkInterval, 10) : options.interval ? parseInt(options.interval, 10) : void 0,
2745
+ routines: options.routines ? options.routines.split(",").map((r) => r.trim()) : void 0,
2746
+ model: options.model,
2747
+ foreground: options.foreground
2748
+ };
2749
+ if (act === "start") {
2750
+ if (options.foreground) {
2751
+ await runDaemonLoop(cwd, daemonOpts);
2752
+ return;
2753
+ }
2754
+ try {
2755
+ const state2 = await startBackgroundDaemon(cwd, daemonOpts);
2756
+ console.log(pc11.green(`
2757
+ \u2713 Background agent daemon started successfully.`));
2758
+ console.log(pc11.dim(` PID: ${state2.pid}`));
2759
+ console.log(pc11.dim(` Peer Review Watchdog: Every ${state2.reviewIntervalMinutes} minutes (zero-cost PR preflight)`));
2760
+ console.log(pc11.dim(` Autowork Backlog Scan: Every ${state2.autoworkIntervalMinutes} minutes`));
2761
+ console.log(pc11.dim(` Routines: ${state2.routines.join(", ")}`));
2762
+ console.log(pc11.dim(` Log file: .jonah-fleet/daemon.log`));
2763
+ console.log(pc11.dim(` Run 'jonah-fleet daemon status' or 'jonah-fleet daemon stop' to manage.`));
2764
+ } catch (err) {
2765
+ console.error(pc11.red(`
2766
+ \u2717 Failed to start daemon: ${err.message}`));
2767
+ process.exit(1);
2768
+ }
2769
+ return;
2770
+ }
2771
+ if (act === "stop") {
2772
+ if (!isDaemonRunning(cwd)) {
2773
+ console.log(pc11.yellow(`
2774
+ \u26A0\uFE0F No local agent daemon is currently running in this repository.`));
2775
+ return;
2776
+ }
2777
+ const state2 = readDaemonState(cwd);
2778
+ console.log(pc11.cyan(`
2779
+ Stopping background agent daemon (PID ${state2?.pid})...`));
2780
+ const stopped = await stopDaemon(cwd);
2781
+ if (stopped) {
2782
+ console.log(pc11.green(`\u2713 Local agent daemon stopped successfully.`));
2783
+ } else {
2784
+ console.error(pc11.red(`\u2717 Could not terminate daemon process.`));
2785
+ process.exit(1);
2786
+ }
2787
+ return;
2788
+ }
2789
+ if (act === "foreground") {
2790
+ await runDaemonLoop(cwd, daemonOpts);
2791
+ return;
2792
+ }
2793
+ const running = isDaemonRunning(cwd);
2794
+ const state = readDaemonState(cwd);
2795
+ const activeWorktrees = await listActiveWorktrees(cwd);
2796
+ console.log(pc11.cyan(`
2797
+ \u{1F916} Jonah Fleet Local Daemon Status
2798
+ `));
2799
+ if (running && state) {
2800
+ console.log(` Status: ${pc11.green(pc11.bold("RUNNING"))}`);
2801
+ console.log(` PID: ${state.pid}`);
2802
+ console.log(` Started: ${new Date(state.startedAt).toLocaleString()}`);
2803
+ console.log(` Peer Review Cadence: Every ${state.reviewIntervalMinutes} minutes (0-token fast preflight)`);
2804
+ console.log(` Autowork Cadence: Every ${state.autoworkIntervalMinutes} minutes`);
2805
+ console.log(` Routines: ${state.routines.join(", ")}`);
2806
+ console.log(` Current State: ${state.status === "working" ? pc11.yellow("WORKING on " + state.activeRoutine) : pc11.green("IDLE")}`);
2807
+ if (state.lastReviewCheckAt) {
2808
+ console.log(` Last Review Check: ${new Date(state.lastReviewCheckAt).toLocaleTimeString()}`);
2809
+ }
2810
+ if (state.lastAutoworkCheckAt) {
2811
+ console.log(` Last Autowork Check: ${new Date(state.lastAutoworkCheckAt).toLocaleTimeString()}`);
2812
+ }
2813
+ } else {
2814
+ console.log(` Status: ${pc11.gray("STOPPED")}`);
2815
+ console.log(pc11.dim(` Run 'jonah-fleet daemon start' to start the local worker daemon.`));
2816
+ }
2817
+ console.log(`
2818
+ Active Worktrees: ${activeWorktrees.length}`);
2819
+ for (const wt of activeWorktrees) {
2820
+ console.log(pc11.dim(` - [${wt.branch}] ${wt.path}`));
2821
+ }
2822
+ console.log("");
2823
+ }
2824
+
1174
2825
  // src/index.ts
1175
2826
  var program = new Command();
1176
2827
  program.name("jonah-fleet").description("Manage autonomous agent fleet, prompt routines, workflows, and skills").version(FLEET_VERSION);
1177
- 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) => {
2828
+ 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) => {
2829
+ await runRoutineCommand(routine, options);
2830
+ });
2831
+ program.command("daemon [action]").description("Manage background local worker daemon polling for unclaimed issues and pull requests").option("-i, --interval <minutes>", "Legacy global polling interval in minutes (default: 30)").option("--review-interval <minutes>", "Peer Review watchdog cadence in minutes (default: 3)").option("--autowork-interval <minutes>", "Autowork backlog cadence in minutes (default: 30)").option("-r, --routines <list>", "Comma-separated routines to run (default: peer-review,autowork)").option("-m, --model <model>", "LLM model override").option("--foreground", "Run daemon in foreground with live console logs").action(async (action, options) => {
2832
+ await runDaemonCommand(action, options);
2833
+ });
2834
+ 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) => {
1178
2835
  await runInit(options);
1179
2836
  });
1180
2837
  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) => {
@@ -1186,7 +2843,11 @@ program.command("status").description("Check the status, health, and drift of in
1186
2843
  program.command("monitor [repos...]").description("Monitor health, active claims, PR review loops, and token spend across fleet repositories").option("-t, --tokens", "Display detailed per-agent token and cost breakdown", false).option("--detailed", "Display detailed metrics breakdown", false).option("-j, --json", "Output telemetry as JSON", false).option("-w, --watch", "Live watch and refresh dashboard", false).option("-i, --interval <seconds>", "Refresh interval in seconds for watch mode", "10").option("-a, --all", "Query all registered repositories from config and manifest", false).option("--add <repo>", "Add a repository to the fleet registry").option("--remove <repo>", "Remove a repository from the fleet registry").action(async (repos, options) => {
1187
2844
  await runMonitor({ ...options, repos });
1188
2845
  });
1189
- program.command("contribute").description("Submit local prompt improvements back upstream to jonah-fleet").option("-t, --title <title>", "Contribution PR title").option("-b, --body <body>", "Contribution PR description").action(async (options) => {
2846
+ 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) => {
1190
2847
  await runContribute(options);
1191
2848
  });
2849
+ 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) => {
2850
+ const action = options.emit ? "emit" : "aggregate";
2851
+ await runTelemetry({ ...options, repos, action });
2852
+ });
1192
2853
  program.parse(process.argv);