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